Skip to content

API reference

The source of truth for what is public is src/qreals/__init__.py. Everything below is re-exported from the top-level qreals namespace; anything not listed here is internal and may change without notice. Each entry is generated from the docstring in the source.

The two computation paths

qreals.rational

The q-rational [p/s]_q as an exact rational function in q.

For a rational the continued fraction terminates, so the MGO formula produces a genuine element of Q(q) rather than a truncated series. This path keeps the result symbolic: q_rational(3, 2) returns (q**2 + q + 1)/(q + 1), not its Taylor coefficients.

Setting q = 1 collapses [n]_q = 1 + q + ... + q^{n-1} back to n, so every q-rational specialises to the ordinary rational at q = 1. The test suite uses that as the defining sanity check.

q_rational

q_rational(p: int, s: int) -> sp.Expr

[p/s]_q as a reduced rational function in q, for integers p, s != 0.

Source code in src\qreals\rational.py
def q_rational(p: int, s: int) -> sp.Expr:
    """[p/s]_q as a reduced rational function in q, for integers p, s != 0."""
    p = int(p)
    s = int(s)
    if s == 0:
        raise ZeroDivisionError("denominator zero")
    if p == s:
        return sp.Integer(1)
    quotients = sp.continued_fraction(sp.Rational(p, s))
    return mgo_build(make_even_length(list(quotients)))

q_int

q_int(n: int) -> sp.Expr

[n]_q for n in Z, as an exact expression in q.

Source code in src\qreals\rational.py
def q_int(n: int) -> sp.Expr:
    """[n]_q for n in Z, as an exact expression in q."""
    n = int(n)
    if n == 0:
        return sp.Integer(0)
    if n > 0:
        return sum((q**i for i in range(n)), sp.Integer(0))
    return -q_int(-n) / q ** (-n)

q_int_qinv

q_int_qinv(n: int) -> sp.Expr

[n]_{q^{-1}}, the q -> q^{-1} substitution of [n]_q.

Source code in src\qreals\rational.py
def q_int_qinv(n: int) -> sp.Expr:
    """[n]_{q^{-1}}, the q -> q^{-1} substitution of [n]_q."""
    n = int(n)
    if n == 0:
        return sp.Integer(0)
    if n > 0:
        return q_int(n) / q ** (n - 1)
    return -q_int_qinv(-n) * q ** (-n)

qreals.truncated

The q-real [x]_q as a truncated power series, for any real x.

This is the path for irrationals (pi, sqrt(2), the golden ratio) and for any input where the exact rational function is not wanted. The MGO formula is evaluated bottom-up over the truncated-series kernel in series, so the result is the first N stable integer Taylor coefficients of [x]_q.

For a rational p/s the CF terminates and this returns the Taylor expansion of the exact function computed in rational; the two agree coefficient for coefficient, which the test suite checks.

q_real_truncated

q_real_truncated(x_repr: str, N: int) -> list[int]

First N stable Taylor coefficients of [x]_q.

Parameters:

Name Type Description Default
x_repr str

a sympy-parseable string, e.g. "pi", "sqrt(2)", "(1+sqrt(5))/2", "E", "3/2".

required
N int

number of stable coefficients required, per MGO Proposition 1.1.

required

Returns:

Type Description
list[int]

A list of N integers [c_0, c_1, ..., c_{N-1}], where c_k is the

list[int]

coefficient of q^k.

Source code in src\qreals\truncated.py
def q_real_truncated(x_repr: str, N: int) -> list[int]:
    """First N stable Taylor coefficients of [x]_q.

    Args:
        x_repr: a sympy-parseable string, e.g. "pi", "sqrt(2)",
            "(1+sqrt(5))/2", "E", "3/2".
        N: number of stable coefficients required, per MGO Proposition 1.1.

    Returns:
        A list of N integers [c_0, c_1, ..., c_{N-1}], where c_k is the
        coefficient of q^k.
    """
    a = cf_partials(x_repr, N)
    if len(a) == 1 and a[0] in (0, 1):
        # [0]_q = 0 and [1]_q = 1 are constants whose single-quotient CF sits
        # outside the even-length normalisation. Integers >= 2 split normally.
        out = [0] * N
        if a[0] == 1 and N > 0:
            out[0] = 1
        return out
    a = make_even_length(a)
    prec = N + 5
    v, coeffs = mgo_build_series(a, prec)
    out = [0] * N
    for k in range(N):
        idx = k - v
        if 0 <= idx < len(coeffs):
            out[k] = int(coeffs[idx])
    return out

The module symbol qreals.q is the sympy symbol used in the rational-function results of q_rational.

Arithmetic between q-reals

qreals.arithmetic

Arithmetic between q-reals: series sum, series product, and q-negation.

The stable core here works on coefficient lists over the truncated-series kernel in series, so it stays pure Python (sympy is used only to read the continued fraction of x, through q_real_truncated). Three things live here, with one caveat each, all spelled out in docs/CORRECTNESS.md:

  • q_add(x, y, N) and q_mul(x, y, N) return the first N Taylor coefficients of the series sum [x]_q + [y]_q and the series product [x]_q * [y]_q. These are the sum and product of the two q-series, NOT [x+y]_q or [x*y]_q: the MGO map x |-> [x]_q is not a ring homomorphism. gosper reaches the same two quantities by a different algorithm, which the tests use as a cross-check.

  • q_neg(x, N) returns the q-deformed negation [-x]_q of Jouteur (arXiv:2503.02122, eq. 2), as a Laurent result (valuation, coeffs). This is the PGL_2(Z)-action negation, which is NOT the coefficient-wise negation of [x]_q and NOT the MGO series of the real number -x. It is an involution, and negation_sum / finite_xnegx use it to study Ovsienko's Example 6.4 (for which real x is [x]_q + [-x]_q a finite Laurent polynomial).

  • radius(x, N) estimates the radius of convergence of the power series [x]_q from its first N coefficients, by the running-max root-test slope. The finite-N value is biased high; see its docstring and the docs.

q_add

q_add(x: str, y: str, N: int) -> list[int]

First N Taylor coefficients of the series sum [x]_q + [y]_q (x, y >= 0).

This is the coefficient-wise sum of the two q-series. It is verified against the bihomographic gosper engine, an independent algorithm, on rationals.

Source code in src\qreals\arithmetic.py
def q_add(x: str, y: str, N: int) -> list[int]:
    """First N Taylor coefficients of the series sum [x]_q + [y]_q (x, y >= 0).

    This is the coefficient-wise sum of the two q-series. It is verified against
    the bihomographic `gosper` engine, an independent algorithm, on rationals.
    """
    if N < 1:
        raise ValueError("N must be at least 1")
    cx = q_real_truncated(x, N)
    cy = q_real_truncated(y, N)
    return [a + b for a, b in zip(cx, cy)]

q_mul

q_mul(x: str, y: str, N: int) -> list[int]

First N Taylor coefficients of the series product [x]_q * [y]_q (x, y >= 0).

This is the Cauchy product (convolution) of the two q-series, cross-checked against the gosper engine's "mul" value on rationals.

Source code in src\qreals\arithmetic.py
def q_mul(x: str, y: str, N: int) -> list[int]:
    """First N Taylor coefficients of the series product [x]_q * [y]_q (x, y >= 0).

    This is the Cauchy product (convolution) of the two q-series, cross-checked
    against the `gosper` engine's "mul" value on rationals.
    """
    if N < 1:
        raise ValueError("N must be at least 1")
    cx = q_real_truncated(x, N)
    cy = q_real_truncated(y, N)
    out = [0] * N
    for i, a in enumerate(cx):
        if a == 0:
            continue
        for j, b in enumerate(cy):
            if i + j >= N:
                break
            out[i + j] += a * b
    return out

q_neg

q_neg(x: str, N: int) -> LaurentCoeffs

The Jouteur q-negation [-x]_q (x >= 0) as (valuation, N coefficients).

[-x]_q is a Laurent series (it carries negative powers of q), so the result is returned as a valuation together with the coefficient list from q^v up.

