Algorithm Search⌘K

Lazard-Rioboo-Trager algorithm in Python

Gives the logarithmic part of a rational integral explicitly via subresultants.

File
lazard_rioboo_trager.py, 203 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
lazard_rioboo_trager.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/lazard-rioboo-trager/. The authors are listed in prov/J2RB9Z.jsonl.

"""Lazard-Rioboo-Trager: the logarithmic part of the integral of A/D over Q.

Univariate polynomials are lists of Fraction, lowest degree first, with a
nonzero last entry; [] is zero. A polynomial in Q[t][x] is a list indexed by
the x-degree whose entries are polynomials in t.

For a square-free D and deg A < deg D, lazard_rioboo_trager(A, D) returns pairs
(Q_i, S_i) with

    integral of A/D = sum over i of sum over Q_i(alpha) = 0 of alpha log S_i(alpha, x).
"""

from fractions import Fraction
from typing import List, Tuple

Poly = List[Fraction]
BiPoly = List[Poly]


def trim(f: Poly) -> Poly:
    f = [Fraction(a) for a in f]
    while f and f[-1] == 0:
        f.pop()
    return f


def add(f: Poly, g: Poly) -> Poly:
    n = max(len(f), len(g))
    return trim([(f[i] if i < len(f) else 0) + (g[i] if i < len(g) else 0) for i in range(n)])


def scale(c, f: Poly) -> Poly:
    return trim([c * a for a in f])


def mul(f: Poly, g: Poly) -> Poly:
    if not f or not g:
        return []
    r = [Fraction(0)] * (len(f) + len(g) - 1)
    for i, a in enumerate(f):
        for j, b in enumerate(g):
            r[i + j] += a * b
    return trim(r)


def deriv(f: Poly) -> Poly:
    return trim([i * f[i] for i in range(1, len(f))])


def pdivmod(f: Poly, g: Poly) -> Tuple[Poly, Poly]:
    r = trim(f)
    q = [Fraction(0)] * max(len(r) - len(g) + 1, 0)
    while len(r) >= len(g) and r:
        c = r[-1] / g[-1]
        s = len(r) - len(g)
        q[s] = c
        for i, b in enumerate(g):
            r[s + i] -= c * b
        r = trim(r)
    return trim(q), r


def monic(f: Poly) -> Poly:
    return scale(1 / f[-1], f) if f else []


def pgcd(f: Poly, g: Poly) -> Poly:
    f, g = trim(f), trim(g)
    while g:
        f, g = g, pdivmod(f, g)[1]
    return monic(f)


def inverse_mod(a: Poly, m: Poly) -> Poly:
    """The inverse of a modulo m, for gcd(a, m) = 1, by the extended Euclidean algorithm."""
    r0, r1 = trim(m), pdivmod(a, m)[1]
    s0, s1 = [], [Fraction(1)]
    while r1:
        q, r = pdivmod(r0, r1)
        r0, r1 = r1, r
        s0, s1 = s1, add(s0, scale(-1, mul(q, s1)))
    if len(r0) != 1:
        raise ValueError("not invertible")
    return pdivmod(scale(1 / r0[0], s0), m)[1]


def square_free(f: Poly) -> List[Poly]:
    """Yun's algorithm: monic [P_1, P_2, ...] with f = lc(f) prod P_i^i."""
    f = trim(f)
    if len(f) <= 1:
        return []
    out = []
    a = pgcd(f, deriv(f))
    b = pdivmod(f, a)[0]
    c = pdivmod(deriv(f), a)[0]
    d = add(c, scale(-1, deriv(b)))
    while len(b) > 1:
        a = pgcd(b, d)
        out.append(a)
        b = pdivmod(b, a)[0]
        c = pdivmod(d, a)[0]
        d = add(c, scale(-1, deriv(b)))
    return out


def det(M: List[List[Fraction]]) -> Fraction:
    M = [list(r) for r in M]
    n = len(M)
    d = Fraction(1)
    for c in range(n):
        p = next((r for r in range(c, n) if M[r][c] != 0), None)
        if p is None:
            return Fraction(0)
        if p != c:
            M[c], M[p] = M[p], M[c]
            d = -d
        d *= M[c][c]
        for r in range(c + 1, n):
            if M[r][c] != 0:
                q = M[r][c] / M[c][c]
                M[r] = [a - q * b for a, b in zip(M[r], M[c])]
    return d


