OEIS Demo

Copy Markdown
Mix.install([
  {:oeis, "~> 0.7.1"},
  {:kino, "~> 0.14.0"},
  {:kino_vega_lite, "~> 0.1.13"}
])

Introduction

This notebook serves as an interactive guide to the oeis Elixir library. We will explore how to search the On-Line Encyclopedia of Integer Sequences (OEIS), retrieve detailed data, and visualize the results.

The primary entry point is the OEIS.search/2 function. It is designed to be flexible, accepting sequence IDs, lists of integers, or keyword queries.

Searching by ID

We will start by examining Recamán's sequence (A005132). This sequence is known for its non-monotonic, "hopping" behavior, which makes it particularly interesting to analyze.

recaman_seq =
  "A005132"
  |> OEIS.search()
  |> then(fn {:single, seq} -> seq end)
  |> tap(fn seq -> IO.puts("Found: #{seq.name}") end)

recaman_seq
|> Enum.with_index()
|> Enum.map(fn {val, idx} -> %{index: idx, value: val} end)
|> Kino.DataTable.new()

Note: The OEIS.Sequence struct implements the Enumerable protocol. This means you can pipe it directly into functions like Enum.with_index/1, treating the sequence object as if it were a simple list of its terms.

Searching by Sequence

If you have a list of numbers and want to identify the sequence, you can pass the list directly to the search function.

# Searching for Catalan numbers: 1, 1, 2, 5, 14
[1, 1, 2, 5, 14]
|> OEIS.search()
|> elem(1)
|> List.first()

Searching by Keyword/Author

You can also filter results by author or keyword. For instance, here is how to find sequences authored by Neil Sloane that are tagged with the "core" keyword.

[author: "Sloane", keyword: "core"]
|> OEIS.search()
|> then(fn {_status, search_results} -> search_results end)
|> Enum.map(fn seq ->
  %{id: seq.id, name: seq.name, author: seq.author}
end)
|> Kino.DataTable.new()

Streaming Results

For queries that may return a large number of results, the library provides a streaming interface. By passing stream: true, OEIS.search/2 returns a lazy Stream that fetches results in pages of 10 as you consume them.

This is particularly useful for building UIs that support infinite scrolling or for processing large numbers of sequences without loading them all into memory at once.

# Lazily fetch the first 25 sequences related to "fib"
"fib"
|> OEIS.search(stream: true)
|> Stream.take(25)
|> Enum.map(fn seq -> %{id: seq.id, name: seq.name} end)
|> Kino.DataTable.new()

Fetching More Data

Search results usually provide just a glimpse of the sequence—typically the first few dozen terms. To truly understand the behavior of a sequence like Recamán's, we need a larger dataset.

We can use OEIS.fetch_more_terms/2 to retrieve the full "b-file" from the OEIS, which often contains thousands of terms. This allows us to see the bigger picture.

expanded_seq =
  recaman_seq
  |> tap(fn seq -> IO.puts("Original count: #{Enum.count(seq)}") end)
  |> OEIS.fetch_more_terms()
  |> then(fn {:ok, seq} -> seq end)
  |> tap(fn seq -> IO.puts("New count: #{Enum.count(seq)}") end)

Now, let's visualize this data. By plotting the sequence, we can move beyond abstract numbers and observe the beautiful, chaotic structure that emerges to clearly see the individual steps.

expanded_seq
|> Enum.with_index()
|> Enum.map(fn {y, x} -> %{"x" => x, "y" => y} end)
|> then(fn data_points ->
  VegaLite.new(width: 800, height: 400)
  |> VegaLite.data_from_values(data_points, only: ["x", "y"])
  |> VegaLite.mark(:line, point: true, tooltip: true)
  |> VegaLite.encode_field(:x, "x", type: :quantitative, title: "Index")
  |> VegaLite.encode_field(:y, "y", type: :quantitative, title: "Value")
end)
|> Kino.VegaLite.new()

Mathematics is all about connections. Sequences in the OEIS often reference other related sequences. For example, Recamán's sequence might be related to other recurrence relations or number theoretic properties.

The OEIS.fetch_xrefs/2 function parses these cross-references and fetches their definitions for you, automatically handling the network requests in parallel.

# Fetch sequences related to Recamán's sequence (A005132)
# We use the recaman_seq variable from earlier
related_sequences = OEIS.fetch_xrefs(recaman_seq, max_concurrency: 5)

related_sequences
|> Enum.map(fn seq -> %{id: seq.id, name: seq.name} end)
|> Kino.DataTable.new()

For sequences with a large number of cross-references, you can also use stream: true to process them lazily.

# Lazily fetch the first 5 sequences related to Fibonacci (A000045)
"A000045"
|> OEIS.search()
|> then(fn {:single, seq} -> seq end)
|> OEIS.fetch_xrefs(stream: true)
|> Stream.take(5)
|> Enum.map(fn seq -> %{id: seq.id, name: seq.name} end)
|> Kino.DataTable.new()

Standardized Options

To give you precise control over your queries, most functions in this library support a consistent set of options. Here is how you can tune the behavior:

  • :timeout - The maximum time (in milliseconds) to wait for a response from the OEIS server. Default is 15,000ms.
  • :max_concurrency - When fetching multiple items (like related sequences), this limits how many requests happen at once. Default is 5.
  • :may_truncate - If set to true (the default), the library will truncate the provided terms and remove leading 0s or 1s. This often increases the chances of finding a match when you only have a partial or slightly different sequence of terms.
  • :respect_sign - When searching by a list of numbers, this determines if negative signs matter. Default is true.
  • :start - The zero-based index of the first result to return. Useful for paging through many results. Default is 0.
  • :stream - If true, returns an Elixir Stream that lazily fetches and emits results. Default is false.

Interactive Search Tool

Now it is your turn to explore. Use the form below to perform your own searches. You can try:

  • An ID: "A000032" (Lucas numbers)
  • A name: "Lucas numbers"
  • A list of terms: "1, 2, 3, 6, 11, 23"

Experiment with different queries to see what you can discover!

input = Kino.Input.text("Search Query")
form = Kino.Control.form([query: input], submit: "Search")
frame = Kino.Frame.new()

Kino.listen(form, fn %{data: %{query: query}} ->
  Kino.Frame.render(frame, Kino.Text.new("Searching..."))

  # Determine if input is a list of numbers or a string
  search_arg =
    if Regex.match?(~r/^[\d\s,]+$/, query) do
      query
      |> String.split([",", " "], trim: true)
      |> Enum.map(&String.to_integer/1)
    else
      query
    end

  search_arg
  |> OEIS.search()
  |> case do
    {:single, seq} ->
      [
        {"Info", Kino.Tree.new(seq)},
        {"Data", seq |> Enum.join(", ") |> Kino.Text.new()}
      ]
      |> Kino.Layout.tabs()

    {status, seqs} when status in [:multi, :partial] ->
      seqs
      |> Enum.map(&%{id: &1.id, name: &1.name})
      |> Kino.DataTable.new()

    {:no_match, msg} ->
      Kino.Text.new(msg)

    error ->
      "Error: #{inspect(error)}" |> Kino.Text.new()
  end
  |> then(&Kino.Frame.render(frame, &1))
end)

Kino.Layout.grid([form, frame], columns: 1)