Source code in src\qreals\arithmetic.py
def q_neg(x: str, N: int) -> LaurentCoeffs:
    """The Jouteur q-negation [-x]_q (x >= 0) as (valuation, N coefficients).

    [-x]_q is a Laurent series (it carries negative powers of q), so the result
    is returned as a valuation together with the coefficient list from q^v up.
    """
    if N < 1:
        raise ValueError("N must be at least 1")
    prec = N + _BUFFER
    neg = _jouteur_neg(_qreal_series(x, prec), prec)
    v, c = neg
    return v, _pad(c, N)

negation_sum

negation_sum(x: str, N: int) -> LaurentCoeffs

[x]_q + [-x]_q (x >= 0) as (valuation, N coefficients), Ovsienko Ex. 6.4.

Source code in src\qreals\arithmetic.py
def negation_sum(x: str, N: int) -> LaurentCoeffs:
    """[x]_q + [-x]_q (x >= 0) as (valuation, N coefficients), Ovsienko Ex. 6.4."""
    if N < 1:
        raise ValueError("N must be at least 1")
    prec = N + _BUFFER
    A = _qreal_series(x, prec)
    total = series.add(A, _jouteur_neg(A, prec), prec)
    v, c = total
    return v, _pad(c, N)

finite_xnegx

finite_xnegx(x: str, order: int = 48) -> bool

Does [x]_q + [-x]_q terminate as a finite Laurent polynomial? (Ex. 6.4)

Computed numerically to the given order: the sum is reported finite when its coefficients past the leading block are a long run of zeros. The proven criterion (finite iff x is a trace-zero quadratic, i.e. a pure square root) and the closed identity behind it are in docs/CORRECTNESS.md; this is the operational check, honest about being a finite-order observation.

Source code in src\qreals\arithmetic.py
def finite_xnegx(x: str, order: int = 48) -> bool:
    """Does [x]_q + [-x]_q terminate as a finite Laurent polynomial? (Ex. 6.4)

    Computed numerically to the given order: the sum is reported finite when its
    coefficients past the leading block are a long run of zeros. The proven
    criterion (finite iff x is a trace-zero quadratic, i.e. a pure square root)
    and the closed identity behind it are in docs/CORRECTNESS.md; this is the
    operational check, honest about being a finite-order observation.
    """
    if order < 8:
        raise ValueError("order must be at least 8 to judge termination")
    _, coeffs = negation_sum(x, order)
    last_nonzero = -1
    for i, c in enumerate(coeffs):
        if c != 0:
            last_nonzero = i
    trailing_zeros = (len(coeffs) - 1) - last_nonzero
    # A terminating Laurent polynomial leaves a long zero tail in this window; a
    # genuine infinite series keeps producing nonzero coefficients near the top.
    return trailing_zeros >= order // 2

radius

radius(x: str, N: int) -> float

Running-max root-test estimate of the radius of convergence of [x]_q.

The Cauchy-Hadamard radius is R = 1 / limsup_k |c_k|^{1/k}. This returns the finite-N reciprocal of the running maximum of |c_k|^{1/k} over 1 <= k < N, i.e. exp(-max_k (ln|c_k|) / k). Returns +inf only when no coefficient past the constant term is nonzero in the window (x = 0 or x = 1).

Finite-N bias, stated honestly: the running maximum over a finite window is at most the true limsup, so this estimate is at least the true radius. It is biased high and decreases toward R from above as N grows. For an integer x, where [x]_q is a polynomial (true radius infinite), the low-order unit coefficients pin the estimate near 1 rather than revealing the infinite radius; that saturation is itself a face of the finite-N bias.

Source code in src\qreals\arithmetic.py
def radius(x: str, N: int) -> float:
    """Running-max root-test estimate of the radius of convergence of [x]_q.

    The Cauchy-Hadamard radius is R = 1 / limsup_k |c_k|^{1/k}. This returns the
    finite-N reciprocal of the running maximum of |c_k|^{1/k} over 1 <= k < N,
    i.e. exp(-max_k (ln|c_k|) / k). Returns +inf only when no coefficient past
    the constant term is nonzero in the window (x = 0 or x = 1).

    Finite-N bias, stated honestly: the running maximum over a finite window is
    at most the true limsup, so this estimate is at least the true radius. It is
    biased high and decreases toward R from above as N grows. For an integer x,
    where [x]_q is a polynomial (true radius infinite), the low-order unit
    coefficients pin the estimate near 1 rather than revealing the infinite
    radius; that saturation is itself a face of the finite-N bias.
    """
    if N < 2:
        raise ValueError("N must be at least 2 to estimate a slope")
    coeffs = q_real_truncated(x, N)
    max_slope: float | None = None
    for k in range(1, len(coeffs)):
        c = coeffs[k]
        if c == 0:
            continue
        slope = math.log(abs(c)) / k
        if max_slope is None or slope > max_slope:
            max_slope = slope
    if max_slope is None:
        return math.inf
    return math.exp(-max_slope)

qreals.qreal

QReal: a convenience wrapper around a computed q-real series.

The functional API in arithmetic and truncated is the stable core; this class is sugar over it. A QReal holds a Laurent result (a valuation and a dense coefficient list) together with a short label saying where it came from, and offers the everyday read-outs plus operators that delegate straight back to the functions:

QReal("pi", 12).coeffs            -> [1, 1, 1, 0, ...]
QReal("3/2", 12) + QReal("13/5")  -> q_add, a new QReal
-QReal("sqrt(2)", 12)             -> q_neg (Jouteur), a Laurent QReal
QReal("3/2", 12) * QReal("5/2")   -> q_mul, a new QReal

Operators carry the same caveats as the functions they call: + and * are the series sum and product [x]_q +/ [y]_q, not [x +/ y]_q, and unary - is the Jouteur PGL_2(Z) negation [-x]_q, not coefficient negation. See docs/CORRECTNESS.md.

QReal

A q-real [x]_q held to N coefficients, with read-outs and operators.

Source code in src\qreals\qreal.py
class QReal:
    """A q-real [x]_q held to N coefficients, with read-outs and operators."""

    __slots__ = ("valuation", "coeffs", "label")

    def __init__(self, x: str, N: int = _DEFAULT_N):
        """Build [x]_q for real x >= 0, keeping its first N Taylor coefficients."""
        if N < 1:
            raise ValueError("N must be at least 1")
        self.valuation: int = 0
        self.coeffs: list[int] = q_real_truncated(str(x), N)
        self.label: str = f"[{x}]_q"

    # -- construction from a raw Laurent result (used by the operators) --------
    @classmethod
    def from_laurent(cls, valuation: int, coeffs: list[int], label: str) -> "QReal":
        """Wrap an already-computed (valuation, coeffs) Laurent result."""
        obj = cls.__new__(cls)
        obj.valuation = int(valuation)
        obj.coeffs = list(coeffs)
        obj.label = label
        return obj

    # -- read-outs -------------------------------------------------------------
    def __len__(self) -> int:
        return len(self.coeffs)

    @property
    def lowest_power(self) -> int:
        """The exponent of the first stored coefficient (the valuation)."""
        return self.valuation

    def radius_estimate(self) -> float:
        """Running-max root-test estimate of the radius of convergence.

        Defined only for a Taylor q-real (valuation 0); a negated/Laurent QReal
        has no power-series radius, so this raises. Delegates to
        `arithmetic.radius` via the coefficient list it already holds.
        """
        if self.valuation != 0:
            raise ValueError(
                "radius_estimate is for a Taylor q-real (valuation 0); this QReal "
                f"has valuation {self.valuation}"
            )
        if len(self.coeffs) < 2:
            raise ValueError("need at least 2 coefficients to estimate a slope")
        return _radius_from_coeffs(self.coeffs)

    def sign_pattern(self) -> str:
        """The sign of each coefficient as a string of '+', '-', '0'."""
        return " ".join("0" if c == 0 else ("+" if c > 0 else "-") for c in self.coeffs)

    def zero_run(self) -> tuple[int, int]:
        """The longest run of consecutive zero coefficients as (start, length).

        start is the index into `coeffs` (so the power is valuation + start);
        length is 0 when there is no zero coefficient. The first such longest run
        is reported on a tie.
        """
        best_start, best_len = 0, 0
        run_start, run_len = 0, 0
        for i, c in enumerate(self.coeffs):
            if c == 0:
                if run_len == 0:
                    run_start = i
                run_len += 1
                if run_len > best_len:
                    best_start, best_len = run_start, run_len
            else:
                run_len = 0
        return best_start, best_len

    # -- operators delegating to the functional API ----------------------------
    def __add__(self, other: "QReal") -> "QReal":
        x, y = _operand_real(self), _operand_real(other)
        n = min(len(self), len(other))
        return QReal.from_laurent(
            0, arithmetic.q_add(x, y, n), f"{self.label} + {other.label}"
        )

    def __mul__(self, other: "QReal") -> "QReal":
        x, y = _operand_real(self), _operand_real(other)
        n = min(len(self), len(other))
        return QReal.from_laurent(
            0, arithmetic.q_mul(x, y, n), f"{self.label} * {other.label}"
        )

    def __neg__(self) -> "QReal":
        x = _operand_real(self)
        v, c = arithmetic.q_neg(x, len(self))
        return QReal.from_laurent(v, c, f"[-{_strip(self.label)}]_q")

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, QReal):
            return NotImplemented
        return self.valuation == other.valuation and self.coeffs == other.coeffs

    def __repr__(self) -> str:
        return f"QReal(label={self.label!r}, valuation={self.valuation}, coeffs={self.coeffs})"