def subresultant(P: Poly, p: int, Q: Poly, q: int, j: int) -> Poly:
    """The j-th subresultant of P and Q, of formal degrees p >= q > j, by determinants."""
    size = p + q - 2 * j
    rows = []
    for k in range(q - j - 1, -1, -1):
        rows.append(([Fraction(0)] * k + list(P)) + [Fraction(0)] * (p + q - j))
    for k in range(p - j - 1, -1, -1):
        rows.append(([Fraction(0)] * k + list(Q)) + [Fraction(0)] * (p + q - j))
    top = p + q - j - 1  # columns x^top down to x^(j+1), then one column x^i
    res = []
    for i in range(j + 1):
        M = [[r[e] for e in range(top, j, -1)] + [r[i]] for r in rows]
        assert len(M) == size and len(M[0]) == size
        res.append(det(M))
    return trim(res)


def interpolate(xs: List[Fraction], ys: List[Fraction]) -> Poly:
    out: Poly = []
    for i, xi in enumerate(xs):
        basis = [Fraction(1)]
        for j, xj in enumerate(xs):
            if j != i:
                basis = scale(1 / (xi - xj), mul(basis, [-xj, Fraction(1)]))
        out = add(out, scale(ys[i], basis))
    return out


def bivariate_subresultant(A: Poly, D: Poly, j: int) -> BiPoly:
    """The j-th subresultant of D and A - t D' w.r.t. x, as a polynomial in Q[t][x]."""
    n = len(D) - 1
    Dp = deriv(D)
    ts = [Fraction(k) for k in range(n + 1)]  # t-degree is at most n - j
    samples = []
    for t0 in ts:
        B = [(A[k] if k < len(A) else 0) - t0 * (Dp[k] if k < len(Dp) else 0) for k in range(n)]
        s = subresultant(D, n, [Fraction(b) for b in B], n - 1, j)
        samples.append(s + [Fraction(0)] * (j + 1 - len(s)))
    S = [interpolate(ts, [samples[k][e] for k in range(len(ts))]) for e in range(j + 1)]
    while S and not S[-1]:
        S.pop()
    return S


def lazard_rioboo_trager(A: Poly, D: Poly) -> List[Tuple[Poly, BiPoly]]:
    """The pairs (Q_i, S_i) of the logarithmic part of the integral of A/D."""
    A, D = trim(A), trim(D)
    n = len(D) - 1
    if n < 1 or len(A) > n:
        raise ValueError("need deg A < deg D")
    if len(pgcd(D, deriv(D))) > 1:
        raise ValueError("D must be square-free")
    if not A:
        return []
    S0 = bivariate_subresultant(A, D, 0)
    R = S0[0] if S0 else []  # @line L1
    Qs = square_free(R)  # @line L2
    out = []
    for i, Qi in enumerate(Qs, start=1):  # @line L3
        if len(Qi) <= 1:
            continue
        if i == n:
            S = [[c] for c in D]  # @line L4
        else:
            S = bivariate_subresultant(A, D, i)  # @line L5
        for j, Aj in enumerate(square_free(S[-1]), start=1):  # @line L6
            g = pgcd(Aj, Qi)
            for _ in range(j):  # @line L7
                S = [pdivmod(c, g)[0] for c in S]
        S = [pdivmod(c, Qi)[1] for c in S]  # @line L8
        while S and not S[-1]:
            S.pop()
        u = inverse_mod(S[-1], Qi)  # make S monic in x modulo Q_i
        S = [pdivmod(mul(u, c), Qi)[1] for c in S]
        out.append((Qi, S))
    return out  # @line L9
test_lazard_rioboo_trager.py 117 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/lazard-rioboo-trager/. The authors are listed in prov/J2RB9Z.jsonl.

import random
from fractions import Fraction

import pytest

from lazard_rioboo_trager import (
    add,
    bivariate_subresultant,
    deriv,
    inverse_mod,
    lazard_rioboo_trager,
    mul,
    pdivmod,
    pgcd,
    scale,
    square_free,
    trim,
)

F = Fraction


def rem_mod_q(S, P, Q):
    """Remainder of P (in Q[t][x]) by S, monic in x, with coefficients in Q[t]/(Q)."""
    r = [pdivmod(c, Q)[1] for c in P]
    while r and not r[-1]:
        r.pop()
    while len(r) >= len(S):
        c = r[-1]
        s = len(r) - len(S)
        for i, b in enumerate(S):
            r[s + i] = pdivmod(add(r[s + i], scale(-1, mul(c, b))), Q)[1]
        while r and not r[-1]:
            r.pop()
    return r


