Sequences#

The sequences library contains functions and classes relating to sequences and ordered structures of rational numbers, coprime integer pairs, and Farey sequences.

These are described below in some detail.

Enumerations#

The rationals() function is an enumeration function for the rationals and generates them as ContinuedFraction objects. There are several different kinds of enumerations, and these are described and depicted in some detail below.

Cantor Diagonalisation#

This is an enumeration described by the diagram below, where the red arrows describe the enumeration path:

Cantor diagonalisation: A method of counting the Rational Numbers

This method is based on first representing all (positive) integer fractions on a two-dimensional grid, which we may call the Cantor grid. On this grid all fractions in the \(i\)-th row have the numerator \(i\), while all the fractions in \(j\)-th column have the denominator \(j\). All integer pairs \((a, b)\) can thus be represented as fractions on the grid, although not all the fractions need to be counted: the rationals correspond to the irreducible fractions, while the composite fractions, which appear in grey in the diagram, are omitted as they are reducible to the rationals.

The rationals() function can be used to perform this enumeration by specifying the enumeration="cantor diagonal" argument. Here is an example for the first 20 rationals in this enumeration:

>>> from continuedfractions.sequences import rationals
>>> rats = rationals(enumeration="cantor diagonal")
>>> for i, r in enumerate(rats, start=1):
...     print(r, end=', ')
...     if i == 19:
...         print(next(rats))
...         break
...
1, 2, 1/2, 1/3, 3, 4, 3/2, 2/3, 1/4, 1/5, 5, 6, 5/2, 4/3, 3/4, 2/5, 1/6, 1/7, 3/5, 5/3

By default the rationals() function generates only positive rational numbers (positive_only=True), and does so in the form of ContinuedFraction objects, e.g.:

>>> rats = rationals(enumeration="cantor diagonal")
>>> first_twenty = [next(rats) for _ in range(20)]
>>> first_twenty
[ContinuedFraction(1, 1), ContinuedFraction(2, 1), ContinuedFraction(1, 2), ContinuedFraction(1, 3), ContinuedFraction(3, 1), ContinuedFraction(4, 1), ContinuedFraction(3, 2), ContinuedFraction(2, 3), ContinuedFraction(1, 4), ContinuedFraction(1, 5), ContinuedFraction(5, 1), ContinuedFraction(6, 1), ContinuedFraction(5, 2), ContinuedFraction(4, 3), ContinuedFraction(3, 4), ContinuedFraction(2, 5), ContinuedFraction(1, 6), ContinuedFraction(1, 7), ContinuedFraction(3, 5), ContinuedFraction(5, 3)]

To include negative rational numbers, as well as \(0\), the function should be called with the positive_only=False optional argument. For example:

>>> rats = rationals(enumeration="cantor diagonal", positive_only=False)
>>> first_twenty_plus_zero = [next(rats) for _ in range(21)]
>>> first_twenty_plus_zero
[ContinuedFraction(0, 1), ContinuedFraction(1, 1), ContinuedFraction(-1, 1), ContinuedFraction(2, 1), ContinuedFraction(-2, 1), ContinuedFraction(1, 2), ContinuedFraction(-1, 2), ContinuedFraction(1, 3), ContinuedFraction(-1, 3), ContinuedFraction(3, 1), ContinuedFraction(-3, 1), ContinuedFraction(4, 1), ContinuedFraction(-4, 1), ContinuedFraction(3, 2), ContinuedFraction(-3, 2),
ContinuedFraction(2, 3), ContinuedFraction(-2, 3), ContinuedFraction(1, 4), ContinuedFraction(-1, 4), ContinuedFraction(1, 5),
ContinuedFraction(-1, 5)]

Note

The function generates infinitely, so should be used carefully: to limit the generation conditions may be applied in the form of integer or float-valued upper bounds, or in terms of bounds on the number of terms that are generated. User-defined options for limiting or configuring the generation may be added as future enhancements.

Also, printing ContinuedFraction objects to the console produces strings of the form x or x/y where x and y are integers, because ContinuedFraction is a subclass of Fraction.

The Cantor diagonal enumeration can be understood in terms of the diagonals \(D_n\) on the grid: the \(n\)-th diagonal \(D_n\) is the subsequence of length \(n\) given by:

\[D_n := \left( \frac{n}{1},\frac{n - 1}{2},\frac{n - 2}{3},\ldots,\frac{1}{n} \right), \hskip{1em}n \geq 1\]

Note that in this sequence the numerators form a decreasing arithmetic sequence \((n. n - 1, n - 2, \ldots, 1)\) with common difference \(1\), and the denominators form an increasing arithmetic sequence \((1, 2, 3, \ldots, n)\) with common difference \(1\). Also, these diagonals include composite fractions, and we don’t consider counting order until the enumeration starts. So we have the diagonals:

\[\begin{split}\begin{align} D_1 &= \left( \frac{1}{1} \right) \\ D_2 &= \left( \frac{2}{1}, \frac{1}{2}, \right) \\ D_3 &= \left( \frac{3}{1}, \frac{2}{2}, \frac{3}{1}, \right) \\ D_4 &= \left( \frac{4}{1}, \frac{3}{2}, \frac{2}{3}, \frac{1}{4} \right) \\ D_5 &= \left( \frac{5}{1}, \frac{4}{2}, \frac{3}{3}, \frac{2}{4}, \frac{1}{5} \right) \\ \ldots \end{align}\end{split}\]