lowest_power property

lowest_power: int

The exponent of the first stored coefficient (the valuation).

__init__

__init__(x: str, N: int = _DEFAULT_N)

Build [x]_q for real x >= 0, keeping its first N Taylor coefficients.

Source code in src\qreals\qreal.py
def __init__(self, x: str, N: int = _DEFAULT_N):
    """Build [x]_q for real x >= 0, keeping its first N Taylor coefficients."""
    if N < 1:
        raise ValueError("N must be at least 1")
    self.valuation: int = 0
    self.coeffs: list[int] = q_real_truncated(str(x), N)
    self.label: str = f"[{x}]_q"

from_laurent classmethod

from_laurent(
    valuation: int, coeffs: list[int], label: str
) -> "QReal"

Wrap an already-computed (valuation, coeffs) Laurent result.

Source code in src\qreals\qreal.py
@classmethod
def from_laurent(cls, valuation: int, coeffs: list[int], label: str) -> "QReal":
    """Wrap an already-computed (valuation, coeffs) Laurent result."""
    obj = cls.__new__(cls)
    obj.valuation = int(valuation)
    obj.coeffs = list(coeffs)
    obj.label = label
    return obj

radius_estimate

radius_estimate() -> float

Running-max root-test estimate of the radius of convergence.

Defined only for a Taylor q-real (valuation 0); a negated/Laurent QReal has no power-series radius, so this raises. Delegates to arithmetic.radius via the coefficient list it already holds.

Source code in src\qreals\qreal.py
def radius_estimate(self) -> float:
    """Running-max root-test estimate of the radius of convergence.

    Defined only for a Taylor q-real (valuation 0); a negated/Laurent QReal
    has no power-series radius, so this raises. Delegates to
    `arithmetic.radius` via the coefficient list it already holds.
    """
    if self.valuation != 0:
        raise ValueError(
            "radius_estimate is for a Taylor q-real (valuation 0); this QReal "
            f"has valuation {self.valuation}"
        )
    if len(self.coeffs) < 2:
        raise ValueError("need at least 2 coefficients to estimate a slope")
    return _radius_from_coeffs(self.coeffs)

sign_pattern

sign_pattern() -> str

The sign of each coefficient as a string of '+', '-', '0'.

Source code in src\qreals\qreal.py
def sign_pattern(self) -> str:
    """The sign of each coefficient as a string of '+', '-', '0'."""
    return " ".join("0" if c == 0 else ("+" if c > 0 else "-") for c in self.coeffs)

zero_run

zero_run() -> tuple[int, int]

The longest run of consecutive zero coefficients as (start, length).

start is the index into coeffs (so the power is valuation + start); length is 0 when there is no zero coefficient. The first such longest run is reported on a tie.

Source code in src\qreals\qreal.py
def zero_run(self) -> tuple[int, int]:
    """The longest run of consecutive zero coefficients as (start, length).

    start is the index into `coeffs` (so the power is valuation + start);
    length is 0 when there is no zero coefficient. The first such longest run
    is reported on a tie.
    """
    best_start, best_len = 0, 0
    run_start, run_len = 0, 0
    for i, c in enumerate(self.coeffs):
        if c == 0:
            if run_len == 0:
                run_start = i
            run_len += 1
            if run_len > best_len:
                best_start, best_len = run_start, run_len
        else:
            run_len = 0
    return best_start, best_len

The bihomographic cross-check engine

qreals.gosper

The q-deformed bihomographic engine, an independent route to q-real arithmetic.

This is the q-analogue of the classical bihomographic continued-fraction state machine (a "Gosper" engine). The classical engine carries a 2x4 integer state and ingests partial quotients of x and y by right-multiplying by the Kronecker-factored blocks A(t) (x) I and I (x) A(t), with the continued-fraction block A(t) = [[t, 1], [1, 0]]. Here every entry is promoted to its MGO q-deformed counterpart, so the state carries Laurent polynomials in q and the engine computes a bilinear function of the q-reals [x]_q and [y]_q directly from the continued fractions of x and y.

The MGO q-block, in the exact regular-CF convention rational and truncated use. For an even-length regular continued fraction a = [a_1, ..., a_{2m}],

A_q^{(i)}(a) = [[ [a]_q    , q^{ a} ], [1, 0]]   at odd  positions (1-indexed),
               [[ [a]_{1/q}, q^{-a} ], [1, 0]]   at even positions,

and [x]_q = R_x / S_x where (R_x, S_x)^T is the first column of the product of the blocks. With monomial columns (XY, X, Y, 1) the 2x4 state S = [[a,b,c,d],[e,f,g,h]] represents z(X, Y) = (aXY+bX+cY+d)/(eXY+fX+gY+h). Ingesting an x-quotient right-multiplies by A_q (x) I; a y-quotient by I (x) A_q. After full ingestion the first-column ratio of S . (prod P_q)(prod Q_q) is z([x]_q, [y]_q): for op="add" that is [x]_q + [y]_q, for op="mul" it is [x]_q * [y]_q.

Why this is here. arithmetic.q_add and arithmetic.q_mul compute the same quantities the cheap way, by expanding each q-real series and adding or convolving the coefficient lists. This engine reaches the result by a different algorithm (a state machine over rational functions in q, never forming the two series separately), so agreement between the two is a real cross-check rather than a re-run of one code path. The test suite uses it exactly that way; see docs/CORRECTNESS.md.

Caveat (the load-bearing one). For op="add" this computes [x]_q + [y]_q, the sum of the two q-series, NOT [x+y]_q, the q-deformation of the real sum. The MGO map x |-> [x]_q is not additive, so these differ already at q^0 (constant term 2 vs 1); the deficit D = [x+y]_q - ([x]_q + [y]_q) is a separate object and is not what this engine returns.

q_gosper

q_gosper(
    x: Fraction, y: Fraction, op: str = "add"
) -> sp.Expr

z([x]_q, [y]_q) as a reduced rational function in q, via the 2x4 state.

op="add" gives [x]_q + [y]_q; op="mul" gives [x]_q * [y]_q. Inputs are rationals (the continued fraction must terminate for the state to close).

Source code in src\qreals\gosper.py
def q_gosper(x: Fraction, y: Fraction, op: str = "add") -> sp.Expr:
    """z([x]_q, [y]_q) as a reduced rational function in q, via the 2x4 state.

    op="add" gives [x]_q + [y]_q; op="mul" gives [x]_q * [y]_q. Inputs are
    rationals (the continued fraction must terminate for the state to close).
    """
    m = _state_matrix(op)
    for i, a in enumerate(q_cf(x)):
        m = m * kron(q_block(i, a), sp.eye(2))
    for j, b in enumerate(q_cf(y)):
        m = m * kron(sp.eye(2), q_block(j, b))
    return sp.cancel(m[0, 0] / m[1, 0])