def certify(A, D):
    """An exact certificate that sum alpha log S_i(alpha, x) differentiates to A/D.

    For each root alpha of Q_i, S_i(alpha, x) is monic of degree i and divides
    D and A - alpha D'. Distinct alphas give coprime factors, since a common
    root b of D would give A(b) = alpha D'(b) with D'(b) != 0. If moreover the
    degrees add up to deg D, the factors S_i(alpha, x) multiply to D / lc(D), and
    each root b of D lies in exactly one of them, with A(b)/D'(b) = alpha. Then
    the derivative sum_alpha alpha S_x / S = sum_b (A(b)/D'(b)) / (x - b) = A/D.
    """
    A, D = trim(A), trim(D)
    pairs = lazard_rioboo_trager(A, D)
    total = 0
    Dt = [[c] for c in D]
    Dp = deriv(D)
    # A - t D' in Q[t][x]: the coefficient of x^k is A_k - D'_k t.
    Bt = [trim([A[k] if k < len(A) else 0, -Dp[k]]) for k in range(len(Dp))]
    for idx, (Q, S) in enumerate(pairs):
        assert len(pgcd(Q, deriv(Q))) == 1  # square-free
        for Q2, _ in pairs[idx + 1:]:
            assert len(pgcd(Q, Q2)) == 1  # pairwise coprime
        i = len(S) - 1
        assert S[-1] == [F(1)]  # monic in x
        assert rem_mod_q(S, Dt, Q) == []
        assert rem_mod_q(S, Bt, Q) == []
        total += i * (len(Q) - 1)
    if A:
        assert total == len(D) - 1
    return pairs


def test_bronstein_example():
    A = [6, 0, -3, 0, 1]  # x^4 - 3x^2 + 6
    D = [4, 0, 5, 0, -5, 0, 1]  # x^6 - 5x^4 + 5x^2 + 4
    R = bivariate_subresultant(trim(A), trim(D), 0)[0]
    assert R == scale(45796, mul(mul([1, 0, 4], [1, 0, 4]), [1, 0, 4]))  # 214^2 (4t^2 + 1)^3
    pairs = certify(A, D)
    assert pairs == [([F(1, 4), 0, 1], [[0, -4], [-3], [0, 2], [1]])]  # x^3 + 2t x^2 - 3x - 4t


def test_small_examples():
    assert certify([1], [0, 1, 0, 1]) == [([-1, 1], [[], [1]]), ([F(1, 2), 1], [[1], [], [1]])]
    assert certify([1], [-2, 0, 1]) == [([F(-1, 8), 0, 1], [[0, -4], [1]])]  # 1/(x^2 - 2)
    assert certify([0, 1], [-1, 0, 1]) == [([F(-1, 2), 1], [[-1], [], [1]])]  # x/(x^2 - 1)
    assert certify([0, 0, 3], [1, 0, 0, 1]) == [([-1, 1], [[1], [], [], [1]])]  # all of D
    assert certify([5], [3, 1]) == [([-5, 1], [[3], [1]])]
    assert lazard_rioboo_trager([], [1, 0, 1]) == []


def test_random():
    rng = random.Random(1990)
    count = 0
    while count < 500:
        D = [F(rng.randint(1, 3))]
        for _ in range(rng.randint(1, 3)):
            p = [F(rng.randint(-3, 3)) for _ in range(rng.randint(1, 2))] + [F(1)]
            D = mul(D, p)
        if len(D) > 5 or len(pgcd(D, deriv(D))) > 1:
            continue
        A = trim([F(rng.randint(-3, 3)) for _ in range(len(D) - 1)])
        certify(A, D)
        count += 1


def test_inverse_and_square_free():
    m = [F(1), 0, 0, 1]
    a = [F(2), 1]
    assert pdivmod(mul(a, inverse_mod(a, m)), m)[1] == [1]
    f = mul(mul([F(-1), 1], [F(-1), 1]), [F(2), 1])
    assert square_free(f) == [[2, 1], [-1, 1]]


def test_bad_input():
    with pytest.raises(ValueError):
        lazard_rioboo_trager([1], [1, 2, 1])
    with pytest.raises(ValueError):
        lazard_rioboo_trager([1, 1], [1, 1])

What the next level needs

  • the tests did not run in this build

Read the full page about Lazard-Rioboo-Trager algorithm