The Cantor diagonal enumeration of the rationals is simply the enumeration on the diagonals given by the following rules:

  • Count \(D_1\) first.

  • For \(n = 2,3,4,\ldots\) in that order, count \(D_n\) from left to right if \(n\) is even, or from right to left if \(n\) is odd.

  • Omit composite fractions.

Another way to think of this enumeration is in terms of the weight \(w\) of a fraction \(\frac{a}{b}\) (not necessarily in reduced form) which can be defined as the positive integer:

\[w\left(\frac{a}{b}\right) = |a| +| b|\]

By definition the \(n\)-th diagonal \(D_n\) contains all fractions of weight \(n + 1\), and as \(\lim_{n \to \infty}\) all fractions of all weights, and thus all rational numbers, are included in the enumeration.

There is a “transposed” version of this enumeration in which, after starting with \(D_1\) as before, we reverse the rules described above:

  • Count \(D_1\) first.

  • For \(n = 1,2,3,\ldots\) in that order, count \(D_n\) from right to left if \(n\) is even, or from left to right if \(n\) is odd.

  • Omit composite fractions.

This enumeration can be performed with the rationals() method function and the enumeration="cantor diagonal transposed" argument:

>>> rats = rationals(enumeration="cantor diagonal transposed")
>>> for i, r in enumerate(rats, start=1):
...     print(r, end=', ')
...     if i == 19:
...         print(next(rats))
...         break
...
1, 1/2, 2, 3, 1/3, 1/4, 2/3, 3/2, 4, 5, 1/5, 1/6, 2/5, 3/4, 4/3, 5/2, 6, 7, 5/3, 3/5

The enumeration is graphically depicted below:

Another version of Cantor diagonalisation

The resulting sequence can be obtained from the numbers in the first sequence by transposing the numerators and denominators.

Rectilinear Enumeration#

There are some other interesting enumeration methods of rationals on the Cantor grid, including one, described here, which may be called “rectilinear”. This is graphically depicted below:

Rectilinear enumeration: A method of counting the Rational Numbers

(As with Cantor diagonalisation, the composite fractions appear in grey, and are not counted.) This enumeration can be understood more clearly in terms of (finite) subsequences \(⅃_n\) that appear as reverse-L shapes in the diagram: so \(⅃_n\) is the subsequence given by:

\[⅃_n := \left( \frac{n}{1},\frac{n}{2},\frac{n}{3},\ldots,\frac{n}{n},\frac{n - 1}{n},\frac{n - 2}{n},\frac{n - 3}{n},\ldots,\frac{1}{n}\right), \hskip{1em} n \geq 1\]

In the \(⅃_n\), as with the Cantor diagonals \(D_n\) described above, we don’t consider counting order until we actually start the enumeration. Here are the first five \(⅃_n\):

\[\begin{split}\begin{align} ⅃_1 &= \left( \frac{1}{1} \right) \\ ⅃_2 &= \left( \frac{2}{1}, \frac{2}{2}, \frac{1}{2}, \right) \\ ⅃_3 &= \left( \frac{3}{1}, \frac{3}{2}, \frac{3}{3}, \frac{2}{3}, \frac{1}{3} \right) \\ ⅃_4 &= \left( \frac{4}{1}, \frac{4}{2}, \frac{4}{3}, \frac{4}{4}, \frac{3}{4}, \frac{2}{4}, \frac{1}{4} \right) \\ ⅃_5 &= \left( \frac{5}{1}, \frac{5}{2}, \frac{5}{3}, \frac{5}{4}, \frac{5}{5}, \frac{4}{5}, \frac{3}{5}, \frac{2}{5}, \frac{1}{5} \right) \\ \ldots \end{align}\end{split}\]

Each \(⅃_n\) is a subsequence of length \(2n - 1\), and we can decompose it into two smaller subsequences \(⅃_{n,1}\) and \(⅃_{n,2}\) given by:

\[\begin{split}\begin{align} ⅃_{n,1} &:= \left( \frac{n}{1},\frac{n}{2},\ldots,\frac{n}{n} \right) \\ ⅃_{n,2} &:= \left( \frac{n - 1}{n},\frac{n - 2}{n},\ldots,\frac{1}{n} \right) \end{align}\end{split}\]

These are the horizontal and vertical segments that make up \(⅃_n\), and have lengths \(n\) and \(n - 1\) respectively. The rectilinear enumeration of the rationals is simply an enumeration on the \(⅃_n\) given by the following rules:

  • Count \(⅃_1\) first.

  • For \(n = 2,3,4,\ldots\) in that order, first count \(⅃_{n,1}\) from left to right and then \(⅃_{n,2}\) from bottom to top if \(n\) is even, or, if \(n\) is odd first count \(⅃_{n,2}\) from top to bottom and then \(⅃_{n,1}\) from right to left.

  • Omit composite fractions.

This enumeration can be performed with the rationals() method function and the enumeration="rectilinear" argument:

>>> rats = rationals(enumeration="rectilinear")
>>> for i, r in enumerate(rats, start=1):
...     print(r, end=', ')
...     if i == 19:
...         print(next(rats))
...         break
...
1, 2, 1/2, 1/3, 2/3, 3/2, 3, 4, 4/3, 3/4, 1/4, 1/5, 2/5, 3/5, 4/5, 5/4, 5/3, 5/2, 5, 6

An interesting property of the \(⅃_n\) is that, for each \(n\), the sequence of weights \(w\left(⅃_n\right)\) forms a palindromic sequence of length \(2n - 1\) starting and finishing with the number \(n + 1\):

\[w \left(⅃_n\right) = \left(\overbrace{n + 1, n + 2, n + 3, \ldots,}^{\text{+ve arithmetic sequence}} 2n \underbrace{,2n - 1, 2n - 2, 2n - 3, \ldots, n + 1}_{\text{-ve arithmetic sequence}}\right)\]

As with the Cantor diagonal method, there is a transposed version of the rectilinear enumeration, where the same subsequences \(⅃_n\) are involved but counted in reverse order and depending on whether \(n\) is even or odd. This enumeration is graphically depicted below:

Rectilinear enumeration (transposed): A method of counting the Rational Numbers

This enumeration is described by the following rules:

  • Count \(⅃_1\) first.

  • For \(n = 2,3,4,\ldots\) in that order, first count \(⅃_{n,2}\) from top to bottom and then \(⅃_{n,1}\) from right to left if \(n\) is even, or, if \(n\) is odd first count \(⅃_{n,1}\) from left to right and then \(⅃_{n,2}\) from bottom to top.

  • Omit composite fractions.

The enumeration can be performed with the rationals() method function using the enumeration="rectilinear transposed" argument:

>>> rats = rationals(enumeration="rectilinear transposed")
>>> for i, r in enumerate(rats, start=1):
...     print(r, end=', ')
...     if i == 19:
...         print(next(rats))
...         break
...
1, 1/2, 2, 3, 3/2, 2/3, 1/3, 1/4, 3/4, 4/3, 4, 5, 5/2, 5/3, 5/4, 4/5, 3/5, 2/5, 1/5, 1/6

Generalisations#

Although not currently supported by the rationals() function, the rectilinear enumeration can be generalised by varying the length of the “initial path” of the enumeration starting from \(\frac{1}{1}\). If we denote the length of this initial path by \(\lambda\), then \(\frac{1}{1}\) was followed by either \(\frac{2}{1}\) in the first kind of rectilinear enumeration that was described, or by \(\frac{1}{2}\) in the rectilinear transposed enumeration, so that \(\lambda = 1\). For \(\lambda > 1\) we can define the initial path as either:

\[\frac{1}{1} \rightarrow \frac{2}{1} \rightarrow \cdots \rightarrow \frac{\lambda + 1}{1}\]

for a downward path, or as:

\[\frac{1}{1} \rightarrow \frac{1}{2} \rightarrow \cdots \rightarrow \frac{1}{\lambda + 1}\]

for a right-ward path. If we choose \(\lambda = 2\) and opt for a downward initial path then this initial path is \(\frac{1}{1} \rightarrow \frac{2}{1} \rightarrow \frac{3}{1}\), and we can enumerate using the rectilinear approach as follows:

Rectilinear enumeration with an initial down path of length 2: A method of counting the Rational Numbers

For each \(\lambda = 1,2,3,\ldots\) we get a slightly different, more “elongated” rectilinear enumeration, which also shows that these form a countably infinite (\(\aleph_0\)) subset of the set of all enumerations of the rationals. There are also natural transposes of these enumerations, similar to what has been described above for the rectilinear enumeration with \(\lambda = 1\).

Mediants#

The (simple) mediant of two rational numbers \(\frac{a}{b}\) and \(\frac{c}{d}\), where \(b, d, b + d \neq 0\), is defined as the rational number:

\[\frac{a + c}{b + d}\]

Given two ContinuedFraction instances it is possible to compute their mediant using the mediant() method:

>>> ContinuedFraction(1, 2).mediant(ContinuedFraction(2, 3))
ContinuedFraction(3, 5)

The result is also a ContinuedFraction instance.

Properties#

Assuming that \(\frac{a}{b} < \frac{c}{d}\) and \(bd > 0\) (which implies both \(\frac{a}{b}\) and \(\frac{c}{d}\) have the same sign) the mediant above has the property that:

\[\frac{a}{b} < \frac{a + c}{b + d} < \frac{c}{d}\]

From the assumptions above this can be proved easily from the following relations:

\[\begin{split}\begin{align} \frac{a}{b} < \frac{c}{d} &\iff \frac{c}{a} > \frac{d}{b} \iff \frac{a}{c} < \frac{b}{d} \\ \frac{a + c}{b + d} &= \frac{a}{b} \cdot \frac{1 + \frac{c}{a}}{1 + \frac{d}{b}} \\ &= \frac{c}{d} \cdot \frac{1 + \frac{a}{c}}{1 + \frac{b}{d}} \end{align}\end{split}\]

Mediants can give good rational approximations to real numbers. We can illustrate the core mediant property with some examples.