gosper_coeffs

gosper_coeffs(
    x: Fraction, y: Fraction, op: str, N: int
) -> list[int]

First N Taylor coefficients [c_0..c_{N-1}] of z([x]_q, [y]_q).

For x, y > 0 both q-reals are regular at q = 0 and the result is a Taylor series, so the valuation is taken to be 0.

Source code in src\qreals\gosper.py
def gosper_coeffs(x: Fraction, y: Fraction, op: str, N: int) -> list[int]:
    """First N Taylor coefficients [c_0..c_{N-1}] of z([x]_q, [y]_q).

    For x, y > 0 both q-reals are regular at q = 0 and the result is a Taylor
    series, so the valuation is taken to be 0.
    """
    _, coeffs = laurent_coeffs(q_gosper(x, y, op), N - 1, lo=0)
    return coeffs

Verification stamp

qreals.verify

Inline cross-checks for a qreals result, the everyday trust signal.

After every computation qreals runs the cheap checks that apply to that input and prints one line, for example::

verified: q=1 matches 3/2, exact = truncated to 12

This module is core. It imports only sympy and the other core modules, never the interface or the certificate layer, so the stamp works with pip install qreals alone and never needs a TeX engine. It writes nothing.

Each check recomputes a value a second, independent way and compares:

  • q=1 specialisation. For a rational input, setting q=1 collapses [n]_q back to n, so [p/s]_q at q=1 must equal the ordinary p/s (RAT Corollary 1.7).
  • exact = truncated. For a rational input the continued fraction terminates, so the truncated series must equal the Taylor expansion of the exact rational function computed by q_rational.
  • truncation stable. The first N coefficients of [x]_q must not change when more are requested (REAL Theorem 1).
  • shift law. [x+1]_q = q [x]_q + 1 (REAL eqn 3). Recomputing [x+1]_q from its own continued fraction and comparing to q*[x]_q + 1 is an algorithm-level cross-check that holds for every real x.

For an irrational input the first two checks cannot run (there is no terminating rational function and the series diverges at q=1); the stamp says so plainly rather than claiming a pass.

The mathematics follows Morier-Genoud and Ovsienko, "q-deformed rationals and q-continued fractions", Forum Math. Sigma 8 (2020); see docs/CORRECTNESS.md.

Stamp dataclass

The set of checks run for one computation, plus the one-line summary.

Source code in src\qreals\verify.py
@dataclass
class Stamp:
    """The set of checks run for one computation, plus the one-line summary."""

    checks: list[Check] = field(default_factory=list)

    @property
    def ran(self) -> list[Check]:
        return [c for c in self.checks if c.status in (_GOOD, _BAD, _ERR)]

    @property
    def ok(self) -> bool:
        """True when at least one check ran and none failed or errored."""
        ran = self.ran
        return bool(ran) and all(c.status == _GOOD for c in ran)

    def line(self) -> str:
        """The one-line stamp, honest about what did and did not run."""
        if not self.checks:
            return ""
        passed = [c.label for c in self.checks if c.status == _GOOD]
        failed = [c.label for c in self.checks if c.status == _BAD]
        errored = [c.label for c in self.checks if c.status == _ERR]
        na = [c.label for c in self.checks if c.status == _NA]
        if failed:
            return "verification FAILED: " + "; ".join(failed)
        parts: list[str] = []
        if passed:
            parts.append("verified: " + ", ".join(passed))
        else:
            parts.append("verified: no check applied to this input")
        if errored:
            parts.append("could not check: " + "; ".join(errored))
        if na:
            parts.append("n/a: " + "; ".join(na))
        return "; ".join(parts)

    def as_dict(self) -> dict[str, Any]:
        return {
            "line": self.line(),
            "ok": self.ok,
            "checks": [
                {"label": c.label, "status": c.status, "detail": c.detail}
                for c in self.checks
            ],
        }

ok property

ok: bool

True when at least one check ran and none failed or errored.

line

line() -> str

The one-line stamp, honest about what did and did not run.

Source code in src\qreals\verify.py
def line(self) -> str:
    """The one-line stamp, honest about what did and did not run."""
    if not self.checks:
        return ""
    passed = [c.label for c in self.checks if c.status == _GOOD]
    failed = [c.label for c in self.checks if c.status == _BAD]
    errored = [c.label for c in self.checks if c.status == _ERR]
    na = [c.label for c in self.checks if c.status == _NA]
    if failed:
        return "verification FAILED: " + "; ".join(failed)
    parts: list[str] = []
    if passed:
        parts.append("verified: " + ", ".join(passed))
    else:
        parts.append("verified: no check applied to this input")
    if errored:
        parts.append("could not check: " + "; ".join(errored))
    if na:
        parts.append("n/a: " + "; ".join(na))
    return "; ".join(parts)

verify

verify(result: dict[str, Any]) -> Stamp

Run the cross-checks that apply to a computation result dict.

Dispatches on result["kind"]. An unknown or absent kind (the help screen, say) yields an empty stamp, which renders as no line at all.

Source code in src\qreals\verify.py
def verify(result: dict[str, Any]) -> Stamp:
    """Run the cross-checks that apply to a computation result dict.

    Dispatches on result["kind"]. An unknown or absent kind (the help screen,
    say) yields an empty stamp, which renders as no line at all.
    """
    kind = result.get("kind")
    data = result.get("data", {})
    if kind == "rational":
        return verify_rational(int(data["p"]), int(data["s"]))
    if kind == "qint":
        return verify_qint(int(data["n"]))
    if kind in ("coeffs", "laurent", "prefix", "locked", "shift", "readouts"):
        return verify_series(str(data["x"]), _series_n(result))
    if kind == "arith":
        return verify_arith(
            str(data["x"]), str(data["y"]), int(data["n"]), str(data.get("op", "add"))
        )
    if kind in ("negation", "finite"):
        return verify_negation(str(data["x"]), int(data.get("n", 12)))
    if kind == "radius":
        return verify_radius(str(data["x"]), int(data["n"]))
    return Stamp([])

Laurent expansions and the board lemmas

qreals.expansions

May-14 board lemmas about q-real Laurent expansions, as first-class functions.

Four lemmas from the Qnumbers01 board (q-numbers vault, may14/), each turned into a tested function:

  1. Integer-part prefix. For real x with t = floor(x), the Laurent expansion of [x]_q opens with [t]_q = 1 + q + ... + q^{t-1} and then a forced 0 coefficient at q^t, the clean seam between integer and fractional parts. integer_part_prefix. (note 1)

  2. Convergents pin down coefficients. For a positive irrational x with continued fraction [a_1, a_2, ...] and partial sum S_n = a_1 + ... + a_n, the n-th convergent computes [x]_q correctly on a known number of low powers. coeffs_locked_by_convergent. (note 2)

  3. The MGO positive form. [x]_q is the Laurent expansion produced by the MGO continued-fraction formula; mgo_laurent reads its coefficients off to a requested order. (note 3)

  4. The shift relations. [x-1]_q = ([x]_q - 1)/q lowers the argument by one and [x+1]_q = q[x]_q + 1 raises it. shift_down and shift_up are the tool that moves a coefficient question between unit intervals. (note 5)

CLI

python -m qreals.expansions pi --order 12

prints the Laurent expansion of [x]_q to the requested order plus its forced integer-part prefix.

integer_part_prefix

integer_part_prefix(
    x: str | int | float | Expr,
) -> list[int]

The forced opening block [floor(x)]_q + 0*q^floor(x) of [x]_q (note 1).

For real x with t = floor(x), the Laurent expansion of [x]_q begins with t coefficients all equal to 1 (spelling out [t]_q) followed by a 0 coefficient at q^t. This returns that prefix as a list of t + 1 integers, [1, 1, ..., 1, 0]; the fractional part of x can perturb only powers q^{t+1} and higher.

Parameters:

Name Type Description Default
x str | int | float | Expr

a sympy-parseable real, e.g. "22/7", "pi", 3, "pi-2".

required

Returns:

Type Description
list[int]

[1] * t + [0], where t = floor(x).

Source code in src\qreals\expansions.py
def integer_part_prefix(x: str | int | float | sp.Expr) -> list[int]:
    """The forced opening block [floor(x)]_q + 0*q^floor(x) of [x]_q (note 1).

    For real x with t = floor(x), the Laurent expansion of [x]_q begins with t
    coefficients all equal to 1 (spelling out [t]_q) followed by a 0 coefficient
    at q^t. This returns that prefix as a list of t + 1 integers,
    [1, 1, ..., 1, 0]; the fractional part of x can perturb only powers q^{t+1}
    and higher.

    Args:
        x: a sympy-parseable real, e.g. "22/7", "pi", 3, "pi-2".

    Returns:
        [1] * t + [0], where t = floor(x).
    """
    t = int(sp.floor(sp.sympify(x)))
    if t < 0:
        raise ValueError(f"integer-part lemma is stated for x >= 0, got floor(x) = {t}")
    return [1] * t + [0]

coeffs_locked_by_convergent

coeffs_locked_by_convergent(
    cf_terms: Sequence[int], n: int
) -> tuple[int, int]

How many Laurent coefficients the n-th convergent of x locks in (note 2).

With continued fraction x = [a_1, a_2, ...] and partial sum S_n = a_1 + ... + a_n, the n-th convergent x_n agrees with [x]_q on every power strictly below q^{S_n - 1}. Returns (S_n, count), where count = S_n - 1 is the number of locked coefficients, i.e. the powers q^0, ..., q^{S_n - 2}. The first power that may differ is q^{S_n - 1}.

Note on the off-by-one. Note 2 phrased the cutoff as "below q^{S_n}", which would make count = S_n. Direct computation refutes that: for x = pi, a_1 = 3, a_2 = 7, S_2 = 10, the convergent [3,7] = 22/7 gives [22/7]_q = 1 + q + q^2 + q^9 + ... whereas the true [pi]_q has a 0 at q^9 (its next term is at q^10). So [22/7]_q and [pi]_q already differ at q^9 = q^{S_2 - 1}: agreement holds only below q^{S_n - 1}, locking S_n - 1 coefficients, not S_n. This matches the existing continued_fraction docstring and is the resolution of the numerical tension flagged in note 5.

Parameters:

Name Type Description Default
cf_terms Sequence[int]

the continued-fraction partial quotients [a_1, a_2, ...].

required
n int

convergent index, 1 <= n <= len(cf_terms).

required

Returns:

Type Description
tuple[int, int]

(S_n, count) with S_n = a_1 + ... + a_n and count = S_n - 1.

Source code in src\qreals\expansions.py
def coeffs_locked_by_convergent(cf_terms: Sequence[int], n: int) -> tuple[int, int]:
    """How many Laurent coefficients the n-th convergent of x locks in (note 2).

    With continued fraction x = [a_1, a_2, ...] and partial sum
    S_n = a_1 + ... + a_n, the n-th convergent x_n agrees with [x]_q on every
    power strictly below q^{S_n - 1}. Returns (S_n, count), where
    count = S_n - 1 is the number of locked coefficients, i.e. the powers
    q^0, ..., q^{S_n - 2}. The first power that may differ is q^{S_n - 1}.

    Note on the off-by-one. Note 2 phrased the cutoff as "below q^{S_n}", which
    would make count = S_n. Direct computation refutes that: for x = pi,
    a_1 = 3, a_2 = 7, S_2 = 10, the convergent [3,7] = 22/7 gives
    [22/7]_q = 1 + q + q^2 + q^9 + ... whereas the true [pi]_q has a 0 at q^9
    (its next term is at q^10). So [22/7]_q and [pi]_q already differ at
    q^9 = q^{S_2 - 1}: agreement holds only below q^{S_n - 1}, locking S_n - 1
    coefficients, not S_n. This matches the existing `continued_fraction`
    docstring and is the resolution of the numerical tension flagged in note 5.

    Args:
        cf_terms: the continued-fraction partial quotients [a_1, a_2, ...].
        n: convergent index, 1 <= n <= len(cf_terms).

    Returns:
        (S_n, count) with S_n = a_1 + ... + a_n and count = S_n - 1.
    """
    if n < 1:
        raise ValueError(f"n must be >= 1, got {n}")
    terms = [int(a) for a in cf_terms]
    if n > len(terms):
        raise ValueError(
            f"need at least n = {n} continued-fraction terms, got {len(terms)}"
        )
    s_n = sum(terms[:n])
    return s_n, s_n - 1

mgo_laurent

mgo_laurent(
    x: str | int | float | Expr, order: int
) -> list[int]

Laurent coefficients of [x]_q through q^order, via the MGO formula (note 3).

Evaluates the MGO positive-form continued fraction for x over the truncated-series kernel and reads off the coefficients of q^0 up to and including q^order. Returns order + 1 integers [c_0, ..., c_order] with c_k the coefficient of q^k. For x >= 1 the series has valuation 0 (c_0 = 1); for 0 < x < 1 it has valuation 1 (c_0 = 0).

Parameters:

Name Type Description Default
x str | int | float | Expr

a sympy-parseable real, e.g. "22/7", "pi", "sqrt(2)".

required
order int

highest power q^order to return; must be >= 0.

required

Returns:

Type Description
list[int]

[c_0, c_1, ..., c_order].

Source code in src\qreals\expansions.py
def mgo_laurent(x: str | int | float | sp.Expr, order: int) -> list[int]:
    """Laurent coefficients of [x]_q through q^order, via the MGO formula (note 3).

    Evaluates the MGO positive-form continued fraction for x over the
    truncated-series kernel and reads off the coefficients of q^0 up to and
    including q^order. Returns order + 1 integers [c_0, ..., c_order] with c_k
    the coefficient of q^k. For x >= 1 the series has valuation 0 (c_0 = 1);
    for 0 < x < 1 it has valuation 1 (c_0 = 0).

    Args:
        x: a sympy-parseable real, e.g. "22/7", "pi", "sqrt(2)".
        order: highest power q^order to return; must be >= 0.

    Returns:
        [c_0, c_1, ..., c_order].
    """
    if order < 0:
        raise ValueError(f"order must be >= 0, got {order}")
    return q_real_truncated(str(x), order + 1)

shift_down

shift_down(coeffs: Sequence[int]) -> list[int]

Coefficients of [x-1]_q = ([x]_q - 1)/q from those of [x]_q (note 5).

Subtracts 1 from the constant term and divides by q, i.e. drops the constant coefficient and shifts every higher power down by one. This is exact only when the constant term is 1, which holds for any [x]_q with x >= 1 (the integer-part lemma forces it); a different constant term means the input is not such a series, so the division by q would produce a negative power, and this raises.

Returns one fewer coefficient than the input (the top power is lost to the division, matching the precision the truncated series can support).

Source code in src\qreals\expansions.py
def shift_down(coeffs: Sequence[int]) -> list[int]:
    """Coefficients of [x-1]_q = ([x]_q - 1)/q from those of [x]_q (note 5).

    Subtracts 1 from the constant term and divides by q, i.e. drops the
    constant coefficient and shifts every higher power down by one. This is
    exact only when the constant term is 1, which holds for any [x]_q with
    x >= 1 (the integer-part lemma forces it); a different constant term means
    the input is not such a series, so the division by q would produce a
    negative power, and this raises.

    Returns one fewer coefficient than the input (the top power is lost to the
    division, matching the precision the truncated series can support).
    """
    c = [int(v) for v in coeffs]
    if not c:
        raise ValueError("empty coefficient list")
    if c[0] != 1:
        raise ValueError(
            f"shift_down needs constant term 1 (an [x]_q with x >= 1), got c_0 = {c[0]}"
        )
    return c[1:]

shift_up

shift_up(coeffs: Sequence[int]) -> list[int]

Coefficients of [x+1]_q = q[x]_q + 1 from those of [x]_q (note 5).