>>> ContinuedFraction('0.5').right_mediant(Fraction(2, 3))
ContinuedFraction(3, 5)
>>> tuple(ContinuedFraction('0.6').coefficients)
(0, 1, 1, 2)
>>> ContinuedFraction(1, 2).mediant(ContinuedFraction('2/3'))
ContinuedFraction(3, 5)
>>> assert ContinuedFraction(1, 2) < ContinuedFraction(1, 2).mediant(Fraction(3, 4)) < ContinuedFraction(3, 4)
# True

In particular, the mediant \(\frac{a + c}{b + d}\) of \(\frac{a}{b}\) and \(\frac{c}{d}\) has the property that if \(bc - ad = 1\) then \(\frac{a + c}{b + d}\) is the fraction with the smallest denominator lying in the (open) interval \((\frac{a}{b}, \frac{c}{d})\). As \(\frac{1}{2}\) and \(\frac{2}{3}\) satisfy the relation \(bc - ad = 2\cdot2 - 1\cdot3 = 4 - 3 = 1\) it follows that their mediant \(\frac{3}{5}\) is the “next” (or “first”) fraction after \(\frac{1}{2}\), but before \(\frac{2}{3}\), compared to any other fraction in that interval with a denominator \(\geq b + d = 5\).

This is an ordering property that links mediants to ordered sequences of rational numbers such as Farey sequences, which are described in more detail here, and also tree orderings such as the Stern-Brocot tree.

Left- and Right-Mediants#

The concept of the simple mediant of two fractions of \(\frac{a}{b}\) and \(\frac{c}{d}\) as given above can be generalised to \(k\)-th left- and right-mediants: for a positive integer \(k\) the \(k\)-th left mediant of \(\frac{a}{b}\) and \(\frac{c}{d}\) can be defined as:

\[\frac{ka + c}{kb + d}, \hskip{3em} k \geq 1\]

while the \(k\)-th right mediant can be defined as:

\[\frac{a + kc}{b + kd}, \hskip{3em} k \geq 1\]

For \(k = 1\) the left- and right-mediants are identical to the simple mediant \(\frac{a + c}{b + d}\), but for \(k > 1\) the \(k\)-th left-mediant is less than the \(k\)-th right mediant. Using the assumptions \(\frac{a}{b} < \frac{c}{d}\) and \(bd > 0\), the proof is given by:

\[\begin{split}\begin{align} \frac{a + kc}{b + kd} - \left(\frac{ka + c}{kb + d}\right) &= \frac{(bc - ad)(k^2 - 1)}{(b + kd)(kb + d)} \\ &\geq 0 \end{align}\end{split}\]

where equality holds if and only if \(k = 1\).

Left- and right-mediants can be constructed easily using the ContinuedFraction class, which provides the left_mediant() and right_mediant() methods.

Here are some examples of constructing left-mediants:

>>> cf1 = ContinuedFraction('1/2')
>>> cf2 = ContinuedFraction(3, 5)
# The default `k = 1` gives you the common, simple mediant of the two rationals
>>> cf1.left_mediant(cf2)
ContinuedFraction(4, 7)
>>> cf1.left_mediant(cf2, k=2)
ContinuedFraction(5, 9)
>>> cf1.left_mediant(cf2, k=100)
ContinuedFraction(103, 205)
>>> cf1.left_mediant(cf2, k=100).as_decimal()
Decimal('0.5024390243902439024390243902')

and right-mediants (with the same two fractions cf1 and cf2 as above):

# The default `k = 1` gives you the common, simple mediant of the two rationals
>>> cf1.right_mediant(cf2)
ContinuedFraction(4, 7)
>>> cf1.right_mediant(cf2, k=2)
ContinuedFraction(7, 12)
>>> cf1.right_mediant(cf2, k=100)
ContinuedFraction(301, 502)
>>> cf1.right_mediant(cf2, k=100).as_decimal()
Decimal('0.5996015936254980079681274900')

As \(k \longrightarrow \infty\) the sequences of left- and right-mediants separate into two, strictly monotonic, sequences converging to opposite limits: the left-mediants form a strictly decreasing sequence lower-bounded by \(\frac{a}{b}\):

\[\frac{a}{b} < \cdots < \frac{3a + c}{3b + d} < \frac{2a + c}{2b + d} < \frac{a + c}{b + d} < \frac{c}{d}\]

thus converging to \(\frac{a}{b}\):

\[\lim_{k \to \infty} \frac{ka + c}{kb + d} = \lim_{k \to \infty} \frac{a + \frac{c}{k}}{b + \frac{d}{k}} = \frac{a}{b}\]

while the right-mediants form a strictly increasing sequence upper-bounded by \(\frac{c}{d}\):

\[\frac{a}{b} < \frac{a + c}{b + d} < \frac{a + 2c}{b + 2d} < \frac{a + 3c}{b + 3d} < \cdots < \frac{c}{d}\]

thus converging to \(\frac{c}{d}\):

\[\lim_{k \to \infty} \frac{a + kc}{b + kd} = \lim_{k \to \infty} \frac{\frac{a}{k} + c}{\frac{b}{k} + d} = \frac{c}{d}\]