Multiplies by q and adds 1: the new constant term is 1 and every old coefficient moves up one power. Inverse of shift_down. Returns one more coefficient than the input.

Source code in src\qreals\expansions.py
def shift_up(coeffs: Sequence[int]) -> list[int]:
    """Coefficients of [x+1]_q = q[x]_q + 1 from those of [x]_q (note 5).

    Multiplies by q and adds 1: the new constant term is 1 and every old
    coefficient moves up one power. Inverse of `shift_down`. Returns one more
    coefficient than the input.
    """
    return [1] + [int(v) for v in coeffs]

format_laurent

format_laurent(coeffs: Sequence[int]) -> str

Render a coefficient list [c_0, c_1, ...] as a readable q-polynomial.

Appends an O(q^N) tail where N = len(coeffs), the first power not held.

Source code in src\qreals\expansions.py
def format_laurent(coeffs: Sequence[int]) -> str:
    """Render a coefficient list [c_0, c_1, ...] as a readable q-polynomial.

    Appends an O(q^N) tail where N = len(coeffs), the first power not held.
    """
    coeffs = [int(c) for c in coeffs]
    parts: list[str] = []
    for k, c in enumerate(coeffs):
        if c == 0:
            continue
        mono = _monomial(k)
        if c == 1:
            term = mono if mono else "1"
        elif c == -1:
            term = f"-{mono}" if mono else "-1"
        else:
            term = f"{c}*{mono}" if mono else f"{c}"
        parts.append(term)
    if not parts:
        body = "0"
    else:
        body = parts[0]
        for term in parts[1:]:
            if term.startswith("-"):
                body += f" - {term[1:]}"
            else:
                body += f" + {term}"
    return f"{body} + O(q^{len(coeffs)})"

Coefficient read-outs

qreals.coefficients

Small read-outs over a coefficient list produced by q_real_truncated.

These answer the questions that come up when looking for patterns in the Taylor coefficients of a q-real: where does the first nonzero term sit, where does the first sign change happen, how large do the coefficients get.

first_nonzero_coefficient_index

first_nonzero_coefficient_index(
    coeffs: Iterable[int],
) -> int
Source code in src\qreals\coefficients.py
def first_nonzero_coefficient_index(coeffs: Iterable[int]) -> int:
    for i, c in enumerate(coeffs):
        if c != 0:
            return i
    return -1

first_negative_coefficient_index

first_negative_coefficient_index(
    coeffs: Iterable[int],
) -> int
Source code in src\qreals\coefficients.py
def first_negative_coefficient_index(coeffs: Iterable[int]) -> int:
    for i, c in enumerate(coeffs):
        if c < 0:
            return i
    return -1

coefficient_max_abs

coefficient_max_abs(coeffs: Iterable[int]) -> int
Source code in src\qreals\coefficients.py
def coefficient_max_abs(coeffs: Iterable[int]) -> int:
    return max((abs(c) for c in coeffs), default=0)

number_of_zeros

number_of_zeros(coeffs: Iterable[int]) -> int
Source code in src\qreals\coefficients.py
def number_of_zeros(coeffs: Iterable[int]) -> int:
    return sum(1 for c in coeffs if c == 0)

Exploration helpers (optional extras)

qreals.features

A named, fixed-length fingerprint of a real number's q-analog.

featurize(x) turns a real number x into a vector of named, deterministic features drawn from two sources: the regular continued fraction of x (which drives the MGO construction) and the integer Taylor coefficients of its q-analog [x]_q. The vector is fixed length for a given set of parameters, so two constants are directly comparable: the point is exploration and nearest-neighbour search over constants, not training a model.

The output is a :class:Fingerprint: a parallel pair of names and values (plain floats), with as_dict() for a name -> value mapping and as_numpy() for a numpy array when numpy is installed. No numpy is needed to build a fingerprint or to compare two of them; numpy is the only optional dependency, behind the qreals[features] extra, and is used only by as_numpy. There is no torch and no model here.

The features, in the order they appear in the vector:

Scalar features (16):

  • valuation: index of the first nonzero coefficient of [x]_q. 0 for x >= 1, 1 for 0 < x < 1.
  • n_zeros: how many of the first n_coeffs coefficients are zero.
  • longest_zero_run: length of the longest run of consecutive zero coefficients in that window.
  • first_negative_index: index of the first negative coefficient, or n_coeffs if none is negative in the window.
  • n_sign_changes: sign changes between consecutive nonzero coefficients.
  • n_sign_runs: number of maximal same-sign runs over the nonzero coefficients (n_sign_changes + 1, or 0 when all coefficients are zero).
  • longest_sign_run: the longest such run.
  • max_abs_coeff: the largest |c_k| in the window.
  • mean_abs_coeff: the mean of |c_k| over the window.
  • log10_max_abs_coeff: log10(1 + max_abs_coeff), a scale-insensitive measure of how big the coefficients get.
  • log10_mean_abs_coeff: log10(1 + mean_abs_coeff).
  • inv_radius: the running-max root-test slope max_k (ln|c_k|)/k over the first n_radius coefficients. This is 1 / (radius-of-convergence estimate); it is 0 exactly when [x]_q is a polynomial (x a nonnegative integer), and larger when the coefficients grow faster. Always finite, unlike the radius itself, which is what makes it usable as a coordinate.
  • cf_len: number of regular continued-fraction terms actually available, up to n_cf (a terminating, i.e. rational, expansion shows up as cf_len < n_cf).
  • cf_max: the largest partial quotient among the first n_cf terms.
  • cf_sum: the sum of the first n_cf partial quotients (its growth sets how fast coefficients lock in, MGO Proposition 1.1).

Block features:

  • cf_0 .. cf_{n_cf-1}: the partial quotients (continued-fraction terms) of x, padded with 0 past a terminating expansion. cf_0 is floor(x).
  • cf_partial_sum_1 .. cf_partial_sum_{n_cf}: cumulative sums of those partial quotients. A plateau marks where a rational expansion terminated.
  • c_0 .. c_{n_coeffs-1}: the signed integer Taylor coefficients of [x]_q, the most direct fingerprint of the series. These can grow large for some constants, so for scale-insensitive nearest-neighbour either normalise the vector or lean on the log-magnitude and shape scalars above.

All features are derived from exact integer coefficients and the exact symbolic continued fraction, so featurize is deterministic: the same x and parameters give the same vector every time.

Fingerprint dataclass

A named, fixed-length feature vector for one real number.

names and values are parallel lists of the same length. x is the input as given, and params records (n_cf, n_coeffs, n_radius) so two fingerprints are only comparable when their params agree.

Source code in src\qreals\features.py
@dataclass(frozen=True)
class Fingerprint:
    """A named, fixed-length feature vector for one real number.

    ``names`` and ``values`` are parallel lists of the same length. ``x`` is the
    input as given, and ``params`` records ``(n_cf, n_coeffs, n_radius)`` so two
    fingerprints are only comparable when their params agree.
    """

    x: str
    names: list[str]
    values: list[float]
    params: tuple[int, int, int]

    @property
    def vector(self) -> list[float]:
        """The feature values, in the fixed order given by ``names``."""
        return self.values

    def as_dict(self) -> dict[str, float]:
        """A name -> value mapping of the fingerprint."""
        return dict(zip(self.names, self.values))

    def as_numpy(self) -> Any:
        """The values as a numpy float array (needs ``qreals[features]``)."""
        try:
            import numpy as np
        except ImportError as exc:  # pragma: no cover - exercised when numpy absent
            raise ImportError(
                "as_numpy needs numpy; install it with pip install qreals[features]"
            ) from exc
        return np.asarray(self.values, dtype=float)

vector property

vector: list[float]

The feature values, in the fixed order given by names.

as_dict

as_dict() -> dict[str, float]

A name -> value mapping of the fingerprint.

Source code in src\qreals\features.py
def as_dict(self) -> dict[str, float]:
    """A name -> value mapping of the fingerprint."""
    return dict(zip(self.names, self.values))

as_numpy

as_numpy() -> Any

The values as a numpy float array (needs qreals[features]).

Source code in src\qreals\features.py
def as_numpy(self) -> Any:
    """The values as a numpy float array (needs ``qreals[features]``)."""
    try:
        import numpy as np
    except ImportError as exc:  # pragma: no cover - exercised when numpy absent
        raise ImportError(
            "as_numpy needs numpy; install it with pip install qreals[features]"
        ) from exc
    return np.asarray(self.values, dtype=float)

featurize

featurize(
    x: str | int | float | Expr,
    *,
    n_cf: int = DEFAULT_N_CF,
    n_coeffs: int = DEFAULT_N_COEFFS,
    n_radius: int = DEFAULT_N_RADIUS,
) -> Fingerprint

A named, fixed-length, deterministic fingerprint of [x]_q.

Parameters:

Name Type Description Default
x str | int | float | Expr

a sympy-parseable real, e.g. "pi", "sqrt(2)", "(1+sqrt(5))/2", "3/2".

required
n_cf int

how many continued-fraction terms (and their partial sums) to keep.

DEFAULT_N_CF
n_coeffs int

how many signed Taylor coefficients of [x]_q to keep.

DEFAULT_N_COEFFS
n_radius int

window for the inv_radius slope feature; must be >= 2.

DEFAULT_N_RADIUS

Returns:

Name Type Description
A Fingerprint

class:Fingerprint. The names and length depend only on the three

Fingerprint

parameters, so fingerprints built with the same parameters are directly

Fingerprint

comparable (see :func:feature_distance, :func:nearest).

The math each feature reads is documented at the top of this module.

Source code in src\qreals\features.py
def featurize(
    x: str | int | float | sp.Expr,
    *,
    n_cf: int = DEFAULT_N_CF,
    n_coeffs: int = DEFAULT_N_COEFFS,
    n_radius: int = DEFAULT_N_RADIUS,
) -> Fingerprint:
    """A named, fixed-length, deterministic fingerprint of [x]_q.

    Args:
        x: a sympy-parseable real, e.g. "pi", "sqrt(2)", "(1+sqrt(5))/2", "3/2".
        n_cf: how many continued-fraction terms (and their partial sums) to keep.
        n_coeffs: how many signed Taylor coefficients of [x]_q to keep.
        n_radius: window for the ``inv_radius`` slope feature; must be >= 2.

    Returns:
        A :class:`Fingerprint`. The names and length depend only on the three
        parameters, so fingerprints built with the same parameters are directly
        comparable (see :func:`feature_distance`, :func:`nearest`).

    The math each feature reads is documented at the top of this module.
    """
    if n_cf < 1:
        raise ValueError("n_cf must be at least 1")
    if n_coeffs < 1:
        raise ValueError("n_coeffs must be at least 1")
    if n_radius < 2:
        raise ValueError("n_radius must be at least 2")

    x_repr = str(x)
    value = _parse_real(x_repr)

    depth = max(n_coeffs, n_radius)
    coeffs = q_real_truncated(x_repr, depth)
    window = coeffs[:n_coeffs]

    valuation = next((i for i, c in enumerate(window) if c != 0), -1)
    n_zeros = sum(1 for c in window if c == 0)
    longest_zero_run = _longest_zero_run(window)
    first_negative = next((i for i, c in enumerate(window) if c < 0), None)
    first_negative_index = float(n_coeffs if first_negative is None else first_negative)
    n_sign_changes, n_sign_runs, longest_sign_run = _sign_run_stats(window)
    abs_coeffs = [abs(c) for c in window]
    max_abs = max(abs_coeffs) if abs_coeffs else 0
    mean_abs = sum(abs_coeffs) / len(abs_coeffs) if abs_coeffs else 0.0
    inv_radius = _inv_radius(coeffs[:n_radius])

    cf = _cf_terms(value, n_cf)
    cf_len = len(cf)
    cf_padded = cf + [0] * (n_cf - cf_len)
    cf_max = max(cf_padded) if cf_padded else 0
    cf_sum = sum(cf_padded)
    cf_partial_sums: list[int] = []
    running = 0
    for term in cf_padded:
        running += term
        cf_partial_sums.append(running)

    scalar_values = [
        float(valuation),
        float(n_zeros),
        float(longest_zero_run),
        first_negative_index,
        float(n_sign_changes),
        float(n_sign_runs),
        float(longest_sign_run),
        float(max_abs),
        float(mean_abs),
        math.log10(1.0 + max_abs),
        math.log10(1.0 + mean_abs),
        float(inv_radius),
        float(cf_len),
        float(cf_max),
        float(cf_sum),
    ]
    values = (
        scalar_values
        + [float(t) for t in cf_padded]
        + [float(s) for s in cf_partial_sums]
        + [float(c) for c in window]
    )
    names = feature_names(n_cf, n_coeffs, n_radius)
    assert len(names) == len(values)  # the fixed-length contract
    return Fingerprint(
        x=x_repr, names=names, values=values, params=(n_cf, n_coeffs, n_radius)
    )

feature_names

feature_names(
    n_cf: int = DEFAULT_N_CF,
    n_coeffs: int = DEFAULT_N_COEFFS,
    n_radius: int = DEFAULT_N_RADIUS,
) -> list[str]

The fixed feature names, in vector order, for the given parameters.

Source code in src\qreals\features.py
def feature_names(
    n_cf: int = DEFAULT_N_CF,
    n_coeffs: int = DEFAULT_N_COEFFS,
    n_radius: int = DEFAULT_N_RADIUS,
) -> list[str]:
    """The fixed feature names, in vector order, for the given parameters."""
    scalars = [
        "valuation",
        "n_zeros",
        "longest_zero_run",
        "first_negative_index",
        "n_sign_changes",
        "n_sign_runs",
        "longest_sign_run",
        "max_abs_coeff",
        "mean_abs_coeff",
        "log10_max_abs_coeff",
        "log10_mean_abs_coeff",
        "inv_radius",
        "cf_len",
        "cf_max",
        "cf_sum",
    ]
    cf = [f"cf_{i}" for i in range(n_cf)]
    cf_sums = [f"cf_partial_sum_{i}" for i in range(1, n_cf + 1)]
    coeffs = [f"c_{i}" for i in range(n_coeffs)]
    return scalars + cf + cf_sums + coeffs

feature_distance

feature_distance(a: Fingerprint, b: Fingerprint) -> float

Euclidean distance between two fingerprints (same parameters required).

The raw coefficient features can dominate this distance for fast-growing constants; normalise the vectors first if you want a shape-based comparison.

Source code in src\qreals\features.py
def feature_distance(a: Fingerprint, b: Fingerprint) -> float:
    """Euclidean distance between two fingerprints (same parameters required).

    The raw coefficient features can dominate this distance for fast-growing
    constants; normalise the vectors first if you want a shape-based comparison.
    """
    if a.params != b.params:
        raise ValueError(
            f"fingerprints use different parameters {a.params} vs {b.params}; "
            "compare only fingerprints built the same way"
        )
    return math.sqrt(sum((u - v) ** 2 for u, v in zip(a.values, b.values)))

nearest

nearest(
    target: Fingerprint,
    pool: Sequence[Fingerprint],
    k: int = 1,
) -> list[tuple[Fingerprint, float]]

The k fingerprints in pool closest to target by Euclidean distance.

Returns (fingerprint, distance) pairs sorted nearest first. A small helper for nearest-neighbour exploration over a set of constants.

Source code in src\qreals\features.py
def nearest(
    target: Fingerprint, pool: Sequence[Fingerprint], k: int = 1
) -> list[tuple[Fingerprint, float]]:
    """The ``k`` fingerprints in ``pool`` closest to ``target`` by Euclidean distance.

    Returns ``(fingerprint, distance)`` pairs sorted nearest first. A small helper
    for nearest-neighbour exploration over a set of constants.
    """
    if k < 1:
        raise ValueError("k must be at least 1")
    scored = [(fp, feature_distance(target, fp)) for fp in pool]
    scored.sort(key=lambda pair: pair[1])
    return scored[:k]

qreals.oeis

Look a coefficient sequence up in the OEIS, with re-verification.