We can see this using the same two fractions cf1 and cf2 as above, starting with the left-mediants:

>>> cf1.left_mediant(cf2)
ContinuedFraction(4, 7)
>>> cf1.left_mediant(cf2).as_decimal()
Decimal('0.5714285714285714285714285714')
>>> cf1.left_mediant(cf2, k=10).as_decimal()
Decimal('0.52')
>>> cf1.left_mediant(cf2, k=100).as_decimal()
Decimal('0.5024390243902439024390243902439024390243902439024390243902439024390243902439024390243902439024390244')
>>> cf1.left_mediant(cf2, k=10 ** 6)
ContinuedFraction(1000003, 2000005)
>>> cf1.left_mediant(cf2, k=10 ** 6).as_decimal()
Decimal('0.5000002499993750015624960938')

and then the right-mediants:

>>> cf1.right_mediant(cf2).as_decimal()
Decimal('0.5714285714285714285714285714')
>>> cf1.right_mediant(cf2, k=10).as_decimal()
Decimal('0.5961538461538461538461538462')
>>> cf1.right_mediant(cf2, k=100).as_decimal()
Decimal('0.5996015936254980079681274900')
>>> cf1.right_mediant(cf2, k=10 ** 6)
ContinuedFraction(3000001, 5000002)
>>> cf1.right_mediant(cf2, k=10 ** 6).as_decimal()
Decimal('0.5999999600000159999936000026')

A particular class of right-mediants are known as semiconvergents, and are described in more detail here.

Coprime Integers#

Two integers \(a, b\) are said to be coprime (or relatively prime) if their greatest common divisor (GCD) is \(1\) - this is also written as \((a, b) = 1\). This occurs if and only \(a\) has no prime factors in common with \(b\).

The notion of coprimality can be extended to finite sets of integers: a finite set of integers \(S = \{a, b, c, \ldots\}\) can be called coprime if the GCD of all the integers in \(S\) is \(1\). A stronger condition is met by \(S\) if it is pairwise coprime, which means the GCD of any two integers in \(S\) is \(1\). The latter implies the former, but the converse does not necessarily hold.

Coprimality is important in several basic ways for the sequences that can be generated with the sequences library, and in this section, some effective methods for generating coprime integer pairs are described, with code examples.

A Simple Approach#

The coprime_pairs() function is a simple but relatively fast generator of pairs of coprime integers bounded by a given integer \(n \geq 1\). Here’s an example for \(n = 5\):

>>> tuple(coprime_pairs(5))
(1, 1), (2, 1), (3, 1), (3, 2), (4, 1), (4, 3), (5, 1), (5, 2), (5, 3), (5, 4)

It can be verified that the number of coprime pairs returned by the function here, namely, \(10\), is indeed equal to \(\phi(1) + \phi(2) + \phi(3) + \phi(4) + \phi(5) = 10\), where \(\phi(n)\) is Euler’s totient function that counts the number of (positive) integers coprime to a given integer \(n \geq 1\), and \(\phi(1) = 1\). The function that counts the value of \(\sum_{k=1}^n \phi(k)\) for a given \(n\) is the summatory totient function \(\Phi(n)\), and the number of coprime pairs returned by coprime_pairs() is equal to \(\Phi(n)\). Here are a few examples for \(n = 1,\ldots,5\):

>>> sum(1 for _ in coprime_pairs(1))
1
>>> sum(1 for _ in coprime_pairs(2))
2
>>> sum(1 for _ in coprime_pairs(3))
4
>>> sum(1 for _ in coprime_pairs(4))
6
>>> sum(1 for _ in coprime_pairs(5))
10
>>> sum(1 for _ in coprime_pairs(10))
32

KSRM Trees#

Coprime integer pairs can also be generated from trees, and a particularly interesting tree-based generative approach is described below.

The KSRMTree class is a class implementation of two ternary trees for representing (and generating) all pairs of (positive) coprime integers, as presented in separate papers by A. R. Kanga, and R. Saunders and T. Randall, and D. W. Mitchell.

Note

The class is named KSRMTree purely for convenience, but it is actually a representation of two (ternary) trees.

The basic idea is that all pairs of (positive) coprime integers \((a, b)\), where \(1 \leq b < a\), can be represented as nodes in one of two ternary trees, the first which has the “parent” node \((2, 1)\) and the second which has the parent node \((3, 1)\). Each node, starting with the parent nodes, has three children given by the relations:

\[\begin{split}(a^\prime, b^\prime) = \begin{cases} (2a - b, a), \hskip{3em} \text{ branch #} 1 \\ (2a + b, a), \hskip{3em} \text{ branch #} 2 \\ (a + 2b, b), \hskip{3em} \text{ branch #} 3 \end{cases}\end{split}\]

all of which are coprime. The children of these nodes by the same branch relations are also coprime, and so on. For the original proofs please refer to the papers.

We can inspect the roots and branches by constructing a KSRMTree instance, and looking at the roots and branches properties.

>>> tree = KSRMTree()
>>> tree.roots
((2, 1), (3, 1))
>>> tree.branches
(NamedCallableProxy("KSRM tree branch #1: (x, y) |--> (2x - y, x)"),
 NamedCallableProxy("KSRM tree branch #2: (x, y) |--> (2x + y, x)"),
 NamedCallableProxy("KSRM tree branch #3: (x, y) |--> (x + 2y, y)"))

The branches property is a tuple of callables (instances of NamedCallableProxy), one for each of the three branches. Each callable takes two (coprime) integers \(a, b\), with \(1 \leq b < a\), as arguments. The nodes can be generated manually as follows:

# Generating the 1st generation of children for the root ``(2, 1)``
>>> [tree.branches[k](2, 1) for k in range(3)]
[(3, 2), (5, 2), (4, 1)]
# Generating the 1st generation of children for the root ``(3, 1)``
>>> [tree.branches[k](3, 1) for k in range(3)]
[(5, 3), (7, 3), (5, 1)]

The generation of coprime pairs via the trees can then be implemented with a generative search procedure that starts separately from the parents \((2, 1)\) and \((3, 1)\), and applies the functions given by the mappings below to each parent:

\[\begin{split}(a, b) &\longmapsto \begin{cases} (2a - b, a), \hskip{3em} \text{ branch #} 1 \\ (2a + b, a), \hskip{3em} \text{ branch #} 2 \\ (a + 2b, b), \hskip{3em} \text{ branch #} 3 \end{cases}\end{split}\]

producing the “1st generation” of \(3 + 3 = 6\) pairs. This can be repeated ad infinitum as required.

Note

The tree with the root \((3, 1)\) only contains coprime pairs of odd integers, under the maps described above.

If we let \(k = 0\) denote the \(0\)-th generation consisting only of the two roots \((2, 1)\) and \((3, 1)\), then for \(k \geq 1\) the \(k\)-th generation, for either tree, will have a total of \(3^k\) children, the total number of all members up to and including the \(k\)-th generation will be \(1 + 3 + 3^2 + \ldots + 3^k = \frac{3^{k + 1} - 1}{2}\), and the total number of all members in both trees up to and including the \(k\)-th generation will be \(3^{k + 1} - 1\).

For \(k = 2\) (two generations) here are the trees, starting with the root \((2, 1)\):

The KSRM coprime pairs tree for the root `(2, 1)`, depth 2

and then the root \((3, 1)\):

The KSRM coprime pairs tree for the root `(3, 1)`, depth 2

The KSRMTree class contains one main search method search(), which is a wrapper and generator that implements the procedure described above.

>>> list(tree.search(1))
[(1, 1)]
>>> list(tree.search(2))
[(1, 1), (2, 1)]
>>> list(tree.search(3))
[(1, 1), (2, 1), (3, 2), (3, 1)]
>>> list(tree.search(5))
[(1, 1), (2, 1), (3, 2), (4, 3), (4, 1), (3, 1), (5, 4), (5, 3), (5, 2), (5, 1)]
>>> list(tree.search(10))
[(1, 1), (2, 1), (3, 2), (4, 3), (5, 4), (6, 5), (7, 6), (8, 7), (9, 8), (8, 3), (7, 2), (5, 2), (8, 5), (9, 2), (4, 1), (7, 4), (9, 4), (6, 1), (8, 1), (3, 1), (5, 3), (7, 5), (9, 7), (7, 3), (5, 1), (9, 5), (7, 1), (9, 1), (10, 9), (10, 7), (10, 3), (10, 1)]

The number of coprime pairs generated for a given \(n \geq 1\) is given by \(\Phi(n) = \sum_{k = 1}^n \phi(k)\).

The search() method is only a wrapper for the actual search function on roots, which is search_root(). This is also a generator, and implements a branch and bound, depth first search (DFS) of the KSRM trees, with pre-ordered traversal of nodes (current node -> left branch -> mid branch -> right branch), and backtracking and pruning on visited nodes. The backtracking function is implemented as the private method _backtrack().

Some examples are given below.

>>> list(tree.search_root(5, (2, 1)))
[(2, 1), (3, 2), (4, 3), (5, 4), (5, 2), (4, 1)]
>>> assert tree.roots[0] == (2, 1)
>>> list(tree.search_root(5, tree.roots[0]))
[(2, 1), (3, 2), (4, 3), (5, 4), (5, 2), (4, 1)]
>>> list(tree.search_root(5, (3, 1)))
[(3, 1), (5, 3), (5, 1)]
>>> assert tree.roots[1] == (3, 1)
>>> list(tree.search_root(5, tree.roots[1]))
[(3, 1), (5, 3), (5, 1)]

The result for a given \(n \geq 1\) is a generator of coprime pairs, yielded in order of traversal, starting from the (given) root node. The tree is only traversed for \(n > 1\). More details on the implementation, including the depth-first search, branch-and-bound, pruning and backtracking and so on can be found in the search_root() API documentation.

The implementation of search_root() is guaranteed to terminate for any given \(n\), as there is always a finite subset of nodes \((a, b)\) satisfying the conditions \(1 \leq b < a \leq n\) and \((a, b) = 1\), and nodes that don’t satisfy these conditions are discarded (pruned).

As the KSRM trees are infinite ternary trees the worst-case time and space complexity of a standard DFS, for a given \(n\), on either tree, are determined by the (variable) search depth \(d\), and the (constant) branching factor of \(3\). For space complexity the combination of backtracking and pruning “failed” nodes in the search ensures that for any given \(n\) the smallest fraction of nodes are stored in memory at any given time - see the _backtrack() and search_root() methods for more details.

Farey Sequences#

The farey_sequence() function can be used to generate Farey sequences:

>>> from continuedfractions.sequences import farey_sequence
>>> tuple(farey_sequence(10))
(FareyFraction(0, 1), FareyFraction(1, 10), FareyFraction(1, 9), FareyFraction(1, 8), FareyFraction(1, 7), FareyFraction(1, 6), FareyFraction(1, 5), FareyFraction(2, 9), FareyFraction(1, 4), FareyFraction(2, 7), FareyFraction(3, 10), FareyFraction(1, 3), FareyFraction(3, 8), FareyFraction(2, 5), FareyFraction(3, 7), FareyFraction(4, 9), FareyFraction(1, 2), FareyFraction(5, 9), FareyFraction(4, 7), FareyFraction(3, 5), FareyFraction(5, 8), FareyFraction(2, 3), FareyFraction(7, 10), FareyFraction(5, 7), FareyFraction(3, 4), FareyFraction(7, 9), FareyFraction(4, 5), FareyFraction(5, 6), FareyFraction(6, 7), FareyFraction(7, 8), FareyFraction(8, 9), FareyFraction(9, 10), FareyFraction(1, 1))

The result is a tuple of FareyFraction instances (just a plain subclass of ContinuedFraction) in ascending order of magnitude, starting with FareyFraction(0, 1) and ending with FareyFraction(1, 1).

The Farey sequence \(F_n\) of order \(n\) is an (ordered) sequence of (irreducible) rational numbers, called Farey fractions, in the closed unit interval \([0, 1]\), which can be defined as follows:

\[\begin{split}\begin{align} F_n = \left(\frac{b}{a}\right) \text{ s.t. } & 1 \leq b < a \leq n,\\ & \text{ or } b = 0, a = 1, \\ & \text{ or } b = a = 1 \end{align}\end{split}\]

The special case is when \(n = 1\) and \(F_1\) is given by:

\[F_1 = \left(\frac{0}{1}, \frac{1}{1}\right)\]

For \(n \geq 2\) the requirement that \(1 \leq b < a \leq n\) means the fractions \(\frac{b}{a} \neq \frac{0}{1}, \frac{1}{1}\) must be irreducible, which implies coprimality \((a, b) = 1\).

The elements of \(F_n\) are written in ascending order of magnitude. The first five Farey sequences are listed below:

\[\begin{split}\begin{align} F_1 &= \left( \frac{0}{1}, \frac{1}{1} \right) \\ F_2 &= \left( \frac{0}{1}, \frac{1}{2}, \frac{1}{1} \right) \\ F_3 &= \left( \frac{0}{1}, \frac{1}{3}, \frac{1}{2}, \frac{2}{3}, \frac{1}{1} \right) \\ F_4 &= \left( \frac{0}{1}, \frac{1}{4}, \frac{1}{3}, \frac{1}{2}, \frac{2}{3}, \frac{3}{4}, \frac{1}{1} \right) \\ F_5 &= \left( \frac{0}{1}, \frac{1}{5}, \frac{1}{4}, \frac{1}{3}, \frac{2}{5}, \frac{1}{2}, \frac{3}{5}, \frac{2}{3}, \frac{3}{4}, \frac{4}{5}, \frac{1}{1} \right) \end{align}\end{split}\]

and this can be checked with the farey_sequence() function:

>>> tuple(farey_sequence(1))
(FareyFraction(0, 1), FareyFraction(1, 1))
>>> tuple(farey_sequence(2))
(FareyFraction(0, 1), FareyFraction(1, 2), FareyFraction(1, 1))
>>> tuple(farey_sequence(3))
(FareyFraction(0, 1), FareyFraction(1, 3), FareyFraction(1, 2), FareyFraction(2, 3), FareyFraction(1, 1))
>>> tuple(farey_sequence(4))
(FareyFraction(0, 1), FareyFraction(1, 4), FareyFraction(1, 3), FareyFraction(1, 2), FareyFraction(2, 3), FareyFraction(3, 4), FareyFraction(1, 1))
>>> tuple(farey_sequence(5))
(FareyFraction(0, 1), FareyFraction(1, 5), FareyFraction(1, 4), FareyFraction(1, 3), FareyFraction(2, 5), FareyFraction(1, 2), FareyFraction(3, 5), FareyFraction(2, 3), FareyFraction(3, 4), FareyFraction(4, 5), FareyFraction(1, 1))

For \(n > 1\) we can write the fractions in \(F_n\) as \(\frac{b}{a}\) where \(a > b\): the coprimality condition \((a, b) = 1\), combined with \(a \leq n\), means that \(F_n\) contains, for each \(a \leq n\), exactly \(\phi(a)\) fractions of the form \(\frac{b}{a}\) where \(a > b\) and \((a, b) = 1\), and \(\phi(k)\) is the totient function.

As \(F_n\) also contains the special fraction \(\frac{0}{1}\) as its initial element, it means that the length \(|F_n|\) of \(F_n\) is given by:

\[|F_n| = 1 + \phi(1) + \phi(2) + \cdots + \phi(n) = 1 + \Phi(n)\]

For \(n > 1\) the sequence \(F_n\) contains all elements of \(F_{n - 1}\). Thus, the length \(|F_n|\) can also be written as:

\[|F_n| = |F_{n - 1}| + \phi(n)\]

Note

For any \(n \geq 1\) the fraction \(\frac{1}{n}\) first occurs as a Farey fraction in the Farey sequence \(F_n\). Also, the fraction \(\frac{1}{2}\) is the middle term in any Farey sequence \(F_n\) where \(n \geq 2\).

As with coprime_pairs() the counts for farey_sequence(), which uses the former, can be checked using the summatory totient function:

>>> assert len(tuple(farey_sequence(1))) == 1 + sum(map(sympy.totient, range(1, 2))) == 2
>>> assert len(tuple(farey_sequence(2))) == 1 + sum(map(sympy.totient, range(1, 3))) == 3
>>> assert len(tuple(farey_sequence(3))) == 1 + sum(map(sympy.totient, range(1, 4))) == 5
>>> assert len(tuple(farey_sequence(4))) == 1 + sum(map(sympy.totient, range(1, 5))) == 7
>>> assert len(tuple(farey_sequence(5))) == 1 + sum(map(sympy.totient, range(1, 6))) == 11
>>> assert len(tuple(farey_sequence(10))) == 1 + sum(map(sympy.totient, range(1, 11))) == 33
>>> assert len(tuple(farey_sequence(100))) == 1 + sum(map(sympy.totient, range(1, 101))) == 3045
>>> assert len(tuple(farey_sequence(1000))) == 1 + sum(map(sympy.totient, range(1, 1001))) == 304193

Farey sequences have some interesting properties and connections with mediants and continued fractions, as described here. In relation to mediants there is the notion of Farey neighbours, which are simply adjacent or consecutive Farey fractions in a Farey sequence \(F_n\). Specifically, if fractions \(\frac{a}{b}\) and \(\frac{c}{d}\), with \(\frac{a}{b} < \frac{c}{d}\), are Farey neighbours in a Farey sequence \(F_n\), where we may assume that \(n\) is the smallest such index, then:

  • the mediant \(\frac{a + c}{b + d}\) is a Farey fraction which first appears in the Farey sequence \(F_{b + d}\).

  • the difference \(\frac{c}{d} - \frac{a}{b} = \frac{bc - ad}{bd} = \frac{1}{bd}\) is a Farey fraction which first appears in the Farey sequence \(F_{bd}\).

This can be checked using farey_sequence(), taking \(\frac{a}{b} = \frac{2}{3}\) and \(\frac{c}{d} = \frac{3}{4}\), which first occur as Farey neighbours in the Farey sequence \(F_4\):

>>> print(', '.join(map(str, farey_sequence(4))))
0, 1/4, 1/3, 1/2, 2/3, 3/4, 1
>>> print(', '.join(map(str, farey_sequence(7))))
0, 1/7, 1/6, 1/5, 1/4, 2/7, 1/3, 2/5, 3/7, 1/2, 4/7, 3/5, 2/3, 5/7, 3/4, 4/5, 5/6, 6/7, 1
>>> FareyFraction(2, 3).mediant(FareyFraction(3, 4))
FareyFraction(5, 7)
>>> assert FareyFraction(2, 3).mediant(FareyFraction(3, 4)) in farey_sequence(7)
>>> FareyFraction(3, 4) - FareyFraction(2, 3)
FareyFraction(1, 12)
>>> print(', '.join(map(str, farey_sequence(12))))
0, 1/12, 1/11, 1/10, 1/9, 1/8, 1/7, 1/6, 2/11, 1/5, 2/9, 1/4, 3/11, 2/7, 3/10, 1/3, 4/11, 3/8, 2/5, 5/12, 3/7, 4/9, 5/11, 1/2, 6/11, 5/9, 4/7, 7/12, 3/5, 5/8, 7/11, 2/3, 7/10, 5/7, 8/11, 3/4, 7/9, 4/5, 9/11, 5/6, 6/7, 7/8, 8/9, 9/10, 10/11, 11/12, 1
>>> assert FareyFraction(3, 4) - FareyFraction(2, 3) in farey_sequence(12)

References#

[1] Courant, R., Robbins, H., & Stewart, I. (1996). What is mathematics?: An elementary approach to ideas and methods (2nd ed.). Oxford University Press

[2] Hatcher, A. (2024, September). Topology of Numbers. American Mathematical Society. https://pi.math.cornell.edu/~hatcher/TN/TNbook.pdf

[3] Kanga, A. R. (1990). The Family Tree of Pythagorean Triplets. The Mathematical Gazette, 26(15), 15-17.

[4] Mitchell, D. W. (2001). An Alternative Characterisation of All Primitive Pythagorean Triples. The Mathematical Gazette, 85(503), 273-275. https://doi.org/10.2307/3622017

[5] Saunders, R., & Randall, T. (1994). The family tree of the Pythagorean triplets revisited. The Mathematical Gazette, 78(482), 190-193. https://doi.org/10.2307/3618576