A q-series produces an integer coefficient sequence; the obvious next question is whether that sequence is already catalogued. This module submits a sequence to the OEIS public API (oeis.org/search), ranks the hits by matching-prefix length (reconciling a handful of sign conventions, since a q-series can arrive negated or alternating relative to the canonical unsigned entry), re-verifies each top hit against its full b-file so a deep divergence such as "matched the first 20 terms but diverged at term 25" is surfaced, and also tries mod-p reductions of the input. Every HTTP response is cached on disk so re-running the same sequence does not hit the network again.

This is an optional helper. The only network dependency is requests, behind the qreals[oeis] extra; the rest of qreals does not import it. Two failure modes are handled without crashing:

  • requests not installed: lookup raises :class:OeisUnavailable, a clear error pointing at pip install qreals[oeis]. Test with :func:available.
  • the network is down or OEIS is unreachable: each query fails quietly and the lookup returns an empty :class:LookupResult (no hits) rather than raising.

Adapted from the standalone qoeis tool. The math context (q-Taylor and q-continued-fraction coefficient sequences) is the same one the rest of qreals computes.

LookupResult dataclass

Source code in src\qreals\oeis.py
@dataclass
class LookupResult:
    input_seq: list[int]
    hits: list[Hit]
    modp_hits: dict[int, list[Hit]]
    primes: tuple[int, ...] = DEFAULT_PRIMES

    @property
    def top(self) -> Optional[Hit]:
        return self.hits[0] if self.hits else None

Hit dataclass

Source code in src\qreals\oeis.py
@dataclass
class Hit:
    anum: str
    name: str
    transform: str  # which sign reconciliation matched ("identity" if none needed)
    prefix_len: int  # leading terms matched against the OEIS 'data' field
    offset: int = 0  # alignment offset into the candidate
    source: str = "raw"  # "raw" or "mod-2", "mod-3", ...
    data_terms: list[int] = field(default_factory=list)
    # b-file re-verification (filled in for the top hits only)
    bfile_checked: bool = False
    bfile_len: Optional[int] = None
    bfile_match_len: Optional[int] = None
    fully_verified: bool = False
    diverged: bool = False
    diverge_term: Optional[int] = None  # 1-based term index of the first disagreement
    input_value: Optional[int] = None  # reconciled input value at the divergence
    bfile_value: Optional[int] = None  # b-file value at the divergence

OeisUnavailable

Bases: RuntimeError

Raised when an OEIS lookup is requested but requests is not installed.

Source code in src\qreals\oeis.py
class OeisUnavailable(RuntimeError):
    """Raised when an OEIS lookup is requested but ``requests`` is not installed."""

lookup

lookup(
    sequence: str | Sequence[int],
    *,
    primes: Sequence[int] = DEFAULT_PRIMES,
    max_hits: int = 10,
    bfile_top: int = 3,
    do_modp: bool = True,
    do_bfile: bool = True,
    cache_dir: str | Path | None = None,
    session: Any = None,
    timeout: int = DEFAULT_TIMEOUT,
    max_workers: int = 6,
) -> LookupResult

Look up sequence in the OEIS.

sequence may be a string ("1,1,2,5,...") or any iterable of ints.

Returns a :class:LookupResult whose hits are ranked by matching-prefix length (sign-reconciled), with the top bfile_top hits re-verified against their full b-files, plus modp_hits for the mod-p reductions of the input.

Needs the requests dependency (pip install qreals[oeis]); raises :class:OeisUnavailable if it is missing. If OEIS is unreachable, every query fails quietly and the result carries no hits.

Source code in src\qreals\oeis.py
def lookup(
    sequence: str | Sequence[int],
    *,
    primes: Sequence[int] = DEFAULT_PRIMES,
    max_hits: int = 10,
    bfile_top: int = 3,
    do_modp: bool = True,
    do_bfile: bool = True,
    cache_dir: str | Path | None = None,
    session: Any = None,
    timeout: int = DEFAULT_TIMEOUT,
    max_workers: int = 6,
) -> LookupResult:
    """Look up ``sequence`` in the OEIS.

    ``sequence`` may be a string (``"1,1,2,5,..."``) or any iterable of ints.

    Returns a :class:`LookupResult` whose ``hits`` are ranked by matching-prefix
    length (sign-reconciled), with the top ``bfile_top`` hits re-verified against
    their full b-files, plus ``modp_hits`` for the mod-p reductions of the input.

    Needs the ``requests`` dependency (``pip install qreals[oeis]``); raises
    :class:`OeisUnavailable` if it is missing. If OEIS is unreachable, every
    query fails quietly and the result carries no hits.
    """
    seq = _coerce_seq(sequence)
    if not seq:
        raise ValueError("empty sequence")
    _get_requests()  # fail fast and clearly if the extra is not installed
    cache_dir = Path(cache_dir) if cache_dir is not None else DEFAULT_CACHE_DIR

    # Assemble every query up front so they can run concurrently.
    queries: dict[str, str] = {_join(seq): "raw"}
    if any(x < 0 for x in seq):
        queries.setdefault(_join([abs(x) for x in seq]), "abs-search")
    modp_inputs: dict[int, list[int]] = {}
    if do_modp:
        for p in primes:
            reduced = [x % p for x in seq]
            modp_inputs[p] = reduced
            queries.setdefault(_join(reduced), f"mod-{p}")

    results_by_query: dict[str, list[dict[str, Any]]] = {}
    with ThreadPoolExecutor(max_workers=max_workers) as pool:
        futures = {
            pool.submit(_search_raw, q, cache_dir, session, timeout): q for q in queries
        }
        for fut, q in futures.items():
            try:
                results_by_query[q] = fut.result()
            except Exception:  # network down, timeout, bad payload: degrade to no hits
                results_by_query[q] = []

    # Primary hits come from the raw (and absolute-value) searches.
    hits_by_anum: dict[str, Hit] = {}
    for q, src in queries.items():
        if src not in ("raw", "abs-search"):
            continue
        for r in results_by_query.get(q, []):
            candidate = _parse_data_field(r["data"])
            m, tname, off = best_transform_match(seq, candidate)
            if m == 0:
                continue
            anum = _anum(r["number"])
            current = hits_by_anum.get(anum)
            if current is None or m > current.prefix_len:
                hits_by_anum[anum] = Hit(
                    anum=anum,
                    name=r.get("name", ""),
                    transform=tname,
                    prefix_len=m,
                    offset=off,
                    source="raw",
                    data_terms=candidate,
                )
    hits = sorted(hits_by_anum.values(), key=lambda h: (-h.prefix_len, h.anum))[
        :max_hits
    ]

    if do_bfile and hits:
        top = hits[:bfile_top]
        with ThreadPoolExecutor(max_workers=max_workers) as pool:
            list(
                pool.map(
                    lambda h: _verify_hit_bfile(h, seq, cache_dir, session, timeout),
                    top,
                )
            )

    # Mod-p reduced hits (no sign reconciliation; the reduction is already canonical).
    modp_hits: dict[int, list[Hit]] = {}
    if do_modp:
        for p, reduced in modp_inputs.items():
            collected: list[Hit] = []
            for r in results_by_query.get(_join(reduced), []):
                candidate = _parse_data_field(r["data"])
                m, off = _best_alignment(reduced, candidate)
                if m >= MIN_MODP_PREFIX:
                    collected.append(
                        Hit(
                            anum=_anum(r["number"]),
                            name=r.get("name", ""),
                            transform="identity",
                            prefix_len=m,
                            offset=off,
                            source=f"mod-{p}",
                            data_terms=candidate,
                        )
                    )
            collected.sort(key=lambda h: (-h.prefix_len, h.anum))
            if collected:
                modp_hits[p] = collected[:3]

    return LookupResult(
        input_seq=seq, hits=hits, modp_hits=modp_hits, primes=tuple(primes)
    )

available

available() -> bool

True when the requests dependency for OEIS lookups is importable.

Source code in src\qreals\oeis.py
def available() -> bool:
    """True when the ``requests`` dependency for OEIS lookups is importable."""
    return importlib.util.find_spec("requests") is not None