From bb183f926f21b95e8b5dce6dc83ab66c0cc8db9d Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Fri, 24 Oct 2025 13:23:31 +0200 Subject: [PATCH 01/93] create blank class for nonsymbolic hypergeometric functions --- src/sage/functions/hypergeometric.py | 12 ++++++++++-- src/sage/functions/hypergeometric_algebraic.py | 18 ++++++++++++++++++ src/sage/functions/meson.build | 1 + 3 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 src/sage/functions/hypergeometric_algebraic.py diff --git a/src/sage/functions/hypergeometric.py b/src/sage/functions/hypergeometric.py index 0e9884780ae..2bc9fe6c4c5 100644 --- a/src/sage/functions/hypergeometric.py +++ b/src/sage/functions/hypergeometric.py @@ -750,8 +750,16 @@ def _deflated(self, a, b, z): return terms return ((1, new),) - -hypergeometric = Hypergeometric() +_hypergeometric = Hypergeometric() + +def hypergeometric(a, b, x): + from sage.rings.polynomial.polynomial_ring import PolynomialRing_generic + from sage.rings.power_series_ring import PowerSeriesRing_generic + if isinstance(x.parent(), (PolynomialRing_generic, PowerSeriesRing_generic)): + from sage.functions.hypergeometric_algebraic import HypergeometricAlgebraic + return HypergeometricAlgebraic(a, b, x) + else: + return _hypergeometric(a, b, x) def closed_form(hyp): diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py new file mode 100644 index 00000000000..d96865e723d --- /dev/null +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -0,0 +1,18 @@ +r""" +Algebraic properties of hypergeometric functions. + +AUTHORS: + +- Xavier Caruso, Florian Fürnsinn (2025-10): initial version +""" + +from sage.structure.sage_object import SageObject + +class HypergeometricAlgebraic(SageObject): + def __init__(self, a, b, x): + self._a = tuple(a) + self._b = tuple(b) + self._x = x + + def _repr_(self): + return "hypergeometric(%s, %s, %s)" % (self._a, self._b, self._x) diff --git a/src/sage/functions/meson.build b/src/sage/functions/meson.build index c37ec96e9ff..4172571c240 100644 --- a/src/sage/functions/meson.build +++ b/src/sage/functions/meson.build @@ -9,6 +9,7 @@ py.install_sources( 'generalized.py', 'hyperbolic.py', 'hypergeometric.py', + 'hypergeometric_algebraic.py', 'jacobi.py', 'log.py', 'min_max.py', From b07972a61c3db476d377d3a7a8fd7333f863eb7e Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Fri, 24 Oct 2025 17:40:29 +0200 Subject: [PATCH 02/93] first implementation (without documentation) --- src/sage/functions/hypergeometric.py | 15 +- .../functions/hypergeometric_algebraic.py | 283 +++++++++++++++++- 2 files changed, 286 insertions(+), 12 deletions(-) diff --git a/src/sage/functions/hypergeometric.py b/src/sage/functions/hypergeometric.py index 2bc9fe6c4c5..096c8a43012 100644 --- a/src/sage/functions/hypergeometric.py +++ b/src/sage/functions/hypergeometric.py @@ -752,13 +752,16 @@ def _deflated(self, a, b, z): _hypergeometric = Hypergeometric() -def hypergeometric(a, b, x): - from sage.rings.polynomial.polynomial_ring import PolynomialRing_generic - from sage.rings.power_series_ring import PowerSeriesRing_generic - if isinstance(x.parent(), (PolynomialRing_generic, PowerSeriesRing_generic)): - from sage.functions.hypergeometric_algebraic import HypergeometricAlgebraic - return HypergeometricAlgebraic(a, b, x) +def hypergeometric(a, b, x, scalar=None): + from sage.rings.polynomial.polynomial_element import Polynomial + from sage.rings.power_series_ring_element import PowerSeries + if isinstance(x, (Polynomial, PowerSeries)): + from sage.functions.hypergeometric_algebraic import Parameters, HypergeometricAlgebraic + parameters = Parameters(a, b) + return HypergeometricAlgebraic(parameters, x, scalar) else: + if scalar is not None: + raise ValueError return _hypergeometric(a, b, x) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index d96865e723d..f0e3f05b576 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -7,12 +7,283 @@ """ from sage.structure.sage_object import SageObject +from sage.misc.classcall_metaclass import ClasscallMetaclass +from sage.structure.sequence import Sequence -class HypergeometricAlgebraic(SageObject): - def __init__(self, a, b, x): - self._a = tuple(a) - self._b = tuple(b) - self._x = x +from sage.misc.misc_c import prod +from sage.misc.functional import log +from sage.functions.other import ceil +from sage.arith.functions import lcm +from sage.misc.cachefunc import cached_method + +from sage.rings.integer_ring import ZZ +from sage.rings.rational_field import QQ +from sage.rings.finite_rings.finite_field_constructor import FiniteField +from sage.rings.padics.factory import QpFP + +from sage.categories.pushout import pushout +from sage.categories.finite_fields import FiniteFields + +from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing +from sage.rings.power_series_ring import PowerSeriesRing +from sage.rings.polynomial.ore_polynomial_ring import OrePolynomialRing + + +def mod2(x, d): + return d - (-x) % d + + +class Parameters(): + def __init__(self, alpha, beta, is_one_included=False): + try: + alpha = sorted([QQ(a) for a in alpha]) + beta = sorted([QQ(b) for b in beta]) + except TypeError: + raise NotImplementedError("parameters must be rational numbers") + i = j = 0 + while i < len(alpha) and j < len(beta): + if alpha[i] == beta[j]: + del alpha[i] + del beta[j] + elif alpha[i] > beta[j]: + j += 1 + else: + i += 1 + if not is_one_included: + beta.append(QQ(1)) + self.alpha = alpha + self.beta = beta + if len(alpha) == 0 and len(beta) == 0: + self.d = 1 + self.bound = 1 + else: + self.d = lcm([ a.denominator() for a in alpha ] + + [ b.denominator() for b in beta ]) + self.bound = 2 * self.d * max(alpha + beta) + 1 + + def is_balanced(self): + return len(self.alpha) == len(self.beta) + + @cached_method + def parenthesis_criterion(self, c): + d = self.d + A = [(mod2(d*c*a, d), -a, 1) for a in self.alpha] + B = [(mod2(d*c*b, d), -b, -1) for b in self.beta] + AB = sorted(A + B) + parenthesis = 0 + previous_paren = -1 + for _, _, paren in AB: + parenthesis += paren + if parenthesis < 0: + return False + previous_paren = paren + return parenthesis >= 0 + + @cached_method + def interlacing_criterion(self, c): + d = self.d + A = [(mod2(d*c*a, d), 1) for a in self.alpha] + B = [(mod2(d*c*b, d), -1) for b in self.beta] + AB = sorted(A + B) + previous_paren = -1 + for _, paren in AB: + if paren == previous_paren: + return False + previous_paren = paren + return True + + def remove_positive_integer_differences(self): + differences = [] + alpha = self.alpha[:] + beta = self.beta[:] + for i in range(len(alpha)): + for j in range(len(beta)): + diff = alpha[i] - beta[j] + if diff in ZZ and diff > 0: + differences.append((diff, i, j)) + for _, i, j in sorted(differences): + if alpha[i] is not None and beta[j] is not None: + alpha[i] = None + beta[j] = None + alpha = [a for a in alpha if a is not None] + beta = [b for b in beta if b is not None] + return Parameters(alpha, beta, is_one_included=True) + + def has_negative_integer_differences(self): + return any(a - b in ZZ and a < b for a in self.alpha for b in self.beta) + + def shift(self): + alpha = [a+1 for a in self.alpha] + beta = [b+1 for b in self.beta] + return Parameters(alpha, beta) + + def scalar(self): + return prod(self.alpha) / prod(self.beta) + + +class HypergeometricAlgebraic(SageObject, metaclass=ClasscallMetaclass): + @staticmethod + def __classcall_private__(cls, parameters, x, scalar): + base = x.parent().base_ring() + if base in FiniteFields() and base.is_prime_field(): + return HypergeometricAlgebraic_GFp(parameters, x, scalar) + if base.characteristic() == 0: + base = pushout(base, QQ) + if base is QQ: + return HypergeometricAlgebraic_QQ(parameters, x, scalar) + return HypergeometricAlgebraic_charzero(parameters, x, scalar) + else: + raise NotImplementedError + + def __init__(self, parameters, x, scalar=None): + self._base = base = x.base_ring() + S = x.parent() + if x != S.gen(): + raise NotImplementedError("x must be the variable") # find a better message + if scalar is None: + self._scalar = base.one() + else: + self._scalar = base(scalar) + if self._scalar == 0: + self._parameters = None + else: + self._parameters = parameters + self._x = S.gen() + self._variable_name = S.variable_name() def _repr_(self): - return "hypergeometric(%s, %s, %s)" % (self._a, self._b, self._x) + s = "hypergeometric(%s, %s, %s)" % (self.alpha(), self.beta(), self._variable_name) + if self._scalar != 1: + s = str(self._scalar) + "*" + s + return s + + def alpha(self): + return tuple(self._parameters.alpha) + + def beta(self): + return tuple(self._parameters.beta)[:-1] + + def denominator(self): + return self._parameters.d + + def differential_operator(self, var='d'): + S = PolynomialRing(self._base, self._variable_name) + x = S.gen() + D = OrePolynomialRing(S, S.derivation(), names=var) + if self._scalar == 0: + return D.one() + t = x * D.gen() + A = D.one() + for a in self.alpha(): + A *= t + S(a) + B = D.one() + for b in self.beta(): + B *= t + S(b-1) + L = t*B - x*A + return D([ c//x for c in L.list() ]) + + def derivative(self): + parameters = self._parameters.shift() + scalar = self._base(self._parameters.scalar()) * self._scalar + return HypergeometricAlgebraic(parameters, self._x, scalar) + + +class HypergeometricAlgebraic_charzero(HypergeometricAlgebraic): + def is_defined(self): + return not any(b in ZZ and b < 0 for b in self._beta) + + def series(self, prec): + S = PowerSeriesRing(self._base, name=self._variable_name) + c = self._scalar + coeffs = [c] + for i in range(prec): + for a in self.alpha(): + c *= a + i + for b in self.beta(): + c /= b + i + c /= i + 1 + coeffs.append(c) + return S(coeffs, prec=prec) + + +class HypergeometricAlgebraic_QQ(HypergeometricAlgebraic_charzero): + def reduce(self, p): + F = FiniteField(p) + x = F['x'].gen() + return HypergeometricAlgebraic(self._parameters, x, self._scalar) + + def has_good_reduction(self, p): + h = self.reduce(p) + return h.is_defined() + + def is_algebraic(self): + if any(a in ZZ and a <= 0 for a in self.alpha()): + return True + if not self._parameters.is_balanced(): + return False + simplified_parameters = self._parameters.remove_positive_integer_differences() + if simplified_parameters.has_negative_integer_differences(): + return False + d = self._parameters.d + return all(simplified_parameters.interlacing_criterion(c) + for c in range(d) if d.gcd(c) == 1) + + def is_globally_bounded(self, include_infinity=True): + if include_infinity and len(self.alpha()) > len(self.beta()) + 1: + return False + d = self.denominator() + for c in range(d): + if d.gcd(c) == 1: + if not self._parameters.parenthesis_criterion(c): + return False + return True + + +class HypergeometricAlgebraic_GFp(HypergeometricAlgebraic): + def __init__(self, alpha, beta, x, scalar=None): + HypergeometricAlgebraic.__init__(self, alpha, beta, x, scalar) + self._p = self._base.cardinality() + + def is_defined(self): + p = self._p + d = self.denominator() + if d.gcd(p) > 1: + return False + u = 1 + if not self._parameters.parenthesis_criterion(u): + return False + u = p % d + while u != 1: + if not self._parameters.parenthesis_criterion(u): + return False + u = p*u % d + bound = self._parameters.bound + if bound < p: + return True + prec = 1 + p ** ceil(log(self._bound(), p)) + try: + self.series(prec) + except ValueError: + return False + return True + + def series(self, prec): + S = PowerSeriesRing(self._base, name=self._variable_name) + p = self._p + pprec = max(1, (len(self._beta) + 1) * ceil(log(prec, p))) + K = QpFP(p, pprec) + c = K(self._scalar) + coeffs = [c] + for i in range(prec-1): + for a in self.alpha(): + c *= a + i + for b in self._beta(): + c /= b + i + c /= i + 1 + if c.valuation() < 0: + raise ValueError("denominator appears in the series at the required precision") + coeffs.append(c) + return S(coeffs, prec=prec) + + def is_algebraic(self): + return self.is_defined() From 139ddce87d8e0f2fb53f6243f725de3756eab15c Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Mon, 27 Oct 2025 14:44:11 +0100 Subject: [PATCH 03/93] p-curvature and Dwork relations --- .../functions/hypergeometric_algebraic.py | 189 +++++++++++++++--- 1 file changed, 165 insertions(+), 24 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index f0e3f05b576..1f7bc089ad9 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -1,6 +1,8 @@ r""" Algebraic properties of hypergeometric functions. +[Tutorial] + AUTHORS: - Xavier Caruso, Florian Fürnsinn (2025-10): initial version @@ -24,6 +26,8 @@ from sage.categories.pushout import pushout from sage.categories.finite_fields import FiniteFields +from sage.matrix.constructor import matrix + from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing from sage.rings.power_series_ring import PowerSeriesRing from sage.rings.polynomial.ore_polynomial_ring import OrePolynomialRing @@ -32,12 +36,47 @@ def mod2(x, d): return d - (-x) % d - class Parameters(): - def __init__(self, alpha, beta, is_one_included=False): + r""" + Class for parameters of hypergeometric functions. + """ + def __init__(self, alpha, beta, add_one=True): + r""" + Initialize this set of parameters. + + INPUT: + + - ``alpha`` -- list of top parameters + + - ``beta`` -- list of bottom parameters + + - ``add_one`` -- boolean (default: ``True``), + if ``True``, add an additional one to the bottom + parameters. + + EXAMPLES:: + + sage: from sage.functions.hypergeometric_algebraic import Parameters + sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: p + ([1/4, 1/3, 1/2], [2/5, 3/5, 1]) + sage: type(p) + + + By default, parameters are sorted, duplicates are removed and + a trailing `1` is added to the bottom parameters:: + + sage: Parameters([1/2, 1/3, 2/3], [2/3]) + ([1/3, 1/2], [1]) + + We can avoid adding the trailing `1` by passing ``add_one=False``):: + + sage: Parameters([1/2, 1/3, 2/3], [2/3], add_one=False) + ([1/3, 1/2], []) + """ try: - alpha = sorted([QQ(a) for a in alpha]) - beta = sorted([QQ(b) for b in beta]) + alpha = sorted([QQ(a) for a in alpha if a is not None]) + beta = sorted([QQ(b) for b in beta if b is not None]) except TypeError: raise NotImplementedError("parameters must be rational numbers") i = j = 0 @@ -49,7 +88,7 @@ def __init__(self, alpha, beta, is_one_included=False): j += 1 else: i += 1 - if not is_one_included: + if add_one: beta.append(QQ(1)) self.alpha = alpha self.beta = beta @@ -61,15 +100,56 @@ def __init__(self, alpha, beta, is_one_included=False): + [ b.denominator() for b in beta ]) self.bound = 2 * self.d * max(alpha + beta) + 1 + def __repr__(self): + r""" + Return a string representation of these parameters. + + EXAMPLES:: + + sage: from sage.functions.hypergeometric_algebraic import Parameters + sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: p # indirect doctest + ([1/4, 1/3, 1/2], [2/5, 3/5, 1]) + """ + return "(%s, %s)" % (self.alpha, self.beta) + def is_balanced(self): + r""" + Return ``True`` if there are as many top parameters as bottom + parameters; ``False`` otherwise. + + EXAMPLES:: + + sage: from sage.functions.hypergeometric_algebraic import Parameters + sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: p + ([1/4, 1/3, 1/2], [2/5, 3/5, 1]) + sage: p.is_balanced() + True + + :: + + sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/5], add_one=False) + sage: p + ([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: p.is_balanced() + False + """ return len(self.alpha) == len(self.beta) @cached_method - def parenthesis_criterion(self, c): + def christol_sorting(self, c=1): + r""" + """ d = self.d - A = [(mod2(d*c*a, d), -a, 1) for a in self.alpha] - B = [(mod2(d*c*b, d), -b, -1) for b in self.beta] - AB = sorted(A + B) + def mod2(x): + return d - (-x) % d + A = [mod2(d*c*a), -a, 1) for a in self.alpha] + B = [mod2(d*c*b), -b, -1) for b in self.beta] + return sorted(A + B) + + def parenthesis_criterion(self, c): + AB = self.christol_sorting(c) parenthesis = 0 previous_paren = -1 for _, _, paren in AB: @@ -79,12 +159,8 @@ def parenthesis_criterion(self, c): previous_paren = paren return parenthesis >= 0 - @cached_method def interlacing_criterion(self, c): - d = self.d - A = [(mod2(d*c*a, d), 1) for a in self.alpha] - B = [(mod2(d*c*b, d), -1) for b in self.beta] - AB = sorted(A + B) + AB = self.christol_sorting(c) previous_paren = -1 for _, paren in AB: if paren == previous_paren: @@ -105,9 +181,7 @@ def remove_positive_integer_differences(self): if alpha[i] is not None and beta[j] is not None: alpha[i] = None beta[j] = None - alpha = [a for a in alpha if a is not None] - beta = [b for b in beta if b is not None] - return Parameters(alpha, beta, is_one_included=True) + return Parameters(alpha, beta, add_one=False) def has_negative_integer_differences(self): return any(a - b in ZZ and a < b for a in self.alpha for b in self.beta) @@ -133,7 +207,7 @@ def __classcall_private__(cls, parameters, x, scalar): return HypergeometricAlgebraic_QQ(parameters, x, scalar) return HypergeometricAlgebraic_charzero(parameters, x, scalar) else: - raise NotImplementedError + raise NotImplementedError("the base must have characteristic zero or be a finite field") def __init__(self, parameters, x, scalar=None): self._base = base = x.base_ring() @@ -148,6 +222,7 @@ def __init__(self, parameters, x, scalar=None): self._parameters = None else: self._parameters = parameters + self._S = S self._x = S.gen() self._variable_name = S.variable_name() @@ -207,11 +282,13 @@ def series(self, prec): class HypergeometricAlgebraic_QQ(HypergeometricAlgebraic_charzero): - def reduce(self, p): + def mod(self, p): F = FiniteField(p) x = F['x'].gen() return HypergeometricAlgebraic(self._parameters, x, self._scalar) + __mod__ = mod + def has_good_reduction(self, p): h = self.reduce(p) return h.is_defined() @@ -240,8 +317,8 @@ def is_globally_bounded(self, include_infinity=True): class HypergeometricAlgebraic_GFp(HypergeometricAlgebraic): - def __init__(self, alpha, beta, x, scalar=None): - HypergeometricAlgebraic.__init__(self, alpha, beta, x, scalar) + def __init__(self, parameters, x, scalar=None): + HypergeometricAlgebraic.__init__(self, parameters, x, scalar) self._p = self._base.cardinality() def is_defined(self): @@ -260,7 +337,7 @@ def is_defined(self): bound = self._parameters.bound if bound < p: return True - prec = 1 + p ** ceil(log(self._bound(), p)) + prec = 1 + p ** ceil(log(self._parameters.bound, p)) try: self.series(prec) except ValueError: @@ -270,14 +347,14 @@ def is_defined(self): def series(self, prec): S = PowerSeriesRing(self._base, name=self._variable_name) p = self._p - pprec = max(1, (len(self._beta) + 1) * ceil(log(prec, p))) + pprec = max(1, (len(self.beta()) + 1) * ceil(log(prec, p))) K = QpFP(p, pprec) c = K(self._scalar) coeffs = [c] for i in range(prec-1): for a in self.alpha(): c *= a + i - for b in self._beta(): + for b in self.beta(): c /= b + i c /= i + 1 if c.valuation() < 0: @@ -287,3 +364,67 @@ def series(self, prec): def is_algebraic(self): return self.is_defined() + + def p_curvature(self): + L = self.differential_operator() + K = L.base_ring().fraction_field() + S = OrePolynomialRing(K, L.parent().twisting_derivation().extend_to_fraction_field(), names='d') + L = S(L.list()) + d = S.gen() + p = self._p + rows = [ ] + n = L.degree() + for i in range(p, p + n): + Li = d**i % L + rows.append([Li[j] for j in range(n)]) + return matrix(rows) + + def dwork_relation(self): + r""" + Return (P1, g1), ..., (Ps, gs) such that + + h = P1*g1^p + ... + Ps*gs^p + """ + if not self._parameters.is_balanced(): + raise ValueError("the hypergeometric function must be a pFq with q = p-1") + if not self.is_defined(): + raise ValueError("this hypergeometric function is not defined") + + # We compute the series expansion up to x^p + p = self._p + S = self._S + cs = self.series(p).list() + + # We compute the relevant exponents + exponents = sorted([(1-b) % p for b in self._parameters.beta]) + exponents.append(p) + Ps = [] + for i in range(len(exponents) - 1): + e = exponents[i] + if e < len(cs) and cs[e]: + P = S(cs[e:exponents[i+1]]) << e + Ps.append((e, P)) + + # We compute the hypergeometric series + Hs = [ ] + for r, P in Ps: + alpha = [ ] + for a in self.alpha(): + ap = (a + (-a) % p) / p # Dwork map + ar = prod(a + i for i in range(r)) + if ar % p == 0: + alpha.append(ap + 1) + else: + alpha.append(ap) + beta = [ ] + for b in self.beta(): + bp = (b + (-b) % p) / p # Dwork map + br = prod(b + i for i in range(r)) + if br % p == 0: + beta.append(bp + 1) + else: + beta.append(bp) + parameters = Parameters(alpha, beta) + Hs.append((P, HypergeometricAlgebraic_GFp(parameters, self._x))) + + return Hs From fd65158181fae653f2d5eda099d4e5302270397c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20F=C3=BCrnsinn?= Date: Mon, 27 Oct 2025 16:25:52 +0100 Subject: [PATCH 04/93] Documentation for scalar --- src/sage/functions/hypergeometric_algebraic.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 1f7bc089ad9..f1b0f56a4f6 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -192,6 +192,19 @@ def shift(self): return Parameters(alpha, beta) def scalar(self): + r""" + Return the scalar of the derivative of the hypergeometric function with + this set of parameters. + + EXAMPLES:: + + sage: from sage.functions.hypergeometric_algebraic import Parameters + sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: p + ([1/4, 1/3, 1/2], [2/5, 3/5, 1]) + sage: p.scalar() + 25/144 + """ return prod(self.alpha) / prod(self.beta) From 9c60b11d1af90d5b32a14c03b92f6155ce187474 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Mon, 27 Oct 2025 16:26:52 +0100 Subject: [PATCH 05/93] framework Parent/Element --- src/sage/functions/hypergeometric.py | 12 +- .../functions/hypergeometric_algebraic.py | 259 +++++++++++------- 2 files changed, 160 insertions(+), 111 deletions(-) diff --git a/src/sage/functions/hypergeometric.py b/src/sage/functions/hypergeometric.py index 096c8a43012..1e933917885 100644 --- a/src/sage/functions/hypergeometric.py +++ b/src/sage/functions/hypergeometric.py @@ -752,16 +752,18 @@ def _deflated(self, a, b, z): _hypergeometric = Hypergeometric() -def hypergeometric(a, b, x, scalar=None): +def hypergeometric(a, b, x): from sage.rings.polynomial.polynomial_element import Polynomial from sage.rings.power_series_ring_element import PowerSeries if isinstance(x, (Polynomial, PowerSeries)): - from sage.functions.hypergeometric_algebraic import Parameters, HypergeometricAlgebraic + from sage.functions.hypergeometric_algebraic import Parameters, HypergeometricFunctions + if not x.is_gen(): + raise NotImplementedError("the argument must be the generator of the polynomial ring") + S = x.parent() + H = HypergeometricFunctions(S.base_ring(), S.variable_name()) parameters = Parameters(a, b) - return HypergeometricAlgebraic(parameters, x, scalar) + return H(parameters) else: - if scalar is not None: - raise ValueError return _hypergeometric(a, b, x) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 1f7bc089ad9..66ff8f1a743 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -8,47 +8,61 @@ - Xavier Caruso, Florian Fürnsinn (2025-10): initial version """ -from sage.structure.sage_object import SageObject -from sage.misc.classcall_metaclass import ClasscallMetaclass +# *************************************************************************** +# Copyright (C) 2025 Xavier Caruso +# Florian Fürnsinn +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# https://www.gnu.org/licenses/ +# *************************************************************************** + +from sage.structure.unique_representation import UniqueRepresentation +from sage.structure.parent import Parent +from sage.structure.element import Element from sage.structure.sequence import Sequence +from sage.structure.category_object import normalize_names -from sage.misc.misc_c import prod -from sage.misc.functional import log -from sage.functions.other import ceil -from sage.arith.functions import lcm -from sage.misc.cachefunc import cached_method +from sage.categories.pushout import pushout +from sage.categories.map import Map +from sage.categories.finite_fields import FiniteFields +from sage.symbolic.ring import SR from sage.rings.integer_ring import ZZ from sage.rings.rational_field import QQ from sage.rings.finite_rings.finite_field_constructor import FiniteField from sage.rings.padics.factory import QpFP -from sage.categories.pushout import pushout -from sage.categories.finite_fields import FiniteFields - -from sage.matrix.constructor import matrix - from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing from sage.rings.power_series_ring import PowerSeriesRing from sage.rings.polynomial.ore_polynomial_ring import OrePolynomialRing +from sage.misc.misc_c import prod +from sage.misc.functional import log +from sage.functions.other import ceil +from sage.arith.functions import lcm +from sage.misc.cachefunc import cached_method -def mod2(x, d): - return d - (-x) % d +from sage.matrix.constructor import matrix + +# Parameters of hypergeometric functions +######################################## class Parameters(): r""" Class for parameters of hypergeometric functions. """ - def __init__(self, alpha, beta, add_one=True): + def __init__(self, top, bottom, add_one=True): r""" Initialize this set of parameters. INPUT: - - ``alpha`` -- list of top parameters + - ``top`` -- list of top parameters - - ``beta`` -- list of bottom parameters + - ``bottom`` -- list of bottom parameters - ``add_one`` -- boolean (default: ``True``), if ``True``, add an additional one to the bottom @@ -75,30 +89,30 @@ def __init__(self, alpha, beta, add_one=True): ([1/3, 1/2], []) """ try: - alpha = sorted([QQ(a) for a in alpha if a is not None]) - beta = sorted([QQ(b) for b in beta if b is not None]) + top = sorted([QQ(a) for a in top if a is not None]) + bottom = sorted([QQ(b) for b in bottom if b is not None]) except TypeError: raise NotImplementedError("parameters must be rational numbers") i = j = 0 - while i < len(alpha) and j < len(beta): - if alpha[i] == beta[j]: - del alpha[i] - del beta[j] - elif alpha[i] > beta[j]: + while i < len(top) and j < len(bottom): + if top[i] == bottom[j]: + del top[i] + del bottom[j] + elif top[i] > bottom[j]: j += 1 else: i += 1 if add_one: - beta.append(QQ(1)) - self.alpha = alpha - self.beta = beta - if len(alpha) == 0 and len(beta) == 0: + bottom.append(QQ(1)) + self.top = top + self.bottom = bottom + if len(top) == 0 and len(bottom) == 0: self.d = 1 self.bound = 1 else: - self.d = lcm([ a.denominator() for a in alpha ] - + [ b.denominator() for b in beta ]) - self.bound = 2 * self.d * max(alpha + beta) + 1 + self.d = lcm([ a.denominator() for a in top ] + + [ b.denominator() for b in bottom ]) + self.bound = 2 * self.d * max(top + bottom) + 1 def __repr__(self): r""" @@ -111,7 +125,7 @@ def __repr__(self): sage: p # indirect doctest ([1/4, 1/3, 1/2], [2/5, 3/5, 1]) """ - return "(%s, %s)" % (self.alpha, self.beta) + return "(%s, %s)" % (self.top, self.bottom) def is_balanced(self): r""" @@ -135,7 +149,7 @@ def is_balanced(self): sage: p.is_balanced() False """ - return len(self.alpha) == len(self.beta) + return len(self.top) == len(self.bottom) @cached_method def christol_sorting(self, c=1): @@ -144,8 +158,8 @@ def christol_sorting(self, c=1): d = self.d def mod2(x): return d - (-x) % d - A = [mod2(d*c*a), -a, 1) for a in self.alpha] - B = [mod2(d*c*b), -b, -1) for b in self.beta] + A = [(mod2(d*c*a), -a, 1) for a in self.top] + B = [(mod2(d*c*b), -b, -1) for b in self.bottom] return sorted(A + B) def parenthesis_criterion(self, c): @@ -162,7 +176,7 @@ def parenthesis_criterion(self, c): def interlacing_criterion(self, c): AB = self.christol_sorting(c) previous_paren = -1 - for _, paren in AB: + for _, _, paren in AB: if paren == previous_paren: return False previous_paren = paren @@ -170,50 +184,38 @@ def interlacing_criterion(self, c): def remove_positive_integer_differences(self): differences = [] - alpha = self.alpha[:] - beta = self.beta[:] - for i in range(len(alpha)): - for j in range(len(beta)): - diff = alpha[i] - beta[j] + top = self.top[:] + bottom = self.bottom[:] + for i in range(len(top)): + for j in range(len(bottom)): + diff = top[i] - bottom[j] if diff in ZZ and diff > 0: differences.append((diff, i, j)) for _, i, j in sorted(differences): - if alpha[i] is not None and beta[j] is not None: - alpha[i] = None - beta[j] = None - return Parameters(alpha, beta, add_one=False) + if top[i] is not None and bottom[j] is not None: + top[i] = None + bottom[j] = None + return Parameters(top, bottom, add_one=False) def has_negative_integer_differences(self): - return any(a - b in ZZ and a < b for a in self.alpha for b in self.beta) + return any(a - b in ZZ and a < b for a in self.top for b in self.bottom) def shift(self): - alpha = [a+1 for a in self.alpha] - beta = [b+1 for b in self.beta] - return Parameters(alpha, beta) + top = [a+1 for a in self.top] + bottom = [b+1 for b in self.bottom] + return Parameters(top, bottom) def scalar(self): - return prod(self.alpha) / prod(self.beta) + return prod(self.top) / prod(self.bottom) -class HypergeometricAlgebraic(SageObject, metaclass=ClasscallMetaclass): - @staticmethod - def __classcall_private__(cls, parameters, x, scalar): - base = x.parent().base_ring() - if base in FiniteFields() and base.is_prime_field(): - return HypergeometricAlgebraic_GFp(parameters, x, scalar) - if base.characteristic() == 0: - base = pushout(base, QQ) - if base is QQ: - return HypergeometricAlgebraic_QQ(parameters, x, scalar) - return HypergeometricAlgebraic_charzero(parameters, x, scalar) - else: - raise NotImplementedError("the base must have characteristic zero or be a finite field") +# Hypergeometric functions +########################## - def __init__(self, parameters, x, scalar=None): - self._base = base = x.base_ring() - S = x.parent() - if x != S.gen(): - raise NotImplementedError("x must be the variable") # find a better message +class HypergeometricAlgebraic(Element): + def __init__(self, parent, parameters, scalar=None): + Element.__init__(self, parent) + base = parent.base_ring() if scalar is None: self._scalar = base.one() else: @@ -222,21 +224,21 @@ def __init__(self, parameters, x, scalar=None): self._parameters = None else: self._parameters = parameters - self._S = S - self._x = S.gen() - self._variable_name = S.variable_name() def _repr_(self): - s = "hypergeometric(%s, %s, %s)" % (self.alpha(), self.beta(), self._variable_name) + s = "hypergeometric(%s, %s, %s)" % (self.top(), self.bottom(), self.parent().variable_name()) if self._scalar != 1: s = str(self._scalar) + "*" + s return s - def alpha(self): - return tuple(self._parameters.alpha) + def base_ring(self): + return self.parent().base_ring() + + def top(self): + return tuple(self._parameters.top) - def beta(self): - return tuple(self._parameters.beta)[:-1] + def bottom(self): + return tuple(self._parameters.bottom)[:-1] def denominator(self): return self._parameters.d @@ -249,10 +251,10 @@ def differential_operator(self, var='d'): return D.one() t = x * D.gen() A = D.one() - for a in self.alpha(): + for a in self.top(): A *= t + S(a) B = D.one() - for b in self.beta(): + for b in self.bottom(): B *= t + S(b-1) L = t*B - x*A return D([ c//x for c in L.list() ]) @@ -260,21 +262,21 @@ def differential_operator(self, var='d'): def derivative(self): parameters = self._parameters.shift() scalar = self._base(self._parameters.scalar()) * self._scalar - return HypergeometricAlgebraic(parameters, self._x, scalar) + return HypergeometricAlgebraic(parameters, scalar=scalar) class HypergeometricAlgebraic_charzero(HypergeometricAlgebraic): def is_defined(self): - return not any(b in ZZ and b < 0 for b in self._beta) + return not any(b in ZZ and b < 0 for b in self._bottom) def series(self, prec): S = PowerSeriesRing(self._base, name=self._variable_name) c = self._scalar coeffs = [c] for i in range(prec): - for a in self.alpha(): + for a in self.top(): c *= a + i - for b in self.beta(): + for b in self.bottom(): c /= b + i c /= i + 1 coeffs.append(c) @@ -283,9 +285,8 @@ def series(self, prec): class HypergeometricAlgebraic_QQ(HypergeometricAlgebraic_charzero): def mod(self, p): - F = FiniteField(p) - x = F['x'].gen() - return HypergeometricAlgebraic(self._parameters, x, self._scalar) + H = HypergeometricFunctions(FiniteField(p), self.parent().variable_name()) + return H(self._parameters, self._scalar) __mod__ = mod @@ -294,19 +295,19 @@ def has_good_reduction(self, p): return h.is_defined() def is_algebraic(self): - if any(a in ZZ and a <= 0 for a in self.alpha()): + if any(a in ZZ and a <= 0 for a in self.top()): return True if not self._parameters.is_balanced(): return False simplified_parameters = self._parameters.remove_positive_integer_differences() if simplified_parameters.has_negative_integer_differences(): return False - d = self._parameters.d + d = simplified_parameters.d return all(simplified_parameters.interlacing_criterion(c) for c in range(d) if d.gcd(c) == 1) def is_globally_bounded(self, include_infinity=True): - if include_infinity and len(self.alpha()) > len(self.beta()) + 1: + if include_infinity and len(self.top()) > len(self.bottom()) + 1: return False d = self.denominator() for c in range(d): @@ -317,9 +318,9 @@ def is_globally_bounded(self, include_infinity=True): class HypergeometricAlgebraic_GFp(HypergeometricAlgebraic): - def __init__(self, parameters, x, scalar=None): - HypergeometricAlgebraic.__init__(self, parameters, x, scalar) - self._p = self._base.cardinality() + def __init__(self, parent, parameters, scalar=None): + HypergeometricAlgebraic.__init__(self, parent, parameters, scalar) + self._p = self.base_ring().cardinality() def is_defined(self): p = self._p @@ -345,16 +346,16 @@ def is_defined(self): return True def series(self, prec): - S = PowerSeriesRing(self._base, name=self._variable_name) + S = self.parent().polynomial_ring() p = self._p - pprec = max(1, (len(self.beta()) + 1) * ceil(log(prec, p))) + pprec = max(1, (len(self.bottom()) + 1) * ceil(log(prec, p))) K = QpFP(p, pprec) c = K(self._scalar) coeffs = [c] for i in range(prec-1): - for a in self.alpha(): + for a in self.top(): c *= a + i - for b in self.beta(): + for b in self.bottom(): c /= b + i c /= i + 1 if c.valuation() < 0: @@ -392,11 +393,11 @@ def dwork_relation(self): # We compute the series expansion up to x^p p = self._p - S = self._S + S = self.parent().polynomial_ring() cs = self.series(p).list() # We compute the relevant exponents - exponents = sorted([(1-b) % p for b in self._parameters.beta]) + exponents = sorted([(1-b) % p for b in self._parameters.bottom]) exponents.append(p) Ps = [] for i in range(len(exponents) - 1): @@ -408,23 +409,69 @@ def dwork_relation(self): # We compute the hypergeometric series Hs = [ ] for r, P in Ps: - alpha = [ ] - for a in self.alpha(): + top = [ ] + for a in self.top(): ap = (a + (-a) % p) / p # Dwork map ar = prod(a + i for i in range(r)) if ar % p == 0: - alpha.append(ap + 1) + top.append(ap + 1) else: - alpha.append(ap) - beta = [ ] - for b in self.beta(): + top.append(ap) + bottom = [ ] + for b in self.bottom(): bp = (b + (-b) % p) / p # Dwork map br = prod(b + i for i in range(r)) if br % p == 0: - beta.append(bp + 1) + bottom.append(bp + 1) else: - beta.append(bp) - parameters = Parameters(alpha, beta) + bottom.append(bp) + parameters = Parameters(top, bottom) Hs.append((P, HypergeometricAlgebraic_GFp(parameters, self._x))) return Hs + + +# Parent +######## + +class HypergeometricToSR(Map): + def _call_(self, h): + from sage.functions.hypergeometric import _hypergeometric + return _hypergeometric(h.top(), h.bottom(), SR.var(h.parent().variable_name())) + +class HypergeometricFunctions(Parent, UniqueRepresentation): + def __init__(self, base, name, category=None): + self._name = normalize_names(1, name)[0] + char = base.characteristic() + if base in FiniteFields() and base.is_prime_field(): + self.Element = HypergeometricAlgebraic_GFp + elif char == 0: + base = pushout(base, QQ) + if base is QQ: + self.Element = HypergeometricAlgebraic_QQ + else: + self.Element = HypergeometricAlgebraic_charzero + else: + raise NotImplementedError("hypergeometric functions are only implemented over finite field and bases of characteristic zero") + Parent.__init__(self, base, category=category) + if char == 0: + SR.register_coercion(HypergeometricToSR(self.Hom(SR))) + + def _repr_(self): + return "Hypergeometric functions in %s over %s" % (self._name, self._base) + + def _element_constructor_(self, *args, **kwds): + return self.element_class(self, *args, **kwds) + + def base_ring(self): + return self._base + + def variable_name(self): + return self._name + + def polynomial_ring(self): + return PolynomialRing(self.base_ring(), self._name) + + def power_series_ring(self): + return PowerSeriesRing(self.base_ring(), self._name) + From abac8eec706dad3a9c55f0b58d9b66c36aced442 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Mon, 27 Oct 2025 16:40:43 +0100 Subject: [PATCH 06/93] fix indentation --- src/sage/functions/hypergeometric_algebraic.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 829c5a6d08d..ec438d79e11 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -206,19 +206,19 @@ def shift(self): return Parameters(top, bottom) def scalar(self): - r""" - Return the scalar of the derivative of the hypergeometric function with - this set of parameters. + r""" + Return the scalar of the derivative of the hypergeometric function with + this set of parameters. - EXAMPLES:: + EXAMPLES:: - sage: from sage.functions.hypergeometric_algebraic import Parameters + sage: from sage.functions.hypergeometric_algebraic import Parameters sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) sage: p ([1/4, 1/3, 1/2], [2/5, 3/5, 1]) sage: p.scalar() 25/144 - """ + """ return prod(self.top) / prod(self.bottom) @@ -485,6 +485,5 @@ def variable_name(self): def polynomial_ring(self): return PolynomialRing(self.base_ring(), self._name) - def power_series_ring(self): - return PowerSeriesRing(self.base_ring(), self._name) - + def power_series_ring(self, default_prec=None): + return PowerSeriesRing(self.base_ring(), self._name, default_prec=prec) From 11d5fbf8b1e1b1d9d71ece2165e9642d4bc846cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20F=C3=BCrnsinn?= Date: Mon, 27 Oct 2025 17:27:37 +0100 Subject: [PATCH 07/93] Added documentation, fixed derivative of hypergeometric functions --- .../functions/hypergeometric_algebraic.py | 43 +++++++++++++++++-- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index ec438d79e11..ed6413ddfd6 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -198,9 +198,43 @@ def remove_positive_integer_differences(self): return Parameters(top, bottom, add_one=False) def has_negative_integer_differences(self): + r""" + Returns ``True`` if there exists a pair of a top parameter and a bottom + parameter, such that the top one minus the bottom one is a negative integer. + Returns ``False`` otherwise. + EXAMPLES:: + + sage: from sage.functions.hypergeometric_algebraic import Parameters + sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: p + ([1/4, 1/3, 1/2], [2/5, 3/5, 1]) + sage: p.has_negative_integer_differences() + False + + :: + + sage: from sage.functions.hypergeometric_algebraic import Parameters + sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/2]) + sage: p + ([1/4, 1/3, 1/2], [2/5, 3/2, 1]) + sage: p.has_negative_integer_differences() + True + """ return any(a - b in ZZ and a < b for a in self.top for b in self.bottom) def shift(self): + r""" + Return the parameters obtained by adding one to each of them. Is used to + define the derivative of the hypergeometric function with these parameters. + EXAMPLES:: + + sage: from sage.functions.hypergeometric_algebraic import Parameters + sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: p + ([1/4, 1/3, 1/2], [2/5, 3/5, 1]) + sage: p.shift() + ([5/4, 4/3, 3/2], [7/5, 8/5, 1]) + """ top = [a+1 for a in self.top] bottom = [b+1 for b in self.bottom] return Parameters(top, bottom) @@ -251,7 +285,7 @@ def top(self): return tuple(self._parameters.top) def bottom(self): - return tuple(self._parameters.bottom)[:-1] + return tuple(self._parameters.bottom[:-1]) def denominator(self): return self._parameters.d @@ -273,9 +307,10 @@ def differential_operator(self, var='d'): return D([ c//x for c in L.list() ]) def derivative(self): - parameters = self._parameters.shift() - scalar = self._base(self._parameters.scalar()) * self._scalar - return HypergeometricAlgebraic(parameters, scalar=scalar) + parameters = Parameters(self.top(), self.bottom(), add_one=False).shift() + scalar = self.base_ring()(self._parameters.scalar()) * self._scalar + H = HypergeometricFunctions(self.base_ring(), self.parent().variable_name()) + return H(parameters, scalar=scalar) #in the output the tuple of bottom parameters has an extra comma, class HypergeometricAlgebraic_charzero(HypergeometricAlgebraic): From 067e045e58c024a5506b9c5fa80305992fbe774b Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Mon, 27 Oct 2025 21:23:36 +0100 Subject: [PATCH 08/93] coercion to SR --- src/sage/functions/hypergeometric.py | 6 +- .../functions/hypergeometric_algebraic.py | 198 +++++++++++------- 2 files changed, 128 insertions(+), 76 deletions(-) diff --git a/src/sage/functions/hypergeometric.py b/src/sage/functions/hypergeometric.py index 1e933917885..af9fba41e69 100644 --- a/src/sage/functions/hypergeometric.py +++ b/src/sage/functions/hypergeometric.py @@ -756,13 +756,11 @@ def hypergeometric(a, b, x): from sage.rings.polynomial.polynomial_element import Polynomial from sage.rings.power_series_ring_element import PowerSeries if isinstance(x, (Polynomial, PowerSeries)): - from sage.functions.hypergeometric_algebraic import Parameters, HypergeometricFunctions if not x.is_gen(): raise NotImplementedError("the argument must be the generator of the polynomial ring") S = x.parent() - H = HypergeometricFunctions(S.base_ring(), S.variable_name()) - parameters = Parameters(a, b) - return H(parameters) + from sage.functions.hypergeometric_algebraic import HypergeometricFunctions + return HypergeometricFunctions(S.base_ring(), S.variable_name())(a, b) else: return _hypergeometric(a, b, x) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index ed6413ddfd6..1c6d1d814ac 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -19,12 +19,15 @@ # https://www.gnu.org/licenses/ # *************************************************************************** +import operator + from sage.structure.unique_representation import UniqueRepresentation from sage.structure.parent import Parent from sage.structure.element import Element from sage.structure.sequence import Sequence from sage.structure.category_object import normalize_names +from sage.categories.action import Action from sage.categories.pushout import pushout from sage.categories.map import Map from sage.categories.finite_fields import FiniteFields @@ -127,6 +130,10 @@ def __repr__(self): """ return "(%s, %s)" % (self.top, self.bottom) + def __eq__(self, other): + return (isinstance(other, Parameters) + and self.top == other.top and self.bottom == other.bottom) + def is_balanced(self): r""" Return ``True`` if there are as many top parameters as bottom @@ -199,9 +206,10 @@ def remove_positive_integer_differences(self): def has_negative_integer_differences(self): r""" - Returns ``True`` if there exists a pair of a top parameter and a bottom - parameter, such that the top one minus the bottom one is a negative integer. - Returns ``False`` otherwise. + Return ``True`` if there exists a pair of a top parameter and a bottom + parameter, such that the top one minus the bottom one is a negative integer; + return ``False`` otherwise. + EXAMPLES:: sage: from sage.functions.hypergeometric_algebraic import Parameters @@ -213,7 +221,6 @@ def has_negative_integer_differences(self): :: - sage: from sage.functions.hypergeometric_algebraic import Parameters sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/2]) sage: p ([1/4, 1/3, 1/2], [2/5, 3/2, 1]) @@ -222,45 +229,12 @@ def has_negative_integer_differences(self): """ return any(a - b in ZZ and a < b for a in self.top for b in self.bottom) - def shift(self): - r""" - Return the parameters obtained by adding one to each of them. Is used to - define the derivative of the hypergeometric function with these parameters. - EXAMPLES:: - - sage: from sage.functions.hypergeometric_algebraic import Parameters - sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) - sage: p - ([1/4, 1/3, 1/2], [2/5, 3/5, 1]) - sage: p.shift() - ([5/4, 4/3, 3/2], [7/5, 8/5, 1]) - """ - top = [a+1 for a in self.top] - bottom = [b+1 for b in self.bottom] - return Parameters(top, bottom) - - def scalar(self): - r""" - Return the scalar of the derivative of the hypergeometric function with - this set of parameters. - - EXAMPLES:: - - sage: from sage.functions.hypergeometric_algebraic import Parameters - sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) - sage: p - ([1/4, 1/3, 1/2], [2/5, 3/5, 1]) - sage: p.scalar() - 25/144 - """ - return prod(self.top) / prod(self.bottom) - # Hypergeometric functions ########################## class HypergeometricAlgebraic(Element): - def __init__(self, parent, parameters, scalar=None): + def __init__(self, parent, arg1, arg2=None, scalar=None): Element.__init__(self, parent) base = parent.base_ring() if scalar is None: @@ -269,15 +243,33 @@ def __init__(self, parent, parameters, scalar=None): self._scalar = base(scalar) if self._scalar == 0: self._parameters = None + elif isinstance(arg1, HypergeometricAlgebraic): + self._parameters = arg1._parameters + self._scalar *= base(arg1._scalar) + elif isinstance(arg1, Parameters): + self._parameters = arg1 else: - self._parameters = parameters + self._parameters = Parameters(arg1, arg2) def _repr_(self): + if self._parameters is None: + return "0" s = "hypergeometric(%s, %s, %s)" % (self.top(), self.bottom(), self.parent().variable_name()) - if self._scalar != 1: - s = str(self._scalar) + "*" + s + scalar = self._scalar + if scalar == 1: + pass + elif scalar._is_atomic(): + scalar = str(scalar) + if scalar == "-1": + s = "-" + s + else: + s = scalar + "*" + s + else: + s = "(%s)*%s" % (scalar, s) return s + # def _latex_(self): + def base_ring(self): return self.parent().base_ring() @@ -287,6 +279,47 @@ def top(self): def bottom(self): return tuple(self._parameters.bottom[:-1]) + def scalar(self): + return self._scalar + + def _add_(self, other): + if self._parameters is None: + return other + if isinstance(other, HypergeometricAlgebraic): + if other._parameters is None: + return self + if self._parameters == other._parameters: + scalar = self._scalar + other._scalar + return self.parent()(self._parameters, scalar=scalar) + return SR(self) + SR(other) + + def _neg_(self): + if self._parameters is None: + return self + return self.parent()(self._parameters, scalar=-self._scalar) + + def _sub_(self, other): + if self._parameters is None: + return other + if isinstance(other, HypergeometricAlgebraic): + if other._parameters is None: + return self + if self._parameters == other._parameters: + scalar = self._scalar - other._scalar + return self.parent()(self._parameters, scalar=scalar) + return SR(self) + SR(other) + + def _mul_(self, other): + return SR(self) * SR(other) + + def _lmul_(self, scalar): + print("lmul") + def _rmul_(self, scalar): + print("rmul") + + def _div_(self, other): + return SR(self) / SR(other) + def denominator(self): return self._parameters.d @@ -298,19 +331,20 @@ def differential_operator(self, var='d'): return D.one() t = x * D.gen() A = D.one() - for a in self.top(): + for a in self._parameters.top: A *= t + S(a) B = D.one() - for b in self.bottom(): + for b in self._parameters._bottom: B *= t + S(b-1) - L = t*B - x*A + L = B - x*A return D([ c//x for c in L.list() ]) def derivative(self): - parameters = Parameters(self.top(), self.bottom(), add_one=False).shift() - scalar = self.base_ring()(self._parameters.scalar()) * self._scalar - H = HypergeometricFunctions(self.base_ring(), self.parent().variable_name()) - return H(parameters, scalar=scalar) #in the output the tuple of bottom parameters has an extra comma, + top = [a+1 for a in self.top()] + bottom = [b+1 for b in self.bottom()] + scalar = prod(self._parameters.top) / prod(self._parameters.bottom) + scalar = self.base_ring()(scalar) * self._scalar + return self.parent()(top, bottom, scalar) class HypergeometricAlgebraic_charzero(HypergeometricAlgebraic): @@ -318,15 +352,14 @@ def is_defined(self): return not any(b in ZZ and b < 0 for b in self._bottom) def series(self, prec): - S = PowerSeriesRing(self._base, name=self._variable_name) + S = self.parent().power_series_ring(prec) c = self._scalar coeffs = [c] for i in range(prec): - for a in self.top(): + for a in self._parameters.top: c *= a + i - for b in self.bottom(): + for b in self._parameters.bottom: c /= b + i - c /= i + 1 coeffs.append(c) return S(coeffs, prec=prec) @@ -334,7 +367,7 @@ def series(self, prec): class HypergeometricAlgebraic_QQ(HypergeometricAlgebraic_charzero): def mod(self, p): H = HypergeometricFunctions(FiniteField(p), self.parent().variable_name()) - return H(self._parameters, self._scalar) + return H(self._parameters, None, self._scalar) __mod__ = mod @@ -366,8 +399,8 @@ def is_globally_bounded(self, include_infinity=True): class HypergeometricAlgebraic_GFp(HypergeometricAlgebraic): - def __init__(self, parent, parameters, scalar=None): - HypergeometricAlgebraic.__init__(self, parent, parameters, scalar) + def __init__(self, parent, arg1, arg2=None, scalar=None): + HypergeometricAlgebraic.__init__(self, parent, arg1, arg2, scalar) self._p = self.base_ring().cardinality() def is_defined(self): @@ -394,18 +427,17 @@ def is_defined(self): return True def series(self, prec): - S = self.parent().polynomial_ring() + S = self.parent().power_series_ring(prec) p = self._p pprec = max(1, (len(self.bottom()) + 1) * ceil(log(prec, p))) K = QpFP(p, pprec) c = K(self._scalar) coeffs = [c] for i in range(prec-1): - for a in self.top(): + for a in self._parameters.top: c *= a + i - for b in self.bottom(): + for b in self._parameters.bottom: c /= b + i - c /= i + 1 if c.valuation() < 0: raise ValueError("denominator appears in the series at the required precision") coeffs.append(c) @@ -439,23 +471,26 @@ def dwork_relation(self): if not self.is_defined(): raise ValueError("this hypergeometric function is not defined") - # We compute the series expansion up to x^p + H = self.parent() p = self._p - S = self.parent().polynomial_ring() + + # We compute the series expansion up to x^p cs = self.series(p).list() - # We compute the relevant exponents + # We compute the relevant exponents and associated coefficients + S = self.parent().polynomial_ring() exponents = sorted([(1-b) % p for b in self._parameters.bottom]) exponents.append(p) Ps = [] for i in range(len(exponents) - 1): - e = exponents[i] - if e < len(cs) and cs[e]: - P = S(cs[e:exponents[i+1]]) << e - Ps.append((e, P)) + ei = exponents[i] + ej = exponents[i+1] + P = S(cs[ei:ej]) + if P: + Ps.append((ei, P << ei)) # We compute the hypergeometric series - Hs = [ ] + pairs = [ ] for r, P in Ps: top = [ ] for a in self.top(): @@ -473,10 +508,9 @@ def dwork_relation(self): bottom.append(bp + 1) else: bottom.append(bp) - parameters = Parameters(top, bottom) - Hs.append((P, HypergeometricAlgebraic_GFp(parameters, self._x))) + pairs.append((P, H(top, bottom))) - return Hs + return pairs # Parent @@ -485,7 +519,12 @@ def dwork_relation(self): class HypergeometricToSR(Map): def _call_(self, h): from sage.functions.hypergeometric import _hypergeometric - return _hypergeometric(h.top(), h.bottom(), SR.var(h.parent().variable_name())) + return h.scalar() * _hypergeometric(h.top(), h.bottom(), SR.var(h.parent().variable_name())) + +class ScalarMultiplication(Action): + def _act_(self, scalar, h): + return h.parent()(h, scalar=scalar) + class HypergeometricFunctions(Parent, UniqueRepresentation): def __init__(self, base, name, category=None): @@ -502,6 +541,8 @@ def __init__(self, base, name, category=None): else: raise NotImplementedError("hypergeometric functions are only implemented over finite field and bases of characteristic zero") Parent.__init__(self, base, category=category) + self.register_action(ScalarMultiplication(base, self, False, operator.mul)) + self.register_action(ScalarMultiplication(base, self, True, operator.mul)) if char == 0: SR.register_coercion(HypergeometricToSR(self.Hom(SR))) @@ -511,6 +552,19 @@ def _repr_(self): def _element_constructor_(self, *args, **kwds): return self.element_class(self, *args, **kwds) + def _coerce_map_from_(self, other): + if (isinstance(other, HypergeometricFunctions) + and other.has_coerce_map_from(self)): + return True + + def _pushout_(self, other): + if isinstance(other, HypergeometricFunctions) and self._name == other._name: + base = pushout(self.base_ring(), other.base_ring()) + if base is not None: + return HypergeometricFunctions(base, self._name) + if SR.has_coerce_map_from(other): + return SR + def base_ring(self): return self._base @@ -521,4 +575,4 @@ def polynomial_ring(self): return PolynomialRing(self.base_ring(), self._name) def power_series_ring(self, default_prec=None): - return PowerSeriesRing(self.base_ring(), self._name, default_prec=prec) + return PowerSeriesRing(self.base_ring(), self._name, default_prec=default_prec) From 93a098d74583d09ac2c955d51e0401841d7ff0fe Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Tue, 28 Oct 2025 06:59:02 +0100 Subject: [PATCH 09/93] latex --- .../functions/hypergeometric_algebraic.py | 59 ++++++++++++++----- 1 file changed, 44 insertions(+), 15 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 1c6d1d814ac..205680fa3ea 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -21,6 +21,16 @@ import operator +from sage.misc.latex import latex +from sage.misc.latex import latex_variable_name +from sage.misc.cachefunc import cached_method + +from sage.misc.misc_c import prod +from sage.misc.functional import log +from sage.functions.other import ceil +from sage.arith.functions import lcm +from sage.matrix.constructor import matrix + from sage.structure.unique_representation import UniqueRepresentation from sage.structure.parent import Parent from sage.structure.element import Element @@ -42,13 +52,6 @@ from sage.rings.power_series_ring import PowerSeriesRing from sage.rings.polynomial.ore_polynomial_ring import OrePolynomialRing -from sage.misc.misc_c import prod -from sage.misc.functional import log -from sage.functions.other import ceil -from sage.arith.functions import lcm -from sage.misc.cachefunc import cached_method - -from sage.matrix.constructor import matrix # Parameters of hypergeometric functions ######################################## @@ -254,21 +257,43 @@ def __init__(self, parent, arg1, arg2=None, scalar=None): def _repr_(self): if self._parameters is None: return "0" - s = "hypergeometric(%s, %s, %s)" % (self.top(), self.bottom(), self.parent().variable_name()) scalar = self._scalar if scalar == 1: - pass + s = "" elif scalar._is_atomic(): scalar = str(scalar) if scalar == "-1": - s = "-" + s + s = "-" else: - s = scalar + "*" + s + s = scalar + "*" else: - s = "(%s)*%s" % (scalar, s) + s = "(%s)*" % scalar + s += "hypergeometric(%s, %s, %s)" % (self.top(), self.bottom(), self.parent().variable_name()) return s - # def _latex_(self): + def _latex_(self): + if self._parameters is None: + return "0" + scalar = self._scalar + if scalar == 1: + s = "" + elif scalar._is_atomic(): + scalar = latex(scalar) + if scalar == "-1": + s = "-" + else: + s = scalar + else: + s = r"\left(%s\right)" % scalar + top = self.top() + bottom = self.bottom() + s += r"\,_{%s} F_{%s} " % (len(top), len(bottom)) + s += r"\left(\begin{matrix} " + s += ",".join(latex(a) for a in top) + s += r"\\" + s += ",".join(latex(b) for b in bottom) + s += r"\end{matrix}; %s \right)" % self.parent().latex_variable_name() + return s def base_ring(self): return self.parent().base_ring() @@ -352,7 +377,7 @@ def is_defined(self): return not any(b in ZZ and b < 0 for b in self._bottom) def series(self, prec): - S = self.parent().power_series_ring(prec) + S = self.parent().power_series_ring() c = self._scalar coeffs = [c] for i in range(prec): @@ -427,7 +452,7 @@ def is_defined(self): return True def series(self, prec): - S = self.parent().power_series_ring(prec) + S = self.parent().power_series_ring() p = self._p pprec = max(1, (len(self.bottom()) + 1) * ceil(log(prec, p))) K = QpFP(p, pprec) @@ -529,6 +554,7 @@ def _act_(self, scalar, h): class HypergeometricFunctions(Parent, UniqueRepresentation): def __init__(self, base, name, category=None): self._name = normalize_names(1, name)[0] + self._latex_name = latex_variable_name(self._name) char = base.characteristic() if base in FiniteFields() and base.is_prime_field(): self.Element = HypergeometricAlgebraic_GFp @@ -571,6 +597,9 @@ def base_ring(self): def variable_name(self): return self._name + def latex_variable_name(self): + return self._latex_name + def polynomial_ring(self): return PolynomialRing(self.base_ring(), self._name) From af4d4537b9489a70ea824b3f32981ad825696d32 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Tue, 28 Oct 2025 16:25:29 +0100 Subject: [PATCH 10/93] annihilating polynomial --- .../functions/hypergeometric_algebraic.py | 105 +++++++++++++++++- 1 file changed, 101 insertions(+), 4 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 205680fa3ea..7feed42911d 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -110,8 +110,16 @@ def __init__(self, top, bottom, add_one=True): i += 1 if add_one: bottom.append(QQ(1)) - self.top = top - self.bottom = bottom + else: + i = bottom.index(QQ(1)) + bottom.append(QQ(1)) + if i < 0: + top.append(QQ(1)) + top.sort() + else: + del bottom[i] + self.top = tuple(top) + self.bottom = tuple(bottom) if len(top) == 0 and len(bottom) == 0: self.d = 1 self.bound = 1 @@ -133,6 +141,9 @@ def __repr__(self): """ return "(%s, %s)" % (self.top, self.bottom) + def __hash__(self): + return hash((self.top, self.bottom)) + def __eq__(self, other): return (isinstance(other, Parameters) and self.top == other.top and self.bottom == other.bottom) @@ -232,6 +243,28 @@ def has_negative_integer_differences(self): """ return any(a - b in ZZ and a < b for a in self.top for b in self.bottom) + def dwork_image(self, p): + try: + top = [(a + (-a) % p) / p for a in self.top] + bottom = [(b + (-b) % p) / p for b in self.bottom] + except ZeroDivisionError: + raise ValueError("denominators of parameters are not coprime to p") + return Parameters(top, bottom, add_one=False) + + def decimal_part(self): + top = [1 + a - ceil(a) for a in self.top] + bottom = [1 + b - ceil(b) for b in self.bottom] + return Parameters(top, bottom, add_one=False) + + def frobenius_order(self, p): + param = self.decimal_part() + iter = param.dwork_image(p) + i = 1 + while param != iter: + iter = iter.dwork_image(p) + i += 1 + return i + # Hypergeometric functions ########################## @@ -254,6 +287,15 @@ def __init__(self, parent, arg1, arg2=None, scalar=None): else: self._parameters = Parameters(arg1, arg2) + def __hash__(self): + return hash((self.base_ring(), self._parameters, self._scalar)) + + def __eq__(self, other): + return (isinstance(other, HypergeometricAlgebraic) + and self.base_ring() is other.base_ring() + and self._parameters == other._parameters + and self._scalar == other._scalar) + def _repr_(self): if self._parameters is None: return "0" @@ -299,10 +341,10 @@ def base_ring(self): return self.parent().base_ring() def top(self): - return tuple(self._parameters.top) + return self._parameters.top def bottom(self): - return tuple(self._parameters.bottom[:-1]) + return self._parameters.bottom[:-1] def scalar(self): return self._scalar @@ -537,6 +579,61 @@ def dwork_relation(self): return pairs + def annihilating_ore_polynomial(self, var='Frob'): + # We remove the scalar + self = self.parent()(self._parameters) + + K = self.base_ring() + zero = K.zero() + S = self.parent().polynomial_ring() + Frob = S.frobenius_endomorphism() + Ore = OrePolynomialRing(S, Frob, names=var) + + p = self._p + order = self._parameters.frobenius_order(p) + + columns = {self: None} + rows = [{self: K.one()}] + # If row is the i-th item of rows, we have: + # self = sum_g row[g] * g**(p**i) + q = 1 + while True: + row = {} + previous_row = rows[-1] + for _ in range(order): + row = {} + for g, P in previous_row.items(): + for Q, h in g.dwork_relation(): + # here g = sum(Q * h^p) + if h in row: + row[h] += P * Q**q + else: + row[h] = P * Q**q + previous_row = row + q *= p # q = p**i + for h in row: + if h not in columns: + columns[h] = None + rows.append(row) + + i = len(rows) + Mrows = [] + Mqo = 1 + for j in range(i-1, -1, -1): + Mrow = [] + for col in columns: + Mrow.append(rows[j].get(col, zero) ** Mqo) + Mrows.append(Mrow) + Mqo *= p ** order + M = matrix(Mrows) + + ker = M.minimal_kernel_basis() + if ker.nrows() > 0: + cs = ker.row(0).list() + coeffs = order * len(cs) * [S.zero()] + for i in range(len(cs)): + coeffs[order*i] = cs[i] + return Ore(coeffs) # Parent ######## From 08b6e3b9f145100029190727da5f2405350ac46e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20F=C3=BCrnsinn?= Date: Wed, 29 Oct 2025 09:45:39 +0100 Subject: [PATCH 11/93] Documentationx --- .../functions/hypergeometric_algebraic.py | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 205680fa3ea..0b37bd40ba1 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -184,6 +184,40 @@ def parenthesis_criterion(self, c): return parenthesis >= 0 def interlacing_criterion(self, c): + r""" + Return ``True`` if the sorted lists of the decimal parts (where integers + are assigned 1 instead of 0) of c*a and c*b for a in the top parameters + and b in the bottom parameters interlace, i.e., the entries in the sorted + union of the two lists alternate between entries from the first and from + the second list. Used to determine algebraicity of the hypergeometric + function with these parameters with the Beukers-Heckman criterion. + + INPUT: + + - ``c`` -- an integer between 1 and ``self.d``, coprime to ``d``. + + EXAMPLES:: + + sage: from sage.functions.hypergeometric_algebraic import Parameters + sage: p = Parameters([1/3, 2/3], [1/2]) + sage: p + ([1/3, 2/3], [1/2, 1]) + sage: p.interlacing_criterion(1) + True + sage: p.interlacing_criterion(5) + True + + :: + + sage: from sage.functions.hypergeometric_algebraic import Parameters + sage: p = Parameters([1/8, 3/8, 5/8], [1/4, 1/2]) + sage: p + ([1/8, 3/8, 5/8], [1/4, 1/2, 1]) + sage: p.interlacing_criterion(1) + True + sage: p.interlacing_criterion(3) + False + """ AB = self.christol_sorting(c) previous_paren = -1 for _, _, paren in AB: @@ -193,6 +227,29 @@ def interlacing_criterion(self, c): return True def remove_positive_integer_differences(self): + r""" + Return parameters, where pairs consisting of a top parameter + and a bottom parameter with positive integer differences are + removed, starting with pairs of minimal positive integer + difference. + EXAMPLES:: + + sage: from sage.functions.hypergeometric_algebraic import Parameters + sage: p = Parameters([5/2, -1/2, 5/3], [3/2, 1/3]) + sage: p + ([-1/2, 5/3, 5/2], [1/3, 3/2, 1]) + sage: p.remove_positive_integer_differences() + ([-1/2, 5/3], [1/3, 1]) + + The choice of which pair with integer differences to remove first + is important:: + + sage: p = Parameters([4, 2, 1/2], [1, 3]) + sage: p + ([1/2, 2, 4], [1, 3, 1]) + sage: p.remove_positive_integer_differences() + ([1/2], [1]) + """ differences = [] top = self.top[:] bottom = self.bottom[:] From c892e02aeefd768f176cc46c655ea9388d102715 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Wed, 29 Oct 2025 09:49:57 +0100 Subject: [PATCH 12/93] faster algorithm for computing kernel --- .../functions/hypergeometric_algebraic.py | 116 +++++++++++++----- 1 file changed, 83 insertions(+), 33 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 7feed42911d..6face1bfff0 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -28,6 +28,7 @@ from sage.misc.misc_c import prod from sage.misc.functional import log from sage.functions.other import ceil +from sage.arith.misc import gcd from sage.arith.functions import lcm from sage.matrix.constructor import matrix @@ -43,6 +44,7 @@ from sage.categories.finite_fields import FiniteFields from sage.symbolic.ring import SR +from sage.combinat.subset import Subsets from sage.rings.integer_ring import ZZ from sage.rings.rational_field import QQ from sage.rings.finite_rings.finite_field_constructor import FiniteField @@ -53,6 +55,39 @@ from sage.rings.polynomial.ore_polynomial_ring import OrePolynomialRing +def insert_zeroes(P, n): + cs = P.list() + coeffs = n * len(cs) * [0] + for i in range(len(cs)): + coeffs[n*i] = cs[i] + return P.parent()(coeffs) + +def kernel(M, repeat=2): + n = M.nrows() + m = M.ncols() + if n > m + 1: + raise RuntimeError + if n <= m: + K = M.base_ring().base_ring() + for _ in range(repeat): + a = K.random_element() + Me = matrix(n, m, [f(a) for f in M.list()]) + if Me.rank() == n: + return + for J in Subsets(range(m), n-1): + MJ = M.matrix_from_columns(J) + minor = MJ.delete_rows([0]).determinant() + if minor.is_zero(): + continue + ker = [minor] + for i in range(1, n): + minor = MJ.delete_rows([i]).determinant() + ker.append((-1)**i * minor) + g = gcd(ker) + ker = [c//g for c in ker] + return ker + + # Parameters of hypergeometric functions ######################################## @@ -194,6 +229,20 @@ def parenthesis_criterion(self, c): previous_paren = paren return parenthesis >= 0 + def p_interlacing_number(self, p): + d = self.d + A = [(1/2 + (-a) % p, 1) for a in self.top] + B = [(1 + (-b) % p, -1) for b in self.bottom] + AB = sorted(A + B) + s = "" + interlacing = 0 + previous_paren = -1 + for _, paren in AB: + if paren == -1 and previous_paren == 1: + interlacing += 1 + previous_paren = paren + return interlacing + def interlacing_criterion(self, c): AB = self.christol_sorting(c) previous_paren = -1 @@ -205,8 +254,8 @@ def interlacing_criterion(self, c): def remove_positive_integer_differences(self): differences = [] - top = self.top[:] - bottom = self.bottom[:] + top = list(self.top) + bottom = list(self.bottom) for i in range(len(top)): for j in range(len(bottom)): diff = top[i] - bottom[j] @@ -379,11 +428,6 @@ def _sub_(self, other): def _mul_(self, other): return SR(self) * SR(other) - def _lmul_(self, scalar): - print("lmul") - def _rmul_(self, scalar): - print("rmul") - def _div_(self, other): return SR(self) / SR(other) @@ -391,7 +435,7 @@ def denominator(self): return self._parameters.d def differential_operator(self, var='d'): - S = PolynomialRing(self._base, self._variable_name) + S = self.parent().polynomial_ring() x = S.gen() D = OrePolynomialRing(S, S.derivation(), names=var) if self._scalar == 0: @@ -401,7 +445,7 @@ def differential_operator(self, var='d'): for a in self._parameters.top: A *= t + S(a) B = D.one() - for b in self._parameters._bottom: + for b in self._parameters.bottom: B *= t + S(b-1) L = B - x*A return D([ c//x for c in L.list() ]) @@ -527,11 +571,14 @@ def p_curvature(self): rows.append([Li[j] for j in range(n)]) return matrix(rows) + def p_curvature_corank(self): + return self._parameters.p_interlacing_number(self._p) + def dwork_relation(self): r""" - Return (P1, g1), ..., (Ps, gs) such that + Return (P1, h1), ..., (Ps, hs) such that - h = P1*g1^p + ... + Ps*gs^p + self = P1*h1^p + ... + Ps*hs^p """ if not self._parameters.is_balanced(): raise ValueError("the hypergeometric function must be a pFq with q = p-1") @@ -580,20 +627,25 @@ def dwork_relation(self): return pairs def annihilating_ore_polynomial(self, var='Frob'): - # We remove the scalar - self = self.parent()(self._parameters) + if not self._parameters.is_balanced(): + raise NotImplementedError("the hypergeometric function is not a pFq with q = p-1") K = self.base_ring() - zero = K.zero() + p = self._p S = self.parent().polynomial_ring() + zero = S.zero() Frob = S.frobenius_endomorphism() Ore = OrePolynomialRing(S, Frob, names=var) - p = self._p + # We remove the scalar + if self._scalar == 0: + return Ore.one() + self = self.parent()(self._parameters) + order = self._parameters.frobenius_order(p) + bound = self.p_curvature_corank() - columns = {self: None} - rows = [{self: K.one()}] + rows = [{self: S.one()}] # If row is the i-th item of rows, we have: # self = sum_g row[g] * g**(p**i) q = 1 @@ -606,34 +658,32 @@ def annihilating_ore_polynomial(self, var='Frob'): for Q, h in g.dwork_relation(): # here g = sum(Q * h^p) if h in row: - row[h] += P * Q**q + row[h] += P * insert_zeroes(Q, q) else: - row[h] = P * Q**q + row[h] = P * insert_zeroes(Q, q) previous_row = row q *= p # q = p**i - for h in row: - if h not in columns: - columns[h] = None rows.append(row) i = len(rows) Mrows = [] Mqo = 1 - for j in range(i-1, -1, -1): + columns = {} + for j in range(i-1, max(-1, i-2-bound), -1): + for col in rows[j]: + columns[col] = None + for j in range(i-1, max(-1, i-2-bound), -1): Mrow = [] for col in columns: - Mrow.append(rows[j].get(col, zero) ** Mqo) + Mrow.append(insert_zeroes(rows[j].get(col, zero), Mqo)) Mrows.append(Mrow) Mqo *= p ** order - M = matrix(Mrows) - - ker = M.minimal_kernel_basis() - if ker.nrows() > 0: - cs = ker.row(0).list() - coeffs = order * len(cs) * [S.zero()] - for i in range(len(cs)): - coeffs[order*i] = cs[i] - return Ore(coeffs) + M = matrix(S, Mrows) + + ker = kernel(M) + if ker is not None: + return insert_zeroes(Ore(ker), order) + # Parent ######## From 72741d8e4647ad689d2cda1fa00675d23b067deb Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Wed, 29 Oct 2025 09:59:45 +0100 Subject: [PATCH 13/93] normalize annihilating polynomial --- src/sage/functions/hypergeometric_algebraic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 6face1bfff0..a2ee349f02e 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -83,7 +83,7 @@ def kernel(M, repeat=2): for i in range(1, n): minor = MJ.delete_rows([i]).determinant() ker.append((-1)**i * minor) - g = gcd(ker) + g = ker[0].leading_coefficient() * gcd(ker) ker = [c//g for c in ker] return ker From f292abb4825558587dbfc1a350cc57d72609e74c Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Wed, 29 Oct 2025 10:02:27 +0100 Subject: [PATCH 14/93] tutorial coming soon --- src/sage/functions/hypergeometric_algebraic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index a2ee349f02e..fdc0a591722 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -1,7 +1,7 @@ r""" Algebraic properties of hypergeometric functions. -[Tutorial] +[Tutorial... coming soon] AUTHORS: From ec446c63a436f4c668cb39bebd31066c4844c988 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20F=C3=BCrnsinn?= Date: Wed, 29 Oct 2025 10:02:34 +0100 Subject: [PATCH 15/93] Tutorial here --- src/sage/functions/hypergeometric_algebraic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 680a198324a..c393cb43288 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -1,7 +1,7 @@ r""" Algebraic properties of hypergeometric functions. -[Tutorial] +[Tutorial here] AUTHORS: From f5b81b2795c5388d4077de478eceb91bfcdbf043 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Wed, 29 Oct 2025 11:30:54 +0100 Subject: [PATCH 16/93] conjectural (and faster) version of is_defined in positive characteristic --- .../functions/hypergeometric_algebraic.py | 49 +++++++++++++++---- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 680a198324a..a2310921781 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -219,25 +219,35 @@ def mod2(x): return sorted(A + B) def parenthesis_criterion(self, c): - AB = self.christol_sorting(c) parenthesis = 0 previous_paren = -1 - for _, _, paren in AB: + for _, _, paren in self.christol_sorting(c): parenthesis += paren if parenthesis < 0: return False previous_paren = paren return parenthesis >= 0 - def p_interlacing_number(self, p): + def q_christol_sorting(self, q): d = self.d - A = [(1/2 + (-a) % p, 1) for a in self.top] - B = [(1 + (-b) % p, -1) for b in self.bottom] - AB = sorted(A + B) - s = "" + A = [(1/2 + (-a) % q, 1) for a in self.top] + B = [(1 + (-b) % q, -1) for b in self.bottom] + return sorted(A + B) + + def q_parenthesis_criterion(self, q): + parenthesis = 0 + previous_paren = -1 + for _, _, paren in self.q_christol_sorting(q): + parenthesis += paren + if parenthesis < 0: + return False + previous_paren = paren + return parenthesis >= 0 + + def q_interlacing_number(self, q): interlacing = 0 previous_paren = -1 - for _, paren in AB: + for _, paren in self.q_christol_sorting(q): if paren == -1 and previous_paren == 1: interlacing += 1 previous_paren = paren @@ -594,6 +604,27 @@ def is_defined(self): return False return True + def is_defined_conjectural(self): + p = self._p + d = self.denominator() + if d.gcd(p) > 1: + return False + u = 1 + if not self._parameters.parenthesis_criterion(u): + return False + u = p % d + while u != 1: + if not self._parameters.parenthesis_criterion(u): + return False + u = p*u % d + bound = self._parameters.bound + q = p + while q <= bound: + if not self._parameters.q_parenthesis_criterion(q): + return False + q *= p + return True + def series(self, prec): S = self.parent().power_series_ring() p = self._p @@ -629,7 +660,7 @@ def p_curvature(self): return matrix(rows) def p_curvature_corank(self): - return self._parameters.p_interlacing_number(self._p) + return self._parameters.q_interlacing_number(self._p) def dwork_relation(self): r""" From d12d93922fddc56ceab4f292e5ce9be7741acda7 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Wed, 29 Oct 2025 12:17:48 +0100 Subject: [PATCH 17/93] good reduction primes --- .../functions/hypergeometric_algebraic.py | 61 ++++++++++++++++++- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index a2310921781..0c5da8c2a9d 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -42,6 +42,7 @@ from sage.categories.pushout import pushout from sage.categories.map import Map from sage.categories.finite_fields import FiniteFields +from sage.sets.primes import Primes from sage.symbolic.ring import SR from sage.combinat.subset import Subsets @@ -237,7 +238,7 @@ def q_christol_sorting(self, q): def q_parenthesis_criterion(self, q): parenthesis = 0 previous_paren = -1 - for _, _, paren in self.q_christol_sorting(q): + for _, paren in self.q_christol_sorting(q): parenthesis += paren if parenthesis < 0: return False @@ -515,7 +516,7 @@ def differential_operator(self, var='d'): for b in self._parameters.bottom: B *= t + S(b-1) L = B - x*A - return D([ c//x for c in L.list() ]) + return D([c//x for c in L.list()]) def derivative(self): top = [a+1 for a in self.top()] @@ -553,6 +554,62 @@ def has_good_reduction(self, p): h = self.reduce(p) return h.is_defined() + def good_reduction_primes(self): + r""" + Return + + (modulus, congruence_classes, exceptionnal_primes) + + ALGORITHM: + + We rely on Christol's criterion ([CF2025]_) + """ + params = self._parameters + d = params.d + + # We check the parenthesis criterion for c=1 + if not params.parenthesis_criterion(1): + return d, [], [] + + # We check the parenthesis criterion for other c + # and derive congruence classes with good reduction + goods = {c: None for c in range(d) if d.gcd(c) == 1} + goods[1] = True + for c in goods: + if goods[c] is not None: + continue + cc = c + goods[c] = True + while cc != 1: + if goods[cc] is False or not params.parenthesis_criterion(cc): + goods[c] = False + break + cc = (cc * c) % d + if goods[c]: + cc = c + while cc != 1: + goods[cc] = True + cc = (cc * c) % d + + # We treat exceptionnal primes + bound = params.bound + exceptions = [] + for p in Primes(): + if p > bound: + break + if d % p == 0 or not goods[p % d]: + continue + q = p + while q <= bound: + if not self._parameters.q_parenthesis_criterion(q): + exceptions.append(p) + break + q *= p + + goods = [c for c, v in goods.items() if v] + return d, goods, exceptions + + def is_algebraic(self): if any(a in ZZ and a <= 0 for a in self.top()): return True From 1fd6112875f934fc44f61d5492a30af49ae23aa3 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Wed, 29 Oct 2025 15:13:22 +0100 Subject: [PATCH 18/93] monodromy + lucas --- .../functions/hypergeometric_algebraic.py | 50 ++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 0c5da8c2a9d..6566c832d32 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -44,12 +44,17 @@ from sage.categories.finite_fields import FiniteFields from sage.sets.primes import Primes +from sage.matrix.special import companion_matrix +from sage.matrix.special import identity_matrix + from sage.symbolic.ring import SR from sage.combinat.subset import Subsets +from sage.rings.infinity import infinity from sage.rings.integer_ring import ZZ from sage.rings.rational_field import QQ from sage.rings.finite_rings.finite_field_constructor import FiniteField from sage.rings.padics.factory import QpFP +from sage.rings.number_field.number_field import CyclotomicField from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing from sage.rings.power_series_ring import PowerSeriesRing @@ -609,7 +614,6 @@ def good_reduction_primes(self): goods = [c for c, v in goods.items() if v] return d, goods, exceptions - def is_algebraic(self): if any(a in ZZ and a <= 0 for a in self.top()): return True @@ -632,6 +636,35 @@ def is_globally_bounded(self, include_infinity=True): return False return True + def monodromy(self, x=0, var='z'): + params = self._parameters + if not params.is_balanced(): + raise ValueError("hypergeometric equation is not Fuchsian") + d = params.d + K = CyclotomicField(d, names=var) + z = K.gen() + S = PolynomialRing(K, names='X') + X = S.gen() + if x == 0: + B = prod(X - z**(b*d) for b in params.bottom) + return companion_matrix(B, format='right').inverse() + elif x == 1: + A = prod(X - z**(a*d) for a in params.top) + B = prod(X - z**(b*d) for b in params.bottom) + return companion_matrix(A, format='right').inverse() * companion_matrix(B, format='right') + elif x is infinity: + A = prod(X - z**(a*d) for a in params.top) + return companion_matrix(A, format='right') + else: + n = len(params.top) + return identity_matrix(QQ, n) + + def is_maximum_unipotent_monodromy(self): + # TODO: check this (maybe ask Daniel) + return all(b in ZZ for b in self.bottom()) + + is_mum = is_maximum_unipotent_monodromy + class HypergeometricAlgebraic_GFp(HypergeometricAlgebraic): def __init__(self, parent, arg1, arg2=None, scalar=None): @@ -772,6 +805,8 @@ def dwork_relation(self): return pairs def annihilating_ore_polynomial(self, var='Frob'): + # QUESTION: does this method actually return the + # minimal Ore polynomial annihilating self? if not self._parameters.is_balanced(): raise NotImplementedError("the hypergeometric function is not a pFq with q = p-1") @@ -829,6 +864,19 @@ def annihilating_ore_polynomial(self, var='Frob'): if ker is not None: return insert_zeroes(Ore(ker), order) + def is_lucas(self): + p = self._p + if self._parameters.frobenius_order(p) > 1: + # TODO: check this + return False + S = self.parent().polynomial_ring() + K = S.fraction_field() + Ore = OrePolynomialRing(K, K.frobenius_endomorphism(), names='F') + Z = Ore(self.annihilating_ore_polynomial()) + Ap = self.series(p).polynomial() + F = Ap * Ore.gen() - 1 + return (Z % F).is_zero() + # Parent ######## From e7676995a7ffab1f38c3913358fad2b65eda12b8 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Wed, 29 Oct 2025 20:32:16 +0100 Subject: [PATCH 19/93] try to fix failing doctests --- src/sage/functions/hypergeometric.py | 32 +++++++-------- .../functions/hypergeometric_algebraic.py | 39 ++++++++++--------- 2 files changed, 34 insertions(+), 37 deletions(-) diff --git a/src/sage/functions/hypergeometric.py b/src/sage/functions/hypergeometric.py index af9fba41e69..b5dc44df6f1 100644 --- a/src/sage/functions/hypergeometric.py +++ b/src/sage/functions/hypergeometric.py @@ -301,10 +301,19 @@ def __call__(self, a, b, z, **kwargs): sage: hypergeometric([2, 3, 4], [4, 1], 1) hypergeometric((2, 3, 4), (4, 1), 1) """ - return BuiltinFunction.__call__(self, - SR._force_pyobject(a), - SR._force_pyobject(b), - z, **kwargs) + from sage.rings.polynomial.polynomial_element import Polynomial + from sage.rings.power_series_ring_element import PowerSeries + if isinstance(z, (Polynomial, PowerSeries)): + if not z.is_gen(): + raise NotImplementedError("the argument must be the generator of the polynomial ring") + S = x.parent() + from sage.functions.hypergeometric_algebraic import HypergeometricFunctions + return HypergeometricFunctions(S.base_ring(), S.variable_name())(a, b) + else: + return BuiltinFunction.__call__(self, + SR._force_pyobject(a), + SR._force_pyobject(b), + z, **kwargs) def _print_latex_(self, a, b, z): r""" @@ -750,20 +759,7 @@ def _deflated(self, a, b, z): return terms return ((1, new),) -_hypergeometric = Hypergeometric() - -def hypergeometric(a, b, x): - from sage.rings.polynomial.polynomial_element import Polynomial - from sage.rings.power_series_ring_element import PowerSeries - if isinstance(x, (Polynomial, PowerSeries)): - if not x.is_gen(): - raise NotImplementedError("the argument must be the generator of the polynomial ring") - S = x.parent() - from sage.functions.hypergeometric_algebraic import HypergeometricFunctions - return HypergeometricFunctions(S.base_ring(), S.variable_name())(a, b) - else: - return _hypergeometric(a, b, x) - +hypergeometric = Hypergeometric() def closed_form(hyp): """ diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 6566c832d32..17980a2fbe0 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -120,7 +120,7 @@ def __init__(self, top, bottom, add_one=True): sage: from sage.functions.hypergeometric_algebraic import Parameters sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) sage: p - ([1/4, 1/3, 1/2], [2/5, 3/5, 1]) + ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) sage: type(p) @@ -128,12 +128,12 @@ def __init__(self, top, bottom, add_one=True): a trailing `1` is added to the bottom parameters:: sage: Parameters([1/2, 1/3, 2/3], [2/3]) - ([1/3, 1/2], [1]) + ((1/3, 1/2), (1,)) We can avoid adding the trailing `1` by passing ``add_one=False``):: sage: Parameters([1/2, 1/3, 2/3], [2/3], add_one=False) - ([1/3, 1/2], []) + ((1/3, 1/2, 1), (1,)) """ try: top = sorted([QQ(a) for a in top if a is not None]) @@ -152,13 +152,14 @@ def __init__(self, top, bottom, add_one=True): if add_one: bottom.append(QQ(1)) else: - i = bottom.index(QQ(1)) - bottom.append(QQ(1)) - if i < 0: + try: + i = bottom.index(QQ(1)) + bottom.append(QQ(1)) + del bottom[i] + except ValueError: + bottom.append(QQ(1)) top.append(QQ(1)) top.sort() - else: - del bottom[i] self.top = tuple(top) self.bottom = tuple(bottom) if len(top) == 0 and len(bottom) == 0: @@ -178,7 +179,7 @@ def __repr__(self): sage: from sage.functions.hypergeometric_algebraic import Parameters sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) sage: p # indirect doctest - ([1/4, 1/3, 1/2], [2/5, 3/5, 1]) + ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) """ return "(%s, %s)" % (self.top, self.bottom) @@ -199,7 +200,7 @@ def is_balanced(self): sage: from sage.functions.hypergeometric_algebraic import Parameters sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) sage: p - ([1/4, 1/3, 1/2], [2/5, 3/5, 1]) + ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) sage: p.is_balanced() True @@ -207,7 +208,7 @@ def is_balanced(self): sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/5], add_one=False) sage: p - ([1/4, 1/3, 1/2], [2/5, 3/5]) + ((1/4, 1/3, 1/2, 1), (2/5, 3/5, 1)) sage: p.is_balanced() False """ @@ -277,7 +278,7 @@ def interlacing_criterion(self, c): sage: from sage.functions.hypergeometric_algebraic import Parameters sage: p = Parameters([1/3, 2/3], [1/2]) sage: p - ([1/3, 2/3], [1/2, 1]) + ((1/3, 2/3), (1/2, 1)) sage: p.interlacing_criterion(1) True sage: p.interlacing_criterion(5) @@ -288,7 +289,7 @@ def interlacing_criterion(self, c): sage: from sage.functions.hypergeometric_algebraic import Parameters sage: p = Parameters([1/8, 3/8, 5/8], [1/4, 1/2]) sage: p - ([1/8, 3/8, 5/8], [1/4, 1/2, 1]) + ((1/8, 3/8, 5/8), (1/4, 1/2, 1)) sage: p.interlacing_criterion(1) True sage: p.interlacing_criterion(3) @@ -313,18 +314,18 @@ def remove_positive_integer_differences(self): sage: from sage.functions.hypergeometric_algebraic import Parameters sage: p = Parameters([5/2, -1/2, 5/3], [3/2, 1/3]) sage: p - ([-1/2, 5/3, 5/2], [1/3, 3/2, 1]) + ((-1/2, 5/3, 5/2), (1/3, 3/2, 1)) sage: p.remove_positive_integer_differences() - ([-1/2, 5/3], [1/3, 1]) + ((-1/2, 5/3), (1/3, 1)) The choice of which pair with integer differences to remove first is important:: sage: p = Parameters([4, 2, 1/2], [1, 3]) sage: p - ([1/2, 2, 4], [1, 3, 1]) + ((1/2, 2, 4), (1, 3, 1)) sage: p.remove_positive_integer_differences() - ([1/2], [1]) + ((1/2,), (1,)) """ differences = [] top = list(self.top) @@ -351,7 +352,7 @@ def has_negative_integer_differences(self): sage: from sage.functions.hypergeometric_algebraic import Parameters sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) sage: p - ([1/4, 1/3, 1/2], [2/5, 3/5, 1]) + ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) sage: p.has_negative_integer_differences() False @@ -359,7 +360,7 @@ def has_negative_integer_differences(self): sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/2]) sage: p - ([1/4, 1/3, 1/2], [2/5, 3/2, 1]) + ((1/4, 1/3, 1/2), (2/5, 3/2, 1)) sage: p.has_negative_integer_differences() True """ From 2f430dd2ee0d9aaf445e2f21b16e661308d26dce Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Wed, 29 Oct 2025 21:08:24 +0100 Subject: [PATCH 20/93] easy fixes --- src/sage/functions/hypergeometric.py | 4 +++- .../functions/hypergeometric_algebraic.py | 19 ++++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/sage/functions/hypergeometric.py b/src/sage/functions/hypergeometric.py index b5dc44df6f1..cac0a018d2a 100644 --- a/src/sage/functions/hypergeometric.py +++ b/src/sage/functions/hypergeometric.py @@ -306,7 +306,7 @@ def __call__(self, a, b, z, **kwargs): if isinstance(z, (Polynomial, PowerSeries)): if not z.is_gen(): raise NotImplementedError("the argument must be the generator of the polynomial ring") - S = x.parent() + S = z.parent() from sage.functions.hypergeometric_algebraic import HypergeometricFunctions return HypergeometricFunctions(S.base_ring(), S.variable_name())(a, b) else: @@ -759,8 +759,10 @@ def _deflated(self, a, b, z): return terms return ((1, new),) + hypergeometric = Hypergeometric() + def closed_form(hyp): """ Try to evaluate ``hyp`` in closed form using elementary diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 17980a2fbe0..136ca33fde1 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -61,6 +61,9 @@ from sage.rings.polynomial.ore_polynomial_ring import OrePolynomialRing +# Helper functions +################## + def insert_zeroes(P, n): cs = P.list() coeffs = n * len(cs) * [0] @@ -68,6 +71,7 @@ def insert_zeroes(P, n): coeffs[n*i] = cs[i] return P.parent()(coeffs) + def kernel(M, repeat=2): n = M.nrows() m = M.ncols() @@ -166,8 +170,8 @@ def __init__(self, top, bottom, add_one=True): self.d = 1 self.bound = 1 else: - self.d = lcm([ a.denominator() for a in top ] - + [ b.denominator() for b in bottom ]) + self.d = lcm([ a.denominator() for a in top ] + + [ b.denominator() for b in bottom ]) self.bound = 2 * self.d * max(top + bottom) + 1 def __repr__(self): @@ -216,13 +220,9 @@ def is_balanced(self): @cached_method def christol_sorting(self, c=1): - r""" - """ d = self.d - def mod2(x): - return d - (-x) % d - A = [(mod2(d*c*a), -a, 1) for a in self.top] - B = [(mod2(d*c*b), -b, -1) for b in self.bottom] + A = [d - (-d*c*a) % d, -a, 1) for a in self.top] + B = [d - (-d*c*b) % d, -b, -1) for b in self.bottom] return sorted(A + B) def parenthesis_criterion(self, c): @@ -534,7 +534,7 @@ def derivative(self): class HypergeometricAlgebraic_charzero(HypergeometricAlgebraic): def is_defined(self): - return not any(b in ZZ and b < 0 for b in self._bottom) + return not any(b in ZZ and b < 0 for b in self.bottom()) def series(self, prec): S = self.parent().power_series_ring() @@ -887,6 +887,7 @@ def _call_(self, h): from sage.functions.hypergeometric import _hypergeometric return h.scalar() * _hypergeometric(h.top(), h.bottom(), SR.var(h.parent().variable_name())) + class ScalarMultiplication(Action): def _act_(self, scalar, h): return h.parent()(h, scalar=scalar) From 5e1a7edf3541d9e124dbf854d7cd13c16ace0d90 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Wed, 29 Oct 2025 21:10:40 +0100 Subject: [PATCH 21/93] missing parenthesis --- src/sage/functions/hypergeometric_algebraic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 136ca33fde1..9551452a802 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -221,8 +221,8 @@ def is_balanced(self): @cached_method def christol_sorting(self, c=1): d = self.d - A = [d - (-d*c*a) % d, -a, 1) for a in self.top] - B = [d - (-d*c*b) % d, -b, -1) for b in self.bottom] + A = [(d - (-d*c*a) % d, -a, 1) for a in self.top] + B = [(d - (-d*c*b) % d, -b, -1) for b in self.bottom] return sorted(A + B) def parenthesis_criterion(self, c): From 77d9a94bbe5dcb2343089c6f06686176ff03d257 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Thu, 30 Oct 2025 08:56:08 +0100 Subject: [PATCH 22/93] hadamard product --- .../functions/hypergeometric_algebraic.py | 78 +++++++++++-------- 1 file changed, 45 insertions(+), 33 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 9551452a802..989ea5293f4 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -35,6 +35,7 @@ from sage.structure.unique_representation import UniqueRepresentation from sage.structure.parent import Parent from sage.structure.element import Element +from sage.structure.element import coerce_binop from sage.structure.sequence import Sequence from sage.structure.category_object import normalize_names @@ -235,31 +236,6 @@ def parenthesis_criterion(self, c): previous_paren = paren return parenthesis >= 0 - def q_christol_sorting(self, q): - d = self.d - A = [(1/2 + (-a) % q, 1) for a in self.top] - B = [(1 + (-b) % q, -1) for b in self.bottom] - return sorted(A + B) - - def q_parenthesis_criterion(self, q): - parenthesis = 0 - previous_paren = -1 - for _, paren in self.q_christol_sorting(q): - parenthesis += paren - if parenthesis < 0: - return False - previous_paren = paren - return parenthesis >= 0 - - def q_interlacing_number(self, q): - interlacing = 0 - previous_paren = -1 - for _, paren in self.q_christol_sorting(q): - if paren == -1 and previous_paren == 1: - interlacing += 1 - previous_paren = paren - return interlacing - def interlacing_criterion(self, c): r""" Return ``True`` if the sorted lists of the decimal parts (where integers @@ -295,14 +271,38 @@ def interlacing_criterion(self, c): sage: p.interlacing_criterion(3) False """ - AB = self.christol_sorting(c) previous_paren = -1 - for _, _, paren in AB: + for _, _, paren in self.christol_sorting(c): if paren == previous_paren: return False previous_paren = paren return True + def q_christol_sorting(self, q): + d = self.d + A = [(1/2 + (-a) % q, 1) for a in self.top] + B = [(1 + (-b) % q, -1) for b in self.bottom] + return sorted(A + B) + + def q_parenthesis_criterion(self, q): + parenthesis = 0 + previous_paren = -1 + for _, paren in self.q_christol_sorting(q): + parenthesis += paren + if parenthesis < 0: + return False + previous_paren = paren + return parenthesis >= 0 + + def q_interlacing_number(self, q): + interlacing = 0 + previous_paren = -1 + for _, paren in self.q_christol_sorting(q): + if paren == -1 and previous_paren == 1: + interlacing += 1 + previous_paren = paren + return interlacing + def remove_positive_integer_differences(self): r""" Return parameters, where pairs consisting of a top parameter @@ -366,6 +366,11 @@ def has_negative_integer_differences(self): """ return any(a - b in ZZ and a < b for a in self.top for b in self.bottom) + def decimal_part(self): + top = [1 + a - ceil(a) for a in self.top] + bottom = [1 + b - ceil(b) for b in self.bottom] + return Parameters(top, bottom, add_one=False) + def dwork_image(self, p): try: top = [(a + (-a) % p) / p for a in self.top] @@ -374,11 +379,6 @@ def dwork_image(self, p): raise ValueError("denominators of parameters are not coprime to p") return Parameters(top, bottom, add_one=False) - def decimal_part(self): - top = [1 + a - ceil(a) for a in self.top] - bottom = [1 + b - ceil(b) for b in self.bottom] - return Parameters(top, bottom, add_one=False) - def frobenius_order(self, p): param = self.decimal_part() iter = param.dwork_image(p) @@ -502,6 +502,17 @@ def _sub_(self, other): def _mul_(self, other): return SR(self) * SR(other) + @coerce_binop + def hadamard_product(self, other): + if self._scalar == 0: + return self + if other._scalar == 0: + return other + top = self.top() + other.top() + bottom = self._parameters.bottom + other.bottom() + scalar = self._scalar * other._scalar + return self.parent()(top, bottom, scalar=scalar) + def _div_(self, other): return SR(self) / SR(other) @@ -597,7 +608,7 @@ def good_reduction_primes(self): goods[cc] = True cc = (cc * c) % d - # We treat exceptionnal primes + # We treat exceptional primes bound = params.bound exceptions = [] for p in Primes(): @@ -719,6 +730,7 @@ def is_defined_conjectural(self): def series(self, prec): S = self.parent().power_series_ring() p = self._p + # TODO: check that the precision is correct pprec = max(1, (len(self.bottom()) + 1) * ceil(log(prec, p))) K = QpFP(p, pprec) c = K(self._scalar) From b5b3733ce3e6d7a63295cc98c4bf42f4cf31eb27 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Thu, 30 Oct 2025 11:40:10 +0100 Subject: [PATCH 23/93] use Qp instead of QpFP --- src/sage/functions/hypergeometric_algebraic.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 989ea5293f4..058b1aeb8a6 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -54,7 +54,7 @@ from sage.rings.integer_ring import ZZ from sage.rings.rational_field import QQ from sage.rings.finite_rings.finite_field_constructor import FiniteField -from sage.rings.padics.factory import QpFP +from sage.rings.padics.factory import Qp from sage.rings.number_field.number_field import CyclotomicField from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing @@ -730,9 +730,7 @@ def is_defined_conjectural(self): def series(self, prec): S = self.parent().power_series_ring() p = self._p - # TODO: check that the precision is correct - pprec = max(1, (len(self.bottom()) + 1) * ceil(log(prec, p))) - K = QpFP(p, pprec) + K = Qp(p, 1) c = K(self._scalar) coeffs = [c] for i in range(prec-1): From 360cb7d01c5cadcf8689d639225a5a4b2447bd54 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Sat, 1 Nov 2025 10:42:37 +0100 Subject: [PATCH 24/93] use Primes --- src/sage/functions/hypergeometric_algebraic.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 058b1aeb8a6..6a93fceae31 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -43,7 +43,6 @@ from sage.categories.pushout import pushout from sage.categories.map import Map from sage.categories.finite_fields import FiniteFields -from sage.sets.primes import Primes from sage.matrix.special import companion_matrix from sage.matrix.special import identity_matrix @@ -51,6 +50,7 @@ from sage.symbolic.ring import SR from sage.combinat.subset import Subsets from sage.rings.infinity import infinity +from sage.sets.primes import Primes from sage.rings.integer_ring import ZZ from sage.rings.rational_field import QQ from sage.rings.finite_rings.finite_field_constructor import FiniteField @@ -610,7 +610,7 @@ def good_reduction_primes(self): # We treat exceptional primes bound = params.bound - exceptions = [] + exceptions = {} for p in Primes(): if p > bound: break @@ -619,12 +619,12 @@ def good_reduction_primes(self): q = p while q <= bound: if not self._parameters.q_parenthesis_criterion(q): - exceptions.append(p) + exceptions[p] = False break q *= p goods = [c for c, v in goods.items() if v] - return d, goods, exceptions + return Primes(modulus=d, classes=goods, exceptions=exceptions) def is_algebraic(self): if any(a in ZZ and a <= 0 for a in self.top()): @@ -818,6 +818,7 @@ def dwork_relation(self): def annihilating_ore_polynomial(self, var='Frob'): # QUESTION: does this method actually return the # minimal Ore polynomial annihilating self? + # Probably not :-( if not self._parameters.is_balanced(): raise NotImplementedError("the hypergeometric function is not a pFq with q = p-1") @@ -894,8 +895,8 @@ def is_lucas(self): class HypergeometricToSR(Map): def _call_(self, h): - from sage.functions.hypergeometric import _hypergeometric - return h.scalar() * _hypergeometric(h.top(), h.bottom(), SR.var(h.parent().variable_name())) + from sage.functions.hypergeometric import hypergeometric + return h.scalar() * hypergeometric(h.top(), h.bottom(), SR.var(h.parent().variable_name())) class ScalarMultiplication(Action): From 55cb54e3119a15e3ec84df0b650e56f029747103 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Sat, 1 Nov 2025 16:07:28 +0100 Subject: [PATCH 25/93] documentation + small fixes/improvements --- src/sage/sets/primes.py | 674 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 609 insertions(+), 65 deletions(-) diff --git a/src/sage/sets/primes.py b/src/sage/sets/primes.py index 84ba396519a..dcb249295e9 100644 --- a/src/sage/sets/primes.py +++ b/src/sage/sets/primes.py @@ -19,6 +19,7 @@ # **************************************************************************** from sage.rings.integer_ring import ZZ +from sage.rings.infinity import infinity from .set import Set_generic from sage.categories.finite_enumerated_sets import FiniteEnumeratedSets from sage.categories.infinite_enumerated_sets import InfiniteEnumeratedSets @@ -27,8 +28,80 @@ class Primes(Set_generic, UniqueRepresentation): + r""" + The set of prime numbers and some of its subsets. + + EXAMPLES: + + The set of all primes numbers:: + + sage: P = Primes(); P + Set of all prime numbers: 2, 3, 5, 7, ... + + The arguments `modulus` and `classes` allows for constructing + subsets given by congruence conditions:: + + sage: Primes(modulus=4) + Set of prime numbers congruent to 1 modulo 4: 5, 13, 17, 29, ... + + We see that, by default, Sagemath selects to congruence class `1`. + The user can user pass in explicitely other classes:: + + sage: Primes(modulus=4, classes=[3]) + Set of prime numbers congruent to 3 modulo 4: 3, 7, 11, 19, ... + sage: Primes(modulus=8, classes=[1, 3]) + Set of prime numbers congruent to 1, 3 modulo 8: 3, 11, 17, 19, ... + + When possible, the congruence conditions are simplified:: + + sage: Primes(modulus=8, classes=[1, 5]) + Set of prime numbers congruent to 1 modulo 4: 5, 13, 17, 29, ... + + We show various operations:: + + sage: P = Primes(modulus=4); P + Set of prime numbers congruent to 1 modulo 4: 5, 13, 17, 29, ... + sage: P.cardinality() + +Infinity + sage: P[:10] + [5, 13, 17, 29, 37, 41, 53, 61, 73, 89] + sage: P.next(500) + 509 + + :: + + sage: Q = Primes(modulus=4, classes=[3]) + sage: PQ = P.union(Q) + sage: PQ + Set of all prime numbers with 2 excluded: 3, 5, 7, 11, ... + sage: PQ.complement_in_primes() + Finite set of prime numbers: 2 + sage: PQ.complement_in_primes().cardinality() + 1 + """ @staticmethod - def __classcall_private__(cls, modulus=1, classes=None, exceptions=None): + def __classcall__(cls, modulus=1, classes=None, exceptions=None): + """ + Normalize the input. + + TESTS:: + + sage: Primes(modulus=10) + Set of prime numbers congruent to 1 modulo 5: 11, 31, 41, 61, ... + sage: Primes(modulus=10) == Primes(modulus=5) + True + + sage: Primes(modulus=9, classes=[1, 3, 4, 7]) + Set of prime numbers congruent to 1 modulo 3 with 3 included: 3, 7, 13, 19, ... + + sage: Primes(modulus=5, exceptions={7: True, 11: False}) + Set of prime numbers congruent to 1 modulo 5 with 7 included and 11 excluded: 7, 31, 41, 61, ... + + sage: Primes(modulus=0) + Traceback (most recent call last): + ... + ValueError: modulus must be nonzero + """ modulus = ZZ(modulus) if modulus == 0: raise ValueError("modulus must be nonzero") @@ -36,9 +109,16 @@ def __classcall_private__(cls, modulus=1, classes=None, exceptions=None): modulus = -modulus if classes is None: classes = [1] - indic = modulus * [False] if exceptions is None: exceptions = {} + if isinstance(exceptions, (tuple, list)): + exceptions = {c: v for c, v in exceptions} + + # We replace congruences of the form + # p = a (mod n) with gcd(a, n) > 1 + # (which only includes a finite number of primes) + # by exceptions + indic = modulus * [False] for c in classes: indic[ZZ(c) % modulus] = True for c in range(modulus): @@ -52,6 +132,9 @@ def __classcall_private__(cls, modulus=1, classes=None, exceptions=None): if c not in exceptions: exceptions[c] = True indic[c] = None + + # We normalize the congruence conditions + # by minimizing the modulus for p, mult in modulus.factor(): while mult > 0: m = modulus // p @@ -88,16 +171,36 @@ def __classcall_private__(cls, modulus=1, classes=None, exceptions=None): exceptions[c] = False modulus = m mult -= 1 + + # We format the final result classes = tuple([c for c in range(modulus) if indic[c] is True]) - excep = [] - for c, v in list(exceptions.items()): + exceptions_list = [] + for c, v in exceptions.items(): c = ZZ(c) if c.is_prime() and (v != (indic[c % modulus] is True)): - excep.append((c, v)) - excep.sort() - return cls.__classcall__(cls, modulus, classes, tuple(excep)) + exceptions_list.append((c, v)) + exceptions_list.sort() + + return super().__classcall__(cls, modulus, classes, tuple(exceptions_list)) def __init__(self, modulus, classes, exceptions): + r""" + Initialize this set. + + TESTS:: + + sage: P = Primes(modulus=4) + sage: P.category() + Category of facade infinite enumerated sets + sage: TestSuite(P).run() + + :: + + sage: Q = Primes(classes=[]).include([2, 3, 5]) + sage: Q.category() + Category of facade finite enumerated sets + sage: TestSuite(Q).run() + """ if classes: category = InfiniteEnumeratedSets() else: @@ -106,42 +209,82 @@ def __init__(self, modulus, classes, exceptions): self._modulus = modulus self._classes = set(classes) self._exceptions = {} - self._included = [] - self._excluded = [] + self._elements = [] for c, v in exceptions: self._exceptions[c] = v - if v: - self._included.append(c) - else: - self._excluded.append(c) - self._included.sort() - self._excluded.sort() + if v and not classes: + self._elements.append(c) + self._elements.sort() def _repr_(self): + r""" + Return a string representation of this subset. + + TESTS:: + + sage: Primes(modulus=4).include(2).exclude(5) # indirect doctest + Set of prime numbers congruent to 1 modulo 4 with 2 included and 5 excluded: 2, 13, 17, 29, ... + + sage: E = Primes(classes=[]) + sage: E # indirect doctest + Empty set of prime numbers + + sage: E.include(range(50), check=False) # indirect doctest + Finite set of prime numbers: 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47 + """ classes = sorted(list(self._classes)) sc = ", ".join([str(c) for c in classes]) - si = ", ".join([str(i) for i in self._included]) - se = ", ".join([str(e) for e in self._excluded]) - if sc == "": - if si == "": - return "Empty set of primes" + included = [] + excluded = [] + for c, v in self._exceptions.items(): + if v: + included.append(c) + else: + excluded.append(c) + si = ", ".join([str(i) for i in sorted(included)]) + se = ", ".join([str(e) for e in sorted(excluded)]) + if not classes: + if not included: + return "Empty set of prime numbers" else: - return "Finite set of primes: %s" % si + return "Finite set of prime numbers: %s" % si if self._modulus == 1: s = "Set of all prime numbers" else: s = "Set of prime numbers congruent to %s modulo %s" % (sc, self._modulus) - if si != "": + if included: s += " with %s included" % si - if se != "": - if si == "": + if excluded: + if not included: s += " with %s excluded" % se else: s += " and %s excluded" % se - s += ": %s, ..." % (", ".join([str(c) for c in self.first_n(4)])) + s += ": %s, ..." % (", ".join([str(c) for c in self[:4]])) return s def __contains__(self, x): + r""" + Return ``True`` if `x` is in this set; ``False`` otherwise. + + INPUT: + + - ``x`` -- an integer + + EXAMPLES:: + + sage: P = Primes(modulus=4) + sage: 3 in P + False + sage: 9 in P + False + sage: 13 in P + True + + TESTS:: + + sage: x in P + False + """ try: if x not in ZZ: return False @@ -153,80 +296,333 @@ def __contains__(self, x): e = self._exceptions.get(x, None) return (e is True) or (e is None and x % self._modulus in self._classes) - def next(self, pr): - pr = ZZ(pr) + def cardinality(self): + r""" + Return the cardinality of this set. + + EXAMPLES:: + + sage: P = Primes(modulus=4); P + Set of prime numbers congruent to 1 modulo 4: 5, 13, 17, 29, ... + sage: P.cardinality() + +Infinity + + :: + + sage: P = Primes(modulus=4, classes=[2]); P + Finite set of prime numbers: 2 + sage: P.cardinality() + 1 + """ + if self.is_finite(): + return ZZ(len(self._elements)) + else: + return infinity + + def first(self, n=None): + r""" + Return the first element in this set. + + EXAMPLES:: + + sage: P = Primes(modulus=5); P + Set of prime numbers congruent to 1 modulo 5: 11, 31, 41, 61, ... + sage: P.first() + 11 + """ + if self.is_empty(): + return + return self.next(1) + + def next(self, x): + r""" + Return the smallest element in this set strictly + greater than ``x``. + + INPUT: + + - ``x`` -- an integer + + EXAMPLES:: + + sage: P = Primes(modulus=5); P + Set of prime numbers congruent to 1 modulo 5: 11, 31, 41, 61, ... + sage: P.next(1000) + 1021 + + If there is no element greater than the given bound, an + error is raised:: + + sage: P = Primes(modulus=10, classes=[2, 5]); P + Finite set of prime numbers: 2, 5 + sage: P.next(10) + Traceback (most recent call last): + ... + ValueError: no element greater that 10 in this set + """ + x = ZZ(x) if not self._classes: - if not (self._included and pr < self._included[-1]): - raise ValueError("no element greater that %s in this set" % pr) + if not (self._elements and x < self._elements[-1]): + raise ValueError("no element greater that %s in this set" % x) min = 0 - max = len(self._included) + max = len(self._elements) while min < max: i = (min + max) // 2 - print(min, max, i) - if self._included[i] <= pr: + if self._elements[i] <= x: min = i + 1 - if self._included[i] > pr: + if self._elements[i] > x: max = i - return self._included[min] + return self._elements[min] while True: - pr = pr.next_prime() - e = self._exceptions.get(pr, None) - if (e is True) or (e is None and pr % self._modulus in self._classes): - return pr + x = x.next_prime() + e = self._exceptions.get(x, None) + if (e is True) or (e is None and x % self._modulus in self._classes): + return x - def first(self): - return self.next(1) + def _an_element_(self): + r""" + Return an element in this set. + + EXAMPLES:: + + sage: P = Primes() + sage: P.an_element() # indirect doctest + 43 - def first_n(self, n): - pr = 1 - ans = [] - for _ in range(n): - try: - pr = self.next(pr) - except ValueError: - return ans - ans.append(pr) - return ans - - def _an_element_(self, n): + If the set is empty, an error is raised:: + + sage: P = Primes(modulus=12, classes=[4]) + sage: P.an_element() # indirect doctest + Traceback (most recent call last): + ... + ValueError: this set is empty + """ if self.is_finite(): - if self._included: - return self._included[0] + if self._elements: + return self._elements[0] raise ValueError("this set is empty") return self.next(42) def unrank(self, n): - pr = 1 - for _ in range(n): - try: - pr = self.next(pr) - except ValueError: - raise ValueError("this set has less than %s elements" % n) - return pr + r""" + Return the ``n``-th element of this set. + + INPUT: + + - ``n`` -- an integer + + EXAMPLES:: + + sage: P = Primes(modulus=5); P + Set of prime numbers congruent to 1 modulo 5: 11, 31, 41, 61, ... + sage: P[0] # indirect doctest + 11 + sage: P[10] # indirect doctest + 211 + + If there is less than `n` elements in this set, an error + is raised:: + + sage: P = Primes(modulus=10, classes=[2, 5]); P + Finite set of prime numbers: 2, 5 + sage: P[1] + 5 + sage: P[2] # indirect doctest + Traceback (most recent call last): + ... + IndexError: this set has not enough elements + + TESTS:: + + sage: P.unrank(-1) + Traceback (most recent call last): + ... + IndexError: index must be nonnegative + """ + if n < 0: + raise IndexError("index must be nonnegative") + if self.is_finite(): + if len(self._elements) <= n: + raise IndexError("this set has not enough elements") + return self._elements[n] + if self._elements: + x = self._elements[-1] + else: + x = 1 + while len(self._elements) <= n: + x = self.next(x) + self._elements.append(x) + return self._elements[n] def is_empty(self): + r""" + Return ``True`` if this set is empty; ``False`` otherwise. + + EXAMPLES:: + + sage: P = Primes(modulus=6); P + Set of prime numbers congruent to 1 modulo 3: 7, 13, 19, 31, ... + sage: P.is_empty() + False + + :: + + sage: P = Primes(modulus=6, classes=[6]); P + Empty set of prime numbers + sage: P.is_empty() + True + + .. SEEALSO:: + + :meth:`is_finite`, :meth:`is_cofinite` + """ return not bool(self._classes) and not bool(self._exceptions) def is_finite(self): + r""" + Return ``True`` if this set is finite; ``False`` otherwise. + + EXAMPLES:: + + sage: P = Primes(modulus=5); P + Set of prime numbers congruent to 1 modulo 5: 11, 31, 41, 61, ... + sage: P.is_finite() + False + + :: + + sage: P = Primes(modulus=10, classes=[2, 5]); P + Finite set of prime numbers: 2, 5 + sage: P.is_finite() + True + + .. SEEALSO:: + + :meth:`is_empty`, :meth:`is_cofinite` + """ return not bool(self._classes) def is_cofinite(self): + r""" + Return ``True`` if this set is cofinite in the set + of all prime numbers; ``False`` otherwise. + + EXAMPLES:: + + sage: P = Primes(modulus=4); P + Set of prime numbers congruent to 1 modulo 4: 5, 13, 17, 29, ... + sage: P.is_cofinite() + False + + :: + + sage: P = Primes(modulus=4, classes=[1, 3]); P + Set of all prime numbers with 2 excluded: 3, 5, 7, 11, ... + sage: P.is_cofinite() + True + + .. SEEALSO:: + + :meth:`is_empty`, :meth:`is_finite` + """ return self._modulus == 1 and bool(self._classes) def density(self): + r""" + Return the density of this set in the set of all + prime numbers. + + EXAMPLES:: + + sage: P = Primes(modulus=4); P + Set of prime numbers congruent to 1 modulo 4: 5, 13, 17, 29, ... + sage: P.density() + 1/2 + """ return len(self._classes) / euler_phi(self._modulus) def include(self, elements, check=True): + r""" + Return this set with the integers in ``elements`` included. + + INPUT: + + - ``elements`` -- an integer, or a tuple/list of integers + + - ``check`` -- a boolean (default: ``True``); if ``False``, + do not raise an error if we try to add composite numbers + + EXAMPLES:: + + sage: P = Primes(modulus=5); P + Set of prime numbers congruent to 1 modulo 5: 11, 31, 41, 61, ... + sage: P.include(2) + Set of prime numbers congruent to 1 modulo 5 with 2 included: 2, 11, 31, 41, ... + sage: P.include([2, 3]) + Set of prime numbers congruent to 1 modulo 5 with 2, 3 included: 2, 3, 11, 31, ... + + If we try to include an element which is already in the set, + nothing changes:: + + sage: P.include(11) + Set of prime numbers congruent to 1 modulo 5: 11, 31, 41, 61, ... + + Trying to include a composite number results in an error:: + + sage: P.include(10) + Traceback (most recent call last): + ... + ValueError: 10 is not a prime number + + We can avoid this by passing in ``check=False``; in this case, + composite numbers are however not added to the set. + This behavior can be convenient if one wants to add all prime + numbers in a range:: + + sage: P.include(range(20, 30), check=False) + Set of prime numbers congruent to 1 modulo 5 with 23, 29 included: 11, 23, 29, 31, ... + + .. SEEALSO:: + + :meth:`exclude` + """ if elements in ZZ: elements = [elements] exceptions = self._exceptions.copy() for x in elements: + x = ZZ(x) if check and not x.is_prime(): raise ValueError("%s is not a prime number" % x) exceptions[x] = True return Primes(self._modulus, self._classes, exceptions) def exclude(self, elements): + r""" + Return this set with the integers in ``elements`` excluded. + + INPUT: + + - ``elements`` -- an integer, or a tuple/list of integers + + EXAMPLES:: + + sage: P = Primes(modulus=5); P + Set of prime numbers congruent to 1 modulo 5: 11, 31, 41, 61, ... + sage: P.exclude(11) + Set of prime numbers congruent to 1 modulo 5 with 11 excluded: 31, 41, 61, 71, ... + sage: P.exclude([11, 31]) + Set of prime numbers congruent to 1 modulo 5 with 11, 31 excluded: 41, 61, 71, 101, ... + + If we try to exclude an element which is not in the set, + nothing changes:: + + sage: P.exclude(2) + Set of prime numbers congruent to 1 modulo 5: 11, 31, 41, 61, ... + + .. SEEALSO:: + + :meth:`include` + """ if elements in ZZ: elements = [elements] exceptions = self._exceptions.copy() @@ -235,6 +631,32 @@ def exclude(self, elements): return Primes(self._modulus, self._classes, exceptions) def complement_in_primes(self): + r""" + Return the complement of this set in the set of all prime + numbers. + + EXAMPLES:: + + sage: P = Primes(modulus=4); P + Set of prime numbers congruent to 1 modulo 4: 5, 13, 17, 29, ... + sage: Q = P.complement_in_primes(); Q + Set of prime numbers congruent to 3 modulo 4 with 2 included: 2, 3, 7, 11, ... + + We check that the union of `P` and `Q` is the whole set of + prime numbers:: + + sage: P.union(Q) + Set of all prime numbers: 2, 3, 5, 7, ... + + and that the intersection is empty:: + + sage: P.intersection(Q) + Empty set of prime numbers + + .. SEEALSO:: + + :meth:`intersection`, :meth:`union` + """ modulus = self._modulus classes = [c for c in range(modulus) if c % self._modulus not in self._classes] @@ -242,10 +664,39 @@ def complement_in_primes(self): return Primes(modulus, classes, exceptions) def intersection(self, other): - if other in ZZ: + r""" + Return the intesection of this set with ``other``. + + INPUT: + + - ``other`` -- a subset of the set of prime numbers + + EXAMPLES:: + + sage: P = Primes(modulus=5); P + Set of prime numbers congruent to 1 modulo 5: 11, 31, 41, 61, ... + sage: Q = Primes(modulus=3, classes=[2]); Q + Set of prime numbers congruent to 2 modulo 3: 2, 5, 11, 17, ... + sage: P.intersection(Q) + Set of prime numbers congruent to 11 modulo 15: 11, 41, 71, 101, ... + + TESTS:: + + sage: P.intersection(ZZ) == P + True + sage: P.intersection(RR) + Traceback (most recent call last): + ... + NotImplementedError: boolean operations are only implemented with other sets of prime numbers + + .. SEEALSO:: + + :meth:`complement_in_primes`, :meth:`union` + """ + if other is ZZ: return self if not isinstance(other, Primes): - raise NotImplementedError("boolean operations are only implemented with other sets of primes") + raise NotImplementedError("boolean operations are only implemented with other sets of prime numbers") modulus = self._modulus.lcm(other._modulus) classes = [c for c in range(modulus) if (c % self._modulus in self._classes @@ -264,10 +715,39 @@ def intersection(self, other): return Primes(modulus, classes, exceptions) def union(self, other): + r""" + Return the intesection of this set with ``other``. + + INPUT: + + - ``other`` -- a subset of the set of prime numbers + + EXAMPLES:: + + sage: P = Primes(modulus=5); P + Set of prime numbers congruent to 1 modulo 5: 11, 31, 41, 61, ... + sage: Q = Primes(modulus=3, classes=[2]); Q + Set of prime numbers congruent to 2 modulo 3: 2, 5, 11, 17, ... + sage: P.union(Q) + Set of prime numbers congruent to 1, 2, 8, 11, 14 modulo 15 with 5 included: 2, 5, 11, 17, ... + + TESTS:: + + sage: P.union(ZZ) == ZZ + True + sage: P.union(RR) + Traceback (most recent call last): + ... + NotImplementedError: boolean operations are only implemented with other sets of prime numbers + + .. SEEALSO:: + + :meth:`complement_in_primes`, :meth:`intersection` + """ if other is ZZ: return ZZ if not isinstance(other, Primes): - raise NotImplementedError("boolean operations are only implemented with other sets of primes") + raise NotImplementedError("boolean operations are only implemented with other sets of prime numbers") modulus = self._modulus.lcm(other._modulus) classes = [c for c in range(modulus) if (c % self._modulus in self._classes @@ -286,10 +766,74 @@ def union(self, other): return Primes(modulus, classes, exceptions) def is_subset(self, other): + r""" + Return ``True`` is this set of is subset of ``other``; + ``False`` otherwise. + + EXAMPLES:: + + sage: P = Primes(modulus=4); P + Set of prime numbers congruent to 1 modulo 4: 5, 13, 17, 29, ... + sage: Q = Primes(modulus=8); Q + Set of prime numbers congruent to 1 modulo 8: 17, 41, 73, 89, ... + sage: P.is_subset(Q) + False + sage: Q.is_subset(P) + True + + .. SEEALSO:: + + :meth:`is_supset`, :meth:`is_disjoint` + """ return self.intersection(other) == self def is_supset(self, other): - return self.intersection(other) == self + r""" + Return ``True`` is this set of is supset of ``other``; + ``False`` otherwise. + + EXAMPLES:: + + sage: P = Primes(modulus=4); P + Set of prime numbers congruent to 1 modulo 4: 5, 13, 17, 29, ... + sage: Q = Primes(modulus=8); Q + Set of prime numbers congruent to 1 modulo 8: 17, 41, 73, 89, ... + sage: P.is_supset(Q) + True + sage: Q.is_supset(P) + False + + .. SEEALSO:: + + :meth:`is_subset`, :meth:`is_disjoint` + """ + return self.intersection(other) == other def is_disjoint(self, other): + r""" + Return ``True`` is this set of is disjoint from ``other``; + ``False`` otherwise. + + EXAMPLES:: + + sage: P = Primes(modulus=4); P + Set of prime numbers congruent to 1 modulo 4: 5, 13, 17, 29, ... + sage: Q = Primes(modulus=4, classes=[3]); Q + Set of prime numbers congruent to 3 modulo 4: 3, 7, 11, 19, ... + sage: P.is_disjoint(Q) + True + + :: + + sage: R = Primes(modulus=5, classes=[3]); R + Set of prime numbers congruent to 3 modulo 5: 3, 13, 23, 43, ... + sage: P.is_disjoint(R) + False + sage: Q.is_disjoint(R) + False + + .. SEEALSO:: + + :meth:`is_subset`, :meth:`is_disjoint` + """ return self.intersection(other).is_empty() From 3c812a59507f794197a3a6edb8843444d96a2732 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Sat, 1 Nov 2025 17:51:05 +0100 Subject: [PATCH 26/93] fix typos in documentation --- src/sage/sets/primes.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/sage/sets/primes.py b/src/sage/sets/primes.py index dcb249295e9..f1071214af9 100644 --- a/src/sage/sets/primes.py +++ b/src/sage/sets/primes.py @@ -38,14 +38,14 @@ class Primes(Set_generic, UniqueRepresentation): sage: P = Primes(); P Set of all prime numbers: 2, 3, 5, 7, ... - The arguments `modulus` and `classes` allows for constructing + The arguments ``modulus`` and ``classes`` allows for constructing subsets given by congruence conditions:: sage: Primes(modulus=4) Set of prime numbers congruent to 1 modulo 4: 5, 13, 17, 29, ... - We see that, by default, Sagemath selects to congruence class `1`. - The user can user pass in explicitely other classes:: + We see that, by default, Sagemath selects the congruence class `1`. + The user can however pass in explicitely other classes:: sage: Primes(modulus=4, classes=[3]) Set of prime numbers congruent to 3 modulo 4: 3, 7, 11, 19, ... @@ -57,7 +57,7 @@ class Primes(Set_generic, UniqueRepresentation): sage: Primes(modulus=8, classes=[1, 5]) Set of prime numbers congruent to 1 modulo 4: 5, 13, 17, 29, ... - We show various operations:: + We show various operations that can be performed on these sets:: sage: P = Primes(modulus=4); P Set of prime numbers congruent to 1 modulo 4: 5, 13, 17, 29, ... @@ -465,7 +465,7 @@ def is_empty(self): sage: P.is_empty() False - :: + :: sage: P = Primes(modulus=6, classes=[6]); P Empty set of prime numbers @@ -489,7 +489,7 @@ def is_finite(self): sage: P.is_finite() False - :: + :: sage: P = Primes(modulus=10, classes=[2, 5]); P Finite set of prime numbers: 2, 5 @@ -514,7 +514,7 @@ def is_cofinite(self): sage: P.is_cofinite() False - :: + :: sage: P = Primes(modulus=4, classes=[1, 3]); P Set of all prime numbers with 2 excluded: 3, 5, 7, 11, ... From b4bb605dbcfd3624aaf749dd372d3ec9135f1344 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Sun, 2 Nov 2025 19:40:45 +0100 Subject: [PATCH 27/93] add a check in the kernel method --- src/sage/functions/hypergeometric_algebraic.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 6a93fceae31..315b6e2c7ad 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -94,6 +94,9 @@ def kernel(M, repeat=2): for i in range(1, n): minor = MJ.delete_rows([i]).determinant() ker.append((-1)**i * minor) + Z = matrix(ker) * M + if not Z.is_zero(): + return g = ker[0].leading_coefficient() * gcd(ker) ker = [c//g for c in ker] return ker From b5058c1ddfb8b86be57caa5aad33126f7eb5be97 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Wed, 5 Nov 2025 13:20:56 +0100 Subject: [PATCH 28/93] reimplement dwork_relation (should also work for small p, now) --- .../functions/hypergeometric_algebraic.py | 151 +++++++++--------- 1 file changed, 72 insertions(+), 79 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 315b6e2c7ad..84eec327da8 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -27,7 +27,7 @@ from sage.misc.misc_c import prod from sage.misc.functional import log -from sage.functions.other import ceil +from sage.functions.other import floor, ceil from sage.arith.misc import gcd from sage.arith.functions import lcm from sage.matrix.constructor import matrix @@ -225,19 +225,19 @@ def is_balanced(self): @cached_method def christol_sorting(self, c=1): d = self.d - A = [(d - (-d*c*a) % d, -a, 1) for a in self.top] - B = [(d - (-d*c*b) % d, -b, -1) for b in self.bottom] + A = [(d - (-d*c*a) % d, -a, -1) for a in self.top] + B = [(d - (-d*c*b) % d, -b, 1) for b in self.bottom] return sorted(A + B) def parenthesis_criterion(self, c): parenthesis = 0 - previous_paren = -1 + previous_paren = 1 for _, _, paren in self.christol_sorting(c): parenthesis += paren - if parenthesis < 0: + if parenthesis > 0: return False previous_paren = paren - return parenthesis >= 0 + return parenthesis <= 0 def interlacing_criterion(self, c): r""" @@ -274,7 +274,7 @@ def interlacing_criterion(self, c): sage: p.interlacing_criterion(3) False """ - previous_paren = -1 + previous_paren = 1 for _, _, paren in self.christol_sorting(c): if paren == previous_paren: return False @@ -283,13 +283,13 @@ def interlacing_criterion(self, c): def q_christol_sorting(self, q): d = self.d - A = [(1/2 + (-a) % q, 1) for a in self.top] - B = [(1 + (-b) % q, -1) for b in self.bottom] + A = [(1/2 + (-a) % q, -1) for a in self.top] + B = [(1 + (-b) % q, 1) for b in self.bottom] return sorted(A + B) def q_parenthesis_criterion(self, q): parenthesis = 0 - previous_paren = -1 + previous_paren = 1 for _, paren in self.q_christol_sorting(q): parenthesis += paren if parenthesis < 0: @@ -299,9 +299,9 @@ def q_parenthesis_criterion(self, q): def q_interlacing_number(self, q): interlacing = 0 - previous_paren = -1 + previous_paren = 1 for _, paren in self.q_christol_sorting(q): - if paren == -1 and previous_paren == 1: + if paren == 1 and previous_paren == -1: interlacing += 1 previous_paren = paren return interlacing @@ -369,6 +369,11 @@ def has_negative_integer_differences(self): """ return any(a - b in ZZ and a < b for a in self.top for b in self.bottom) + def shift(self, s): + top = [a+s for a in self.top] + bottom = [b+s for b in self.bottom] + return Parameters(top, bottom, add_one=False) + def decimal_part(self): top = [1 + a - ceil(a) for a in self.top] bottom = [1 + b - ceil(b) for b in self.bottom] @@ -412,6 +417,7 @@ def __init__(self, parent, arg1, arg2=None, scalar=None): self._parameters = arg1 else: self._parameters = Parameters(arg1, arg2) + self._coeffs = [self._scalar] def __hash__(self): return hash((self.base_ring(), self._parameters, self._scalar)) @@ -505,6 +511,22 @@ def _sub_(self, other): def _mul_(self, other): return SR(self) * SR(other) + def series(self, prec): + S = self.parent().power_series_ring() + coeffs = self._coeffs + start = len(coeffs) - 1 + c = coeffs[-1] + for i in range(start, prec - 1): + for a in self._parameters.top: + c *= a + i + for b in self._parameters.bottom: + c /= b + i + coeffs.append(c) + return S(coeffs, prec=prec) + + def shift(self, s): + return self.parent()(self._parameters.shift(s), scalar=self._scalar) + @coerce_binop def hadamard_product(self, other): if self._scalar == 0: @@ -550,18 +572,6 @@ class HypergeometricAlgebraic_charzero(HypergeometricAlgebraic): def is_defined(self): return not any(b in ZZ and b < 0 for b in self.bottom()) - def series(self, prec): - S = self.parent().power_series_ring() - c = self._scalar - coeffs = [c] - for i in range(prec): - for a in self._parameters.top: - c *= a + i - for b in self._parameters.bottom: - c /= b + i - coeffs.append(c) - return S(coeffs, prec=prec) - class HypergeometricAlgebraic_QQ(HypergeometricAlgebraic_charzero): def mod(self, p): @@ -684,7 +694,23 @@ def is_maximum_unipotent_monodromy(self): class HypergeometricAlgebraic_GFp(HypergeometricAlgebraic): def __init__(self, parent, arg1, arg2=None, scalar=None): HypergeometricAlgebraic.__init__(self, parent, arg1, arg2, scalar) - self._p = self.base_ring().cardinality() + self._p = p = self.base_ring().cardinality() + self._coeffs = [Qp(p, 1)(self._scalar)] + + def series(self, prec): + S = self.parent().power_series_ring() + coeffs = self._coeffs + start = len(coeffs) - 1 + c = coeffs[-1] + for i in range(start, prec - 1): + for a in self._parameters.top: + c *= a + i + for b in self._parameters.bottom: + c /= b + i + if c.valuation() < 0: + raise ValueError("denominator appears in the series at the required precision") + coeffs.append(c) + return S(coeffs, prec=prec) def is_defined(self): p = self._p @@ -730,22 +756,6 @@ def is_defined_conjectural(self): q *= p return True - def series(self, prec): - S = self.parent().power_series_ring() - p = self._p - K = Qp(p, 1) - c = K(self._scalar) - coeffs = [c] - for i in range(prec-1): - for a in self._parameters.top: - c *= a + i - for b in self._parameters.bottom: - c /= b + i - if c.valuation() < 0: - raise ValueError("denominator appears in the series at the required precision") - coeffs.append(c) - return S(coeffs, prec=prec) - def is_algebraic(self): return self.is_defined() @@ -766,6 +776,10 @@ def p_curvature(self): def p_curvature_corank(self): return self._parameters.q_interlacing_number(self._p) + def dwork_image(self, r=0): + parameters = self._parameters.shift(r).dwork_image(self._p) + return self.parent()(parameters, scalar=self._scalar) + def dwork_relation(self): r""" Return (P1, h1), ..., (Ps, hs) such that @@ -777,46 +791,25 @@ def dwork_relation(self): if not self.is_defined(): raise ValueError("this hypergeometric function is not defined") - H = self.parent() p = self._p - - # We compute the series expansion up to x^p - cs = self.series(p).list() - - # We compute the relevant exponents and associated coefficients S = self.parent().polynomial_ring() - exponents = sorted([(1-b) % p for b in self._parameters.bottom]) - exponents.append(p) - Ps = [] - for i in range(len(exponents) - 1): - ei = exponents[i] - ej = exponents[i+1] - P = S(cs[ei:ej]) - if P: - Ps.append((ei, P << ei)) - - # We compute the hypergeometric series - pairs = [ ] - for r, P in Ps: - top = [ ] - for a in self.top(): - ap = (a + (-a) % p) / p # Dwork map - ar = prod(a + i for i in range(r)) - if ar % p == 0: - top.append(ap + 1) - else: - top.append(ap) - bottom = [ ] - for b in self.bottom(): - bp = (b + (-b) % p) / p # Dwork map - br = prod(b + i for i in range(r)) - if br % p == 0: - bottom.append(bp + 1) + x = S.gen() + Ps = {} + s = self.series(p) + for r in range(p): + h = self.dwork_image(r) + e = r + while not h.is_defined(): + h = h.shift(1) + e += p + if e >= s.prec(): + s = self.series(e + p) + if s[e]: + if h in Ps: + Ps[h] += s[e] * x**e else: - bottom.append(bp) - pairs.append((P, H(top, bottom))) - - return pairs + Ps[h] = s[e] * x**e + return Ps def annihilating_ore_polynomial(self, var='Frob'): # QUESTION: does this method actually return the @@ -850,7 +843,7 @@ def annihilating_ore_polynomial(self, var='Frob'): for _ in range(order): row = {} for g, P in previous_row.items(): - for Q, h in g.dwork_relation(): + for h, Q in g.dwork_relation().items(): # here g = sum(Q * h^p) if h in row: row[h] += P * insert_zeroes(Q, q) From 47c5e720a1d0a65cdd61fb3f387b9edac8701c03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20F=C3=BCrnsinn?= Date: Wed, 5 Nov 2025 17:05:23 +0100 Subject: [PATCH 29/93] (Hopefully) fixed is_defined_conjectural --- src/sage/functions/hypergeometric_algebraic.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 84eec327da8..dd4496402d6 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -138,7 +138,7 @@ def __init__(self, top, bottom, add_one=True): sage: Parameters([1/2, 1/3, 2/3], [2/3]) ((1/3, 1/2), (1,)) - We can avoid adding the trailing `1` by passing ``add_one=False``):: + We can avoid adding the trailing `1` by passing ``add_one=False``:: sage: Parameters([1/2, 1/3, 2/3], [2/3], add_one=False) ((1/3, 1/2, 1), (1,)) @@ -282,7 +282,6 @@ def interlacing_criterion(self, c): return True def q_christol_sorting(self, q): - d = self.d A = [(1/2 + (-a) % q, -1) for a in self.top] B = [(1 + (-b) % q, 1) for b in self.bottom] return sorted(A + B) @@ -292,10 +291,10 @@ def q_parenthesis_criterion(self, q): previous_paren = 1 for _, paren in self.q_christol_sorting(q): parenthesis += paren - if parenthesis < 0: + if parenthesis > 0: return False previous_paren = paren - return parenthesis >= 0 + return parenthesis <= 0 def q_interlacing_number(self, q): interlacing = 0 From 722a75ccc21e6de813c43525c6636e479002dbbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20F=C3=BCrnsinn?= Date: Wed, 5 Nov 2025 17:32:23 +0100 Subject: [PATCH 30/93] Implemented is_almost_defined() --- .../functions/hypergeometric_algebraic.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index dd4496402d6..375e6c72690 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -711,7 +711,9 @@ def series(self, prec): coeffs.append(c) return S(coeffs, prec=prec) - def is_defined(self): + + + def is_almost_defined(self): p = self._p d = self.denominator() if d.gcd(p) > 1: @@ -724,6 +726,13 @@ def is_defined(self): if not self._parameters.parenthesis_criterion(u): return False u = p*u % d + return True + + def is_defined(self): + p = self._p + d = self.denominator() + if not self.is_almost_defined(): + return False bound = self._parameters.bound if bound < p: return True @@ -737,16 +746,8 @@ def is_defined(self): def is_defined_conjectural(self): p = self._p d = self.denominator() - if d.gcd(p) > 1: + if not self.is_almost_defined(): return False - u = 1 - if not self._parameters.parenthesis_criterion(u): - return False - u = p % d - while u != 1: - if not self._parameters.parenthesis_criterion(u): - return False - u = p*u % d bound = self._parameters.bound q = p while q <= bound: From 51778cea0b5667fe1a89d60eaff6e4826e0cf2b7 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Wed, 5 Nov 2025 18:34:16 +0100 Subject: [PATCH 31/93] use is_defined_conjectural --- src/sage/functions/hypergeometric_algebraic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index dd4496402d6..e83edd92a65 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -787,7 +787,7 @@ def dwork_relation(self): """ if not self._parameters.is_balanced(): raise ValueError("the hypergeometric function must be a pFq with q = p-1") - if not self.is_defined(): + if not self.is_defined_conjectural(): raise ValueError("this hypergeometric function is not defined") p = self._p @@ -798,7 +798,7 @@ def dwork_relation(self): for r in range(p): h = self.dwork_image(r) e = r - while not h.is_defined(): + while not h.is_defined_conjectural(): h = h.shift(1) e += p if e >= s.prec(): From 9734194d8f0ab64600c55f772d70095002f64cb0 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Wed, 5 Nov 2025 22:19:37 +0100 Subject: [PATCH 32/93] implement valuation + code refactorisation --- .../functions/hypergeometric_algebraic.py | 276 ++++++++++++------ 1 file changed, 185 insertions(+), 91 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 5db91959ee1..0c61c17d4fe 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -54,6 +54,7 @@ from sage.rings.integer_ring import ZZ from sage.rings.rational_field import QQ from sage.rings.finite_rings.finite_field_constructor import FiniteField +from sage.rings.padics.padic_generic import pAdicGeneric from sage.rings.padics.factory import Qp from sage.rings.number_field.number_field import CyclotomicField @@ -286,14 +287,21 @@ def q_christol_sorting(self, q): B = [(1 + (-b) % q, 1) for b in self.bottom] return sorted(A + B) + def q_parenthesis(self, q): + parenthesis = maximum = shift = 0 + for s, paren in self.q_christol_sorting(q): + parenthesis += paren + if parenthesis > maximum: + maximum = parenthesis + shift = s + return shift, maximum + def q_parenthesis_criterion(self, q): parenthesis = 0 - previous_paren = 1 for _, paren in self.q_christol_sorting(q): parenthesis += paren if parenthesis > 0: return False - previous_paren = paren return parenthesis <= 0 def q_interlacing_number(self, q): @@ -404,19 +412,46 @@ def __init__(self, parent, arg1, arg2=None, scalar=None): Element.__init__(self, parent) base = parent.base_ring() if scalar is None: - self._scalar = base.one() + scalar = base.one() else: - self._scalar = base(scalar) - if self._scalar == 0: - self._parameters = None + scalar = base(scalar) + if scalar == 0: + parameters = None elif isinstance(arg1, HypergeometricAlgebraic): - self._parameters = arg1._parameters - self._scalar *= base(arg1._scalar) + parameters = arg1._parameters + scalar *= base(arg1._scalar) elif isinstance(arg1, Parameters): - self._parameters = arg1 + parameters = arg1 else: - self._parameters = Parameters(arg1, arg2) - self._coeffs = [self._scalar] + parameters = Parameters(arg1, arg2) + char = self.parent()._char + if char == 0: + if any(b in ZZ and b < 0 for b in parameters.bottom): + raise ValueError("the parameters %s does not define a hypergeometric function" % parameters) + else: + error = ValueError("the parameters %s does not define a hypergeometric function in characteristic %s" % (parameters, char)) + d = parameters.d + if d.gcd(char) > 1: + raise error + u = 1 + while True: + if not parameters.parenthesis_criterion(u): + raise error + u = char*u % d + if u == 1: + break + # Xavier's conjecture: + if not parameters.q_parenthesis_criterion(char): + raise error + # q = char + # while q <= parameters.bound: + # if not parameters.q_parenthesis_criterion(q): + # raise error + # q *= char + self._scalar = scalar + self._parameters = parameters + self._coeffs = [scalar] + self._char = char def __hash__(self): return hash((self.base_ring(), self._parameters, self._scalar)) @@ -480,6 +515,14 @@ def bottom(self): def scalar(self): return self._scalar + def change_ring(self, R): + H = self.parent().change_ring(R) + return H(self._parameters, None, self._scalar) + + def change_variable_name(self, name): + H = self.parent().change_variable_name(name) + return H(self._parameters, None, self._scalar) + def _add_(self, other): if self._parameters is None: return other @@ -510,8 +553,7 @@ def _sub_(self, other): def _mul_(self, other): return SR(self) * SR(other) - def series(self, prec): - S = self.parent().power_series_ring() + def _compute_coeffs(self, prec): coeffs = self._coeffs start = len(coeffs) - 1 c = coeffs[-1] @@ -521,7 +563,11 @@ def series(self, prec): for b in self._parameters.bottom: c /= b + i coeffs.append(c) - return S(coeffs, prec=prec) + + def series(self, prec): + S = self.parent().power_series_ring() + self._compute_coeffs(prec) + return S(self._coeffs, prec=prec) def shift(self, s): return self.parent()(self._parameters.shift(s), scalar=self._scalar) @@ -567,28 +613,25 @@ def derivative(self): return self.parent()(top, bottom, scalar) -class HypergeometricAlgebraic_charzero(HypergeometricAlgebraic): - def is_defined(self): - return not any(b in ZZ and b < 0 for b in self.bottom()) - +# Over the rationals -class HypergeometricAlgebraic_QQ(HypergeometricAlgebraic_charzero): - def mod(self, p): - H = HypergeometricFunctions(FiniteField(p), self.parent().variable_name()) - return H(self._parameters, None, self._scalar) +class HypergeometricAlgebraic_QQ(HypergeometricAlgebraic): + def __mod__(self, p): + k = FiniteField(p) + val = self._scalar.valuation(p) + if val == 0: + return self.change_ring(k) + h = self.change_ring(Qp(p, 1)) + return h.residue() - __mod__ = mod + def valuation(self, p): + return self.change_ring(Qp(p, 1)).valuation() def has_good_reduction(self, p): - h = self.reduce(p) - return h.is_defined() + return self.valuation(p) >= 0 def good_reduction_primes(self): r""" - Return - - (modulus, congruence_classes, exceptionnal_primes) - ALGORITHM: We rely on Christol's criterion ([CF2025]_) @@ -628,12 +671,8 @@ def good_reduction_primes(self): break if d % p == 0 or not goods[p % d]: continue - q = p - while q <= bound: - if not self._parameters.q_parenthesis_criterion(q): - exceptions[p] = False - break - q *= p + if self.valuation(p) < 0: + exceptions[p] = False goods = [c for c, v in goods.items() if v] return Primes(modulus=d, classes=goods, exceptions=exceptions) @@ -684,12 +723,72 @@ def monodromy(self, x=0, var='z'): return identity_matrix(QQ, n) def is_maximum_unipotent_monodromy(self): - # TODO: check this (maybe ask Daniel) return all(b in ZZ for b in self.bottom()) is_mum = is_maximum_unipotent_monodromy +# Over the p-adics + +class HypergeometricAlgebraic_padic(HypergeometricAlgebraic): + def __init__(self, parent, arg1, arg2=None, scalar=None): + HypergeometricAlgebraic.__init__(self, parent, arg1, arg2, scalar) + K = self.base_ring() + self._p = K.prime() + self._e = K.e() + + def residue(self): + k = self.base_ring().residue_field() + if self._scalar.valuation() == 0: + return self.change_base(k) + val, shift = self._val_pos() + if val < 0: + raise ValueError("bad reduction") + if val > 0: + H = HypergeometricFunctions(k, self.parent().variable_name()) + raise NotImplementedError("the reduction is not a hypergeometric function") + # In fact, it is x^s * h[s] * h, with + # . s = shift + # . h = self.shift(s) + + def _val_pos(self): + p = self._p + d = self.denominator() + parameters = self._parameters + if d.gcd(p) > 1: + return -infinity, None + u = 1 + if not parameters.parenthesis_criterion(u): + return -infinity, None + u = p % d + while u != 1: + if not parameters.parenthesis_criterion(u): + return -infinity, None + u = p*u % d + # From here, it is absolutely conjectural! + val = self._scalar.valuation() + pos = 0 + q = 1 + while True: + s, v = parameters.q_parenthesis(p) + if v == 0: + break + val -= self._e * v + pos += q*s + q *= p + parameters = parameters.shift(s).dwork_image(p) + return val, pos + + def valuation(self): + val, _ = self._val_pos() + return val + + def log_radius(self): + raise NotImplementedError + + +# Over prime finite fields + class HypergeometricAlgebraic_GFp(HypergeometricAlgebraic): def __init__(self, parent, arg1, arg2=None, scalar=None): HypergeometricAlgebraic.__init__(self, parent, arg1, arg2, scalar) @@ -698,23 +797,15 @@ def __init__(self, parent, arg1, arg2=None, scalar=None): def series(self, prec): S = self.parent().power_series_ring() - coeffs = self._coeffs - start = len(coeffs) - 1 - c = coeffs[-1] - for i in range(start, prec - 1): - for a in self._parameters.top: - c *= a + i - for b in self._parameters.bottom: - c /= b + i - if c.valuation() < 0: - raise ValueError("denominator appears in the series at the required precision") - coeffs.append(c) - return S(coeffs, prec=prec) - - + self._compute_coeffs(prec) + try: + f = S(self._coeffs, prec=prec) + except ValueError: + raise ValueError("denominator appears in the series at the required precision") + return f def is_almost_defined(self): - p = self._p + p = self._char d = self.denominator() if d.gcd(p) > 1: return False @@ -729,7 +820,7 @@ def is_almost_defined(self): return True def is_defined(self): - p = self._p + p = self._char d = self.denominator() if not self.is_almost_defined(): return False @@ -744,7 +835,7 @@ def is_defined(self): return True def is_defined_conjectural(self): - p = self._p + p = self._char d = self.denominator() if not self.is_almost_defined(): return False @@ -757,7 +848,7 @@ def is_defined_conjectural(self): return True def is_algebraic(self): - return self.is_defined() + return True def p_curvature(self): L = self.differential_operator() @@ -765,7 +856,7 @@ def p_curvature(self): S = OrePolynomialRing(K, L.parent().twisting_derivation().extend_to_fraction_field(), names='d') L = S(L.list()) d = S.gen() - p = self._p + p = self._char rows = [ ] n = L.degree() for i in range(p, p + n): @@ -774,11 +865,7 @@ def p_curvature(self): return matrix(rows) def p_curvature_corank(self): - return self._parameters.q_interlacing_number(self._p) - - def dwork_image(self, r=0): - parameters = self._parameters.shift(r).dwork_image(self._p) - return self.parent()(parameters, scalar=self._scalar) + return self._parameters.q_interlacing_number(self._char) def dwork_relation(self): r""" @@ -786,40 +873,41 @@ def dwork_relation(self): self = P1*h1^p + ... + Ps*hs^p """ - if not self._parameters.is_balanced(): + parameters = self._parameters + if not parameters.is_balanced(): raise ValueError("the hypergeometric function must be a pFq with q = p-1") - if not self.is_defined_conjectural(): - raise ValueError("this hypergeometric function is not defined") - - p = self._p - S = self.parent().polynomial_ring() - x = S.gen() + p = self._char + H = self.parent() + F = H.base_ring() + Hp = H.change_ring(Qp(p, 1)) + x = H.polynomial_ring().gen() + coeffs = self._coeffs Ps = {} - s = self.series(p) for r in range(p): - h = self.dwork_image(r) - e = r - while not h.is_defined_conjectural(): - h = h.shift(1) - e += p - if e >= s.prec(): - s = self.series(e + p) - if s[e]: + params = parameters.shift(r).dwork_image(p) + _, s = Hp(params)._val_pos() + h = H(params.shift(s)) + e = s*p + r + if e >= len(coeffs): + self._compute_coeffs(e + 1) + c = F(coeffs[e]) + if c: if h in Ps: - Ps[h] += s[e] * x**e + Ps[h] += c * x**e else: - Ps[h] = s[e] * x**e + Ps[h] = c * x**e return Ps def annihilating_ore_polynomial(self, var='Frob'): # QUESTION: does this method actually return the # minimal Ore polynomial annihilating self? # Probably not :-( - if not self._parameters.is_balanced(): + parameters = self._parameters + if not parameters.is_balanced(): raise NotImplementedError("the hypergeometric function is not a pFq with q = p-1") K = self.base_ring() - p = self._p + p = self._char S = self.parent().polynomial_ring() zero = S.zero() Frob = S.frobenius_endomorphism() @@ -828,9 +916,9 @@ def annihilating_ore_polynomial(self, var='Frob'): # We remove the scalar if self._scalar == 0: return Ore.one() - self = self.parent()(self._parameters) + self = self.parent()(parameters) - order = self._parameters.frobenius_order(p) + order = parameters.frobenius_order(p) bound = self.p_curvature_corank() rows = [{self: S.one()}] @@ -873,7 +961,7 @@ def annihilating_ore_polynomial(self, var='Frob'): return insert_zeroes(Ore(ker), order) def is_lucas(self): - p = self._p + p = self._char if self._parameters.frobenius_order(p) > 1: # TODO: check this return False @@ -904,17 +992,17 @@ class HypergeometricFunctions(Parent, UniqueRepresentation): def __init__(self, base, name, category=None): self._name = normalize_names(1, name)[0] self._latex_name = latex_variable_name(self._name) - char = base.characteristic() + self._char = char = base.characteristic() + if char == 0: + base = pushout(base, QQ) if base in FiniteFields() and base.is_prime_field(): self.Element = HypergeometricAlgebraic_GFp - elif char == 0: - base = pushout(base, QQ) - if base is QQ: - self.Element = HypergeometricAlgebraic_QQ - else: - self.Element = HypergeometricAlgebraic_charzero + elif base is QQ: + self.Element = HypergeometricAlgebraic_QQ + elif isinstance(base, pAdicGeneric): + self.Element = HypergeometricAlgebraic_padic else: - raise NotImplementedError("hypergeometric functions are only implemented over finite field and bases of characteristic zero") + self.Element = HypergeometricAlgebraic Parent.__init__(self, base, category=category) self.register_action(ScalarMultiplication(base, self, False, operator.mul)) self.register_action(ScalarMultiplication(base, self, True, operator.mul)) @@ -949,6 +1037,12 @@ def variable_name(self): def latex_variable_name(self): return self._latex_name + def change_ring(self, R): + return HypergeometricFunctions(R, self._name) + + def change_variable_name(self, name): + return HypergeometricFunctions(self._base, name) + def polynomial_ring(self): return PolynomialRing(self.base_ring(), self._name) From 03260c8b03ea4d39937375a37af1ca669659122c Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Thu, 6 Nov 2025 07:47:37 +0100 Subject: [PATCH 33/93] small fixes --- .../functions/hypergeometric_algebraic.py | 53 ++++++++++--------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 0c61c17d4fe..35caaf374d2 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -425,29 +425,30 @@ def __init__(self, parent, arg1, arg2=None, scalar=None): else: parameters = Parameters(arg1, arg2) char = self.parent()._char - if char == 0: - if any(b in ZZ and b < 0 for b in parameters.bottom): - raise ValueError("the parameters %s does not define a hypergeometric function" % parameters) - else: - error = ValueError("the parameters %s does not define a hypergeometric function in characteristic %s" % (parameters, char)) - d = parameters.d - if d.gcd(char) > 1: - raise error - u = 1 - while True: - if not parameters.parenthesis_criterion(u): + if scalar: + if char == 0: + if any(b in ZZ and b < 0 for b in parameters.bottom): + raise ValueError("the parameters %s does not define a hypergeometric function" % parameters) + else: + error = ValueError("the parameters %s does not define a hypergeometric function in characteristic %s" % (parameters, char)) + d = parameters.d + if d.gcd(char) > 1: raise error - u = char*u % d - if u == 1: - break - # Xavier's conjecture: - if not parameters.q_parenthesis_criterion(char): - raise error - # q = char - # while q <= parameters.bound: - # if not parameters.q_parenthesis_criterion(q): - # raise error - # q *= char + u = 1 + while True: + if not parameters.parenthesis_criterion(u): + raise error + u = char*u % d + if u == 1: + break + # Xavier's conjecture: + if not parameters.q_parenthesis_criterion(char): + raise error + # q = char + # while q <= parameters.bound: + # if not parameters.q_parenthesis_criterion(q): + # raise error + # q *= char self._scalar = scalar self._parameters = parameters self._coeffs = [scalar] @@ -741,15 +742,17 @@ def residue(self): k = self.base_ring().residue_field() if self._scalar.valuation() == 0: return self.change_base(k) - val, shift = self._val_pos() + val, pos = self._val_pos() if val < 0: raise ValueError("bad reduction") if val > 0: - H = HypergeometricFunctions(k, self.parent().variable_name()) + H = self.parent().change_ring(k) + return H(self._parameters, scalar=0) raise NotImplementedError("the reduction is not a hypergeometric function") # In fact, it is x^s * h[s] * h, with - # . s = shift + # . s = pos # . h = self.shift(s) + # Do we want to implement polynomial linear combinaison of hypergeometric functions? def _val_pos(self): p = self._p From ef372ca8c55ad695de22da84afef0e963c0f8d3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20F=C3=BCrnsinn?= Date: Thu, 6 Nov 2025 10:06:04 +0100 Subject: [PATCH 34/93] Documentation --- .../functions/hypergeometric_algebraic.py | 67 ++++++++++++++++++- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 375e6c72690..c4078c15778 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -311,6 +311,7 @@ def remove_positive_integer_differences(self): and a bottom parameter with positive integer differences are removed, starting with pairs of minimal positive integer difference. + EXAMPLES:: sage: from sage.functions.hypergeometric_algebraic import Parameters @@ -369,16 +370,66 @@ def has_negative_integer_differences(self): return any(a - b in ZZ and a < b for a in self.top for b in self.bottom) def shift(self, s): + r""" + Return the parameters obtained by adding s to each of them. + + INPUT: + + - ``s`` -- a rational number. + + EXAMPLES:: + + sage: from sage.functions.hypergeometric_algebraic import Parameters + sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: p + ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) + sage: p.shift(2) + ((1, 9/4, 7/3, 5/2), (12/5, 13/5, 3, 1)) + """ top = [a+s for a in self.top] bottom = [b+s for b in self.bottom] return Parameters(top, bottom, add_one=False) def decimal_part(self): + r""" + Return the parameters obtained by taking the decimal part of each of + the parameters, where integers are assigned 1 instead of 0. + + EXAMPLES:: + + sage: from sage.functions.hypergeometric_algebraic import Parameters + sage: p = Parameters([5/4, 1/3, 2], [2/5, -2/5]) + sage: p + ((1/3, 5/4, 2), (-2/5, 2/5, 1)) + sage: p.decimal_part() + ((1/4, 1/3, 1), (2/5, 3/5, 1)) + """ top = [1 + a - ceil(a) for a in self.top] bottom = [1 + b - ceil(b) for b in self.bottom] return Parameters(top, bottom, add_one=False) def dwork_image(self, p): + r""" + Return the parameters obtained by applying the Dwork map to each of + the parameters. The Dwork map D_p(x) of a p-adic integer x is defined + as the unique p-adic integer such that p*D_p(x) - x is a nonnegative + integer smaller than p. Raise a ValuError in case the prime is not + coprime to the common denominators of the parameters. + + INPUT: + + - ``p`` -- a prime number. + + EXAMPLE:: + + sage: from sage.functions.hypergeometric_algebraic import Parameters + sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: p + ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) + sage: p.dwork_image(7) + ((1/3, 1/2, 3/4), (1/5, 4/5, 1)) + """ + # Maybe add doctest for ValueError try: top = [(a + (-a) % p) / p for a in self.top] bottom = [(b + (-b) % p) / p for b in self.bottom] @@ -387,6 +438,20 @@ def dwork_image(self, p): return Parameters(top, bottom, add_one=False) def frobenius_order(self, p): + r""" + Return the Frobenius order of the hypergeometric function with this set + of parameters, that is the order of the Dwork map acting on the decimal + parts of the parameters. + + INPUT: + + - ``p`` -- a prime number. + + EXAMPLES:: + + sage: + + """ param = self.decimal_part() iter = param.dwork_image(p) i = 1 @@ -711,8 +776,6 @@ def series(self, prec): coeffs.append(c) return S(coeffs, prec=prec) - - def is_almost_defined(self): p = self._p d = self.denominator() From 48b9ebbda76cf25e0287d604d670814fee1f031e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20F=C3=BCrnsinn?= Date: Fri, 7 Nov 2025 14:40:21 +0100 Subject: [PATCH 35/93] documentation --- .../functions/hypergeometric_algebraic.py | 50 +++++++++++++++---- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 49e7b62d1a3..cd5dc754d39 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -225,6 +225,33 @@ def is_balanced(self): @cached_method def christol_sorting(self, c=1): + r""" + Return a sorted list of triples, where each triple is associated to one + of the parameters a, and consists of the decimal part of d*c*a (where + integers are assigned 1 instead of 0), the negative value of a, and a + sign (plus or minus 1), where top parameters are assigned -1 and bottom + parameters +1. Sorting the list lexecographically according to the + first two entries of the tuples sorts the corresponing parameters + according to the total ordering << defined on p.6 in ([Chr1986]_). + + INPUT: + + - ``c`` -- integer (default: ``1``) + + EXAMPLES:: + + sage: from sage.functions.hypergeometric_algebraic import Parameters + sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: p + ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) + sage: p.christol_sorting(7) + [(12, -3/5, 1), + (20, -1/3, -1), + (30, -1/2, -1), + (45, -1/4, -1), + (48, -2/5, 1), + (60, -1, 1)] + """ d = self.d A = [(d - (-d*c*a) % d, -a, -1) for a in self.top] B = [(d - (-d*c*b) % d, -b, 1) for b in self.bottom] @@ -251,7 +278,7 @@ def interlacing_criterion(self, c): INPUT: - - ``c`` -- an integer between 1 and ``self.d``, coprime to ``d``. + - ``c`` -- integer. EXAMPLES:: @@ -383,7 +410,7 @@ def shift(self, s): INPUT: - - ``s`` -- a rational number. + - ``s`` -- rational number. EXAMPLES:: @@ -419,21 +446,21 @@ def decimal_part(self): def dwork_image(self, p): r""" Return the parameters obtained by applying the Dwork map to each of - the parameters. The Dwork map D_p(x) of a p-adic integer x is defined + the parameters. The Dwork map D_p(x) of a p-adic integer x is defined as the unique p-adic integer such that p*D_p(x) - x is a nonnegative - integer smaller than p. Raise a ValuError in case the prime is not + integer smaller than p. Raise a ValuError in case the prime is not coprime to the common denominators of the parameters. INPUT: - - ``p`` -- a prime number. + - ``p`` -- prime number. EXAMPLE:: sage: from sage.functions.hypergeometric_algebraic import Parameters sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) sage: p - ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) + ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) sage: p.dwork_image(7) ((1/3, 1/2, 3/4), (1/5, 4/5, 1)) """ @@ -453,12 +480,17 @@ def frobenius_order(self, p): INPUT: - - ``p`` -- a prime number. + - ``p`` -- prime number. EXAMPLES:: - sage: - + sage: from sage.functions.hypergeometric_algebraic import Parameters + sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: p + ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) + sage: p.frobenius_order(7) + 2 + """ param = self.decimal_part() iter = param.dwork_image(p) From 489adc9515884148a766889b6f9f5e5bdea6c9ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20F=C3=BCrnsinn?= Date: Fri, 7 Nov 2025 16:45:15 +0100 Subject: [PATCH 36/93] Alsmost complete documentation for class Parameters --- .../functions/hypergeometric_algebraic.py | 123 +++++++++++++++++- 1 file changed, 121 insertions(+), 2 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index cd5dc754d39..7a7287f14f6 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -258,6 +258,41 @@ def christol_sorting(self, c=1): return sorted(A + B) def parenthesis_criterion(self, c): + r""" + Return ``True`` if in each prefix of the list + ``self.christol_sorting(c)`` there are at least as many triples with + third entry -1 as triples with third entry +1. Return ``False`` + otherwise. + + INPUT: + + - ``c`` -- an integer + + EXAMPLES:: + + sage: from sage.functions.hypergeometric_algebraic import Parameters + sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: p + ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) + sage: p.christol_sorting(7) + [(12, -3/5, 1), + (20, -1/3, -1), + (30, -1/2, -1), + (45, -1/4, -1), + (48, -2/5, 1), + (60, -1, 1)] + sage: p.parenthesis_criterion(7) + False + sage: p.christol_sorting(1) + [(15, -1/4, -1), + (20, -1/3, -1), + (24, -2/5, 1), + (30, -1/2, -1), + (36, -3/5, 1), + (60, -1, 1)] + sage: p.parenthesis_criterion(1) + True + """ parenthesis = 0 previous_paren = 1 for _, _, paren in self.christol_sorting(c): @@ -310,11 +345,50 @@ def interlacing_criterion(self, c): return True def q_christol_sorting(self, q): + r""" + Return a sorted list of pairs, one associated to each top parameter a, + and one associated to each bottom parameter b where the pair is either + (1/2 + (-a) % q, -1) or (1 + (-b) % q, 1). + + INPUT: + + - ``q`` -- integer + + EXAMPLES:: + + sage: from sage.functions.hypergeometric_algebraic import Parameters + sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: p + ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) + sage: p.q_christol_sorting(7) + [(2, 1), (2.5, -1), (3.5, -1), (5.5, -1), (6, 1), (7, 1)] + """ A = [(1/2 + (-a) % q, -1) for a in self.top] B = [(1 + (-b) % q, 1) for b in self.bottom] return sorted(A + B) def q_parenthesis(self, q): + r""" + Return maximal value of the sum of all the second entries of the pairs + in a prefix of ``self.q_christol_sorting(q)`` and the first entry of + the last pair in the prefix of smallest length where this value is + attained. + + INPUT: + + - ``q`` -- integer. + + EXAMPLES:: + + sage: from sage.functions.hypergeometric_algebraic import Parameters + sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: p + ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) + sage: p.q_christol_sorting(7) + [(2, 1), (2.5, -1), (3.5, -1), (5.5, -1), (6, 1), (7, 1)] + sage: p.q_parenthesis(7) + (2, 1) + """ parenthesis = maximum = shift = 0 for s, paren in self.q_christol_sorting(q): parenthesis += paren @@ -324,6 +398,31 @@ def q_parenthesis(self, q): return shift, maximum def q_parenthesis_criterion(self, q): + r""" + Return ``True`` if in each prefix of the list + ``self.q_christol_sorting(q)`` there are at least as many pairs with + second entry -1 as pairs with second entry +1. Return ``False`` + otherwise. + + INPUT: + + - ``q`` -- integer + + EXAMPLES:: + + sage: from sage.functions.hypergeometric_algebraic import Parameters + sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: p + ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) + sage: p.q_christol_sorting(7) + [(2, 1), (2.5, -1), (3.5, -1), (5.5, -1), (6, 1), (7, 1)] + sage: p.q_parenthesis_criterion(7) + False + sage: p.q_christol_sorting(61) + [(15.5, -1), (20.5, -1), (25, 1), (30.5, -1), (37, 1), (61, 1)] + sage: p.q_parenthesis_criterion(61) + True + """ parenthesis = 0 for _, paren in self.q_christol_sorting(q): parenthesis += paren @@ -332,6 +431,26 @@ def q_parenthesis_criterion(self, q): return parenthesis <= 0 def q_interlacing_number(self, q): + r""" + Return the number of pairs in the list ``self.q_christol_sorting(q)`` + with second entry 1, that were preceded by a pair with second entry + -1. + + INPUT: + + - ``q`` -- integer. + + EXAMPLES:: + + sage: from sage.functions.hypergeometric_algebraic import Parameters + sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: p + ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) + sage: p.q_christol_sorting(7) + [(2, 1), (2.5, -1), (3.5, -1), (5.5, -1), (6, 1), (7, 1)] + sage: p.q_interlacing_number(7) + 1 + """ interlacing = 0 previous_paren = 1 for _, paren in self.q_christol_sorting(q): @@ -525,9 +644,9 @@ def __init__(self, parent, arg1, arg2=None, scalar=None): if scalar: if char == 0: if any(b in ZZ and b < 0 for b in parameters.bottom): - raise ValueError("the parameters %s does not define a hypergeometric function" % parameters) + raise ValueError("the parameters %s do not define a hypergeometric function" % parameters) else: - error = ValueError("the parameters %s does not define a hypergeometric function in characteristic %s" % (parameters, char)) + error = ValueError("the parameters %s do not define a hypergeometric function in characteristic %s" % (parameters, char)) d = parameters.d if d.gcd(char) > 1: raise error From 7fd40de02c0ad0cbdcce588b2e575542e625c5d9 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Sun, 9 Nov 2025 08:43:29 +0100 Subject: [PATCH 37/93] small edits in the documentation --- .../functions/hypergeometric_algebraic.py | 181 +++++++++--------- 1 file changed, 93 insertions(+), 88 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 7a7287f14f6..90a89df2a5c 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -127,10 +127,10 @@ def __init__(self, top, bottom, add_one=True): EXAMPLES:: sage: from sage.functions.hypergeometric_algebraic import Parameters - sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) - sage: p + sage: pa = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: pa ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) - sage: type(p) + sage: type(pa) By default, parameters are sorted, duplicates are removed and @@ -186,8 +186,8 @@ def __repr__(self): EXAMPLES:: sage: from sage.functions.hypergeometric_algebraic import Parameters - sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) - sage: p # indirect doctest + sage: pa = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: pa # indirect doctest ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) """ return "(%s, %s)" % (self.top, self.bottom) @@ -207,18 +207,18 @@ def is_balanced(self): EXAMPLES:: sage: from sage.functions.hypergeometric_algebraic import Parameters - sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) - sage: p + sage: pa = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: pa ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) - sage: p.is_balanced() + sage: pa.is_balanced() True :: - sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/5], add_one=False) - sage: p + sage: pa = Parameters([1/4, 1/3, 1/2], [2/5, 3/5], add_one=False) + sage: pa ((1/4, 1/3, 1/2, 1), (2/5, 3/5, 1)) - sage: p.is_balanced() + sage: pa.is_balanced() False """ return len(self.top) == len(self.bottom) @@ -232,26 +232,26 @@ def christol_sorting(self, c=1): sign (plus or minus 1), where top parameters are assigned -1 and bottom parameters +1. Sorting the list lexecographically according to the first two entries of the tuples sorts the corresponing parameters - according to the total ordering << defined on p.6 in ([Chr1986]_). + according to the total ordering (defined on p.6 in [Chr1986]_). INPUT: - - ``c`` -- integer (default: ``1``) + - ``c`` -- an integer (default: ``1``) EXAMPLES:: sage: from sage.functions.hypergeometric_algebraic import Parameters - sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) - sage: p + sage: pa = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: pa ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) - sage: p.christol_sorting(7) + sage: pa.christol_sorting(7) [(12, -3/5, 1), (20, -1/3, -1), (30, -1/2, -1), (45, -1/4, -1), (48, -2/5, 1), (60, -1, 1)] - """ + """ d = self.d A = [(d - (-d*c*a) % d, -a, -1) for a in self.top] B = [(d - (-d*c*b) % d, -b, 1) for b in self.bottom] @@ -266,31 +266,31 @@ def parenthesis_criterion(self, c): INPUT: - - ``c`` -- an integer + - ``c`` -- an integer EXAMPLES:: sage: from sage.functions.hypergeometric_algebraic import Parameters - sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) - sage: p + sage: pa = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: pa ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) - sage: p.christol_sorting(7) + sage: pa.christol_sorting(7) [(12, -3/5, 1), (20, -1/3, -1), (30, -1/2, -1), (45, -1/4, -1), (48, -2/5, 1), (60, -1, 1)] - sage: p.parenthesis_criterion(7) + sage: pa.parenthesis_criterion(7) False - sage: p.christol_sorting(1) + sage: pa.christol_sorting(1) [(15, -1/4, -1), (20, -1/3, -1), (24, -2/5, 1), (30, -1/2, -1), (36, -3/5, 1), (60, -1, 1)] - sage: p.parenthesis_criterion(1) + sage: pa.parenthesis_criterion(1) True """ parenthesis = 0 @@ -313,28 +313,28 @@ def interlacing_criterion(self, c): INPUT: - - ``c`` -- integer. + - ``c`` -- an integer EXAMPLES:: sage: from sage.functions.hypergeometric_algebraic import Parameters - sage: p = Parameters([1/3, 2/3], [1/2]) - sage: p + sage: pa = Parameters([1/3, 2/3], [1/2]) + sage: pa ((1/3, 2/3), (1/2, 1)) - sage: p.interlacing_criterion(1) + sage: pa.interlacing_criterion(1) True - sage: p.interlacing_criterion(5) + sage: pa.interlacing_criterion(5) True :: sage: from sage.functions.hypergeometric_algebraic import Parameters - sage: p = Parameters([1/8, 3/8, 5/8], [1/4, 1/2]) - sage: p + sage: pa = Parameters([1/8, 3/8, 5/8], [1/4, 1/2]) + sage: pa ((1/8, 3/8, 5/8), (1/4, 1/2, 1)) - sage: p.interlacing_criterion(1) + sage: pa.interlacing_criterion(1) True - sage: p.interlacing_criterion(3) + sage: pa.interlacing_criterion(3) False """ previous_paren = 1 @@ -352,15 +352,15 @@ def q_christol_sorting(self, q): INPUT: - - ``q`` -- integer + - ``q`` -- an integer EXAMPLES:: sage: from sage.functions.hypergeometric_algebraic import Parameters - sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) - sage: p + sage: pa = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: pa ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) - sage: p.q_christol_sorting(7) + sage: pa.q_christol_sorting(7) [(2, 1), (2.5, -1), (3.5, -1), (5.5, -1), (6, 1), (7, 1)] """ A = [(1/2 + (-a) % q, -1) for a in self.top] @@ -370,23 +370,23 @@ def q_christol_sorting(self, q): def q_parenthesis(self, q): r""" Return maximal value of the sum of all the second entries of the pairs - in a prefix of ``self.q_christol_sorting(q)`` and the first entry of + in a prefix of ``self.q_christol_sorting(q)`` and the first entry of the last pair in the prefix of smallest length where this value is attained. INPUT: - - ``q`` -- integer. + - ``q`` -- an integer EXAMPLES:: sage: from sage.functions.hypergeometric_algebraic import Parameters - sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) - sage: p + sage: pa = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: pa ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) - sage: p.q_christol_sorting(7) + sage: pa.q_christol_sorting(7) [(2, 1), (2.5, -1), (3.5, -1), (5.5, -1), (6, 1), (7, 1)] - sage: p.q_parenthesis(7) + sage: pa.q_parenthesis(7) (2, 1) """ parenthesis = maximum = shift = 0 @@ -406,21 +406,21 @@ def q_parenthesis_criterion(self, q): INPUT: - - ``q`` -- integer + - ``q`` -- an integer EXAMPLES:: sage: from sage.functions.hypergeometric_algebraic import Parameters - sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) - sage: p + sage: pa = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: pa ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) - sage: p.q_christol_sorting(7) + sage: pa.q_christol_sorting(7) [(2, 1), (2.5, -1), (3.5, -1), (5.5, -1), (6, 1), (7, 1)] - sage: p.q_parenthesis_criterion(7) + sage: pa.q_parenthesis_criterion(7) False - sage: p.q_christol_sorting(61) + sage: pa.q_christol_sorting(61) [(15.5, -1), (20.5, -1), (25, 1), (30.5, -1), (37, 1), (61, 1)] - sage: p.q_parenthesis_criterion(61) + sage: pa.q_parenthesis_criterion(61) True """ parenthesis = 0 @@ -438,17 +438,17 @@ def q_interlacing_number(self, q): INPUT: - - ``q`` -- integer. + - ``q`` -- an integer. EXAMPLES:: sage: from sage.functions.hypergeometric_algebraic import Parameters - sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) - sage: p + sage: pa = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: pa ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) - sage: p.q_christol_sorting(7) + sage: pa.q_christol_sorting(7) [(2, 1), (2.5, -1), (3.5, -1), (5.5, -1), (6, 1), (7, 1)] - sage: p.q_interlacing_number(7) + sage: pa.q_interlacing_number(7) 1 """ interlacing = 0 @@ -469,19 +469,19 @@ def remove_positive_integer_differences(self): EXAMPLES:: sage: from sage.functions.hypergeometric_algebraic import Parameters - sage: p = Parameters([5/2, -1/2, 5/3], [3/2, 1/3]) - sage: p + sage: pa = Parameters([5/2, -1/2, 5/3], [3/2, 1/3]) + sage: pa ((-1/2, 5/3, 5/2), (1/3, 3/2, 1)) - sage: p.remove_positive_integer_differences() + sage: pa.remove_positive_integer_differences() ((-1/2, 5/3), (1/3, 1)) The choice of which pair with integer differences to remove first is important:: - sage: p = Parameters([4, 2, 1/2], [1, 3]) - sage: p + sage: pa = Parameters([4, 2, 1/2], [1, 3]) + sage: pa ((1/2, 2, 4), (1, 3, 1)) - sage: p.remove_positive_integer_differences() + sage: pa.remove_positive_integer_differences() ((1/2,), (1,)) """ differences = [] @@ -507,18 +507,18 @@ def has_negative_integer_differences(self): EXAMPLES:: sage: from sage.functions.hypergeometric_algebraic import Parameters - sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) - sage: p + sage: pa = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: pa ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) - sage: p.has_negative_integer_differences() + sage: pa.has_negative_integer_differences() False :: - sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/2]) - sage: p + sage: pa = Parameters([1/4, 1/3, 1/2], [2/5, 3/2]) + sage: pa ((1/4, 1/3, 1/2), (2/5, 3/2, 1)) - sage: p.has_negative_integer_differences() + sage: pa.has_negative_integer_differences() True """ return any(a - b in ZZ and a < b for a in self.top for b in self.bottom) @@ -529,15 +529,15 @@ def shift(self, s): INPUT: - - ``s`` -- rational number. + - ``s`` -- a rational number EXAMPLES:: sage: from sage.functions.hypergeometric_algebraic import Parameters - sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) - sage: p + sage: pa = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: pa ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) - sage: p.shift(2) + sage: pa.shift(2) ((1, 9/4, 7/3, 5/2), (12/5, 13/5, 3, 1)) """ top = [a+s for a in self.top] @@ -552,10 +552,10 @@ def decimal_part(self): EXAMPLES:: sage: from sage.functions.hypergeometric_algebraic import Parameters - sage: p = Parameters([5/4, 1/3, 2], [2/5, -2/5]) - sage: p + sage: pa = Parameters([5/4, 1/3, 2], [2/5, -2/5]) + sage: pa ((1/3, 5/4, 2), (-2/5, 2/5, 1)) - sage: p.decimal_part() + sage: pa.decimal_part() ((1/4, 1/3, 1), (2/5, 3/5, 1)) """ top = [1 + a - ceil(a) for a in self.top] @@ -565,25 +565,31 @@ def decimal_part(self): def dwork_image(self, p): r""" Return the parameters obtained by applying the Dwork map to each of - the parameters. The Dwork map D_p(x) of a p-adic integer x is defined - as the unique p-adic integer such that p*D_p(x) - x is a nonnegative - integer smaller than p. Raise a ValuError in case the prime is not - coprime to the common denominators of the parameters. + the parameters. The Dwork map `D_p(x)` of a p-adic integer x is defined + as the unique p-adic integer such that `p D_p(x) - x` is a nonnegative + integer smaller than p. INPUT: - - ``p`` -- prime number. + - ``p`` -- a prime number EXAMPLE:: sage: from sage.functions.hypergeometric_algebraic import Parameters - sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) - sage: p + sage: pa = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: pa ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) - sage: p.dwork_image(7) + sage: pa.dwork_image(7) ((1/3, 1/2, 3/4), (1/5, 4/5, 1)) + + If `p` is not coprime to the common denominators of the parameters, + a ``ValueError`` is raised:: + + sage: pa.dwork_image(3) + Traceback (most recent call last): + ... + ValueError: denominators of parameters are not coprime to p """ - # Maybe add doctest for ValueError try: top = [(a + (-a) % p) / p for a in self.top] bottom = [(b + (-b) % p) / p for b in self.bottom] @@ -599,17 +605,16 @@ def frobenius_order(self, p): INPUT: - - ``p`` -- prime number. + - ``p`` -- a prime number EXAMPLES:: sage: from sage.functions.hypergeometric_algebraic import Parameters - sage: p = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) - sage: p + sage: pa = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: pa ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) - sage: p.frobenius_order(7) + sage: pa.frobenius_order(7) 2 - """ param = self.decimal_part() iter = param.dwork_image(p) From 102705a8f42fba11c28eb3d36a5ba50813cb9bec Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Sun, 9 Nov 2025 09:00:17 +0100 Subject: [PATCH 38/93] some features we may want to include --- .../functions/hypergeometric_algebraic.py | 40 +++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 90a89df2a5c..d44afdfe5d5 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -628,6 +628,14 @@ def frobenius_order(self, p): # Hypergeometric functions ########################## +# Do we want to implement polynomial linear combinaison +# of hypergeometric functions? +# Advantages: +# . reductions mod p of hypergeometric functions have this form in general +# . many methods can be extended to this context +# Difficulty: +# . not sure we can handle easily simplifications! + class HypergeometricAlgebraic(Element): def __init__(self, parent, arg1, arg2=None, scalar=None): Element.__init__(self, parent) @@ -775,6 +783,9 @@ def _sub_(self, other): def _mul_(self, other): return SR(self) * SR(other) + def __call__(self, x): + return SR(self)(x) + def _compute_coeffs(self, prec): coeffs = self._coeffs start = len(coeffs) - 1 @@ -921,6 +932,9 @@ def is_globally_bounded(self, include_infinity=True): return False return True + def p_curvature_ranks(self): + raise NotImplementedError + def monodromy(self, x=0, var='z'): params = self._parameters if not params.is_balanced(): @@ -973,7 +987,6 @@ def residue(self): # In fact, it is x^s * h[s] * h, with # . s = pos # . h = self.shift(s) - # Do we want to implement polynomial linear combinaison of hypergeometric functions? def _val_pos(self): p = self._p @@ -1007,7 +1020,13 @@ def valuation(self): val, _ = self._val_pos() return val - def log_radius(self): + def radius_of_convergence(self): + raise NotImplementedError + + def newton_polygon(self): + raise NotImplementedError + + def __call__(self, x): raise NotImplementedError @@ -1015,6 +1034,8 @@ def log_radius(self): class HypergeometricAlgebraic_GFp(HypergeometricAlgebraic): def __init__(self, parent, arg1, arg2=None, scalar=None): + # TODO: do we want to simplify automatically if the + # hypergeometric series is a polynomial? HypergeometricAlgebraic.__init__(self, parent, arg1, arg2, scalar) self._p = p = self.base_ring().cardinality() self._coeffs = [Qp(p, 1)(self._scalar)] @@ -1071,6 +1092,18 @@ def is_defined_conjectural(self): q *= p return True + def __call__(self, x): + return self.polynomial()(x) + + def is_polynomial(self): + raise NotImplementedError + + def degree(self): + raise NotImplementedError + + def polynomial(self): + raise NotImplementedError + def is_algebraic(self): return True @@ -1088,7 +1121,8 @@ def p_curvature(self): rows.append([Li[j] for j in range(n)]) return matrix(rows) - def p_curvature_corank(self): + def p_curvature_corank(self): # maybe p_curvature_rank is preferable? + # TODO: check if it is also correct when the parameters are not balanced return self._parameters.q_interlacing_number(self._char) def dwork_relation(self): From 03a6b5e092ace69f91064fdc437623be91a6365e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20F=C3=BCrnsinn?= Date: Mon, 10 Nov 2025 17:30:55 +0100 Subject: [PATCH 39/93] documentation, fixed formal difference --- .../functions/hypergeometric_algebraic.py | 254 +++++++++++++++++- 1 file changed, 253 insertions(+), 1 deletion(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index d44afdfe5d5..71dba84e62e 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -637,7 +637,24 @@ def frobenius_order(self, p): # . not sure we can handle easily simplifications! class HypergeometricAlgebraic(Element): + r""" + Class for hypergeometric functions over arbitrary base rings. + """ def __init__(self, parent, arg1, arg2=None, scalar=None): + r""" + Initialize the hypergeometric function. + + INPUT: + + - ``parent`` -- + + - ``arg1`` -- + + - ``arg 2`` -- + + - ``scalar`` -- + + """ Element.__init__(self, parent) base = parent.base_ring() if scalar is None: @@ -734,26 +751,134 @@ def _latex_(self): return s def base_ring(self): + r""" + Return the ring over which the hypergeometric function is defined. + + EXAMPLES:: + + sage: S. = QQ[] + sage: T. = Qp(5)[] + sage: U. = GF(5)[] + sage: V. = CC[] + sage: f, g, h, k = hypergeometric([1/3, 2/3], [1/2], x), hypergeometric([1/3, 2/3], [1/2], y), hypergeometric([1/3, 2/3], [1/2], z), hypergeometric([1/3, 2/3], [1/2], w) + sage: f.base_ring() + Rational Field + sage: g.base_ring() + 5-adic Field with capped relative precision 20 + sage: h.base_ring() + Finite Field of size 5 + sage: k.base_ring() + Complex Field with 53 bits of precision + """ return self.parent().base_ring() def top(self): + r""" + Return the top parameters of the hypergeometric function + + EXAMPLES:: + + sage: S. = QQ[] + sage: f = hypergeometric([1/3, 2/3], [1/2], x) + sage: f.top() + (1/3, 2/3) + """ return self._parameters.top def bottom(self): + r""" + Return the bottom parameters of the hypergeometric function (excluding + the extra ``1``) + + EXAMPLES:: + + sage: S. = QQ[] + sage: f = hypergeometric([1/3, 2/3], [1/2], x) + sage: f.bottom() + (1/2,) + """ return self._parameters.bottom[:-1] def scalar(self): + r""" + Return the scalar of the hypergeometric function + + EXAMPLES:: + + sage: S. = QQ[] + sage: f = hypergeometric([1/3, 2/3], [1/2], x) + sage: f.scalar() + 1 + sage: g = 4*f + sage: g.scalar() + 4 + """ return self._scalar def change_ring(self, R): + r""" + Return the hypergeometric function with changed base ring + + INPUT: + + - ``R`` -- a ring + + EXAMPLES:: + + sage: S. = QQ[] + sage: f = hypergeometric([1/3, 2/3], [1/2], x) + sage: f.base_ring() + Rational Field + sage: g = f.change_ring(Qp(5)) + sage: g.base_ring() + 5-adic Field with capped relative precision 20 + """ H = self.parent().change_ring(R) return H(self._parameters, None, self._scalar) def change_variable_name(self, name): + r""" + Return the hypergeometric function with changed variable name + + INPUT: + + - ``name`` -- string containing the new variable name + + EXAMPLES:: + + sage: S. = Qp(5)[] + sage: T. = Qp(5)[] + sage: f = hypergeometric([1/3, 2/3], [1/2], x) + sage: f + hypergeometric((1/3, 2/3), (1/2,), x) + sage: g = f.change_variable_name('y') + sage: g + hypergeometric((1/3, 2/3), (1/2,), y) + """ H = self.parent().change_variable_name(name) return H(self._parameters, None, self._scalar) def _add_(self, other): + r""" + Return the (formal) sum of the hypergeometric function and another, + defined over the same ring. + + INPUT: + + - ``other`` -- a hypergeometric function defined over the same + ring. + + EXAMPLES:: + + sage: S. = QQ[] + sage: f = hypergeometric([1/3, 2/3], [1/2], x) + sage: g = 1/2 * hypergeometric([1/3, 2/3], [1/2], x) + sage: h = hypergeometric([1/5, 2/5], [3/5], x) + sage: f + g + 3/2*hypergeometric((1/3, 2/3), (1/2,), x) + sage: f + h + hypergeometric((1/3, 2/3), (1/2,), x) + hypergeometric((1/5, 2/5), (3/5,), x) + """ if self._parameters is None: return other if isinstance(other, HypergeometricAlgebraic): @@ -765,11 +890,41 @@ def _add_(self, other): return SR(self) + SR(other) def _neg_(self): + r""" + Return the negative of the hypergeometric function. + + EXAMPLES:: + + sage: S. = QQ[] + sage: f = 2*hypergeometric([1/3, 2/3], [1/2], x) + sage: -f + -2*hypergeometric((1/3, 2/3), (1/2,), x) + """ if self._parameters is None: return self return self.parent()(self._parameters, scalar=-self._scalar) def _sub_(self, other): + r""" + Return the (formal) difference of the hypergeometric function with + another, defined over the same ring. + + INPUT: + + - ``other`` -- a hypergeometric function defined over the same + ring. + + EXAMPLES:: + + sage: S. = QQ[] + sage: f = hypergeometric([1/3, 2/3], [1/2], x) + sage: g = 1/2 * hypergeometric([1/3, 2/3], [1/2], x) + sage: h = hypergeometric([1/5, 2/5], [3/5], x) + sage: f - g + 1/2*hypergeometric((1/3, 2/3), (1/2,), x) + sage: f - h + hypergeometric((1/3, 2/3), (1/2,), x) - hypergeometric((1/5, 2/5), (3/5,), x) + """ if self._parameters is None: return other if isinstance(other, HypergeometricAlgebraic): @@ -778,15 +933,54 @@ def _sub_(self, other): if self._parameters == other._parameters: scalar = self._scalar - other._scalar return self.parent()(self._parameters, scalar=scalar) - return SR(self) + SR(other) + return SR(self) - SR(other) def _mul_(self, other): + r""" + Return the (formal) product of the hypergeometric function and + another, defined over the same ring. + + INPUT: + + - ``other`` -- a hypergeometric function defined over the same + ring. + + EXAMPLES:: + + sage: S. = QQ[] + sage: f = hypergeometric([1/3, 2/3], [1/2], x) + sage: g = 1/2 * hypergeometric([1/3, 2/3], [1/2], x) + sage: h = hypergeometric([1/5, 2/5], [3/5], x) + sage: f*g + 1/2*hypergeometric((1/3, 2/3), (1/2,), x)^2 + sage: f*h + hypergeometric((1/3, 2/3), (1/2,), x)*hypergeometric((1/5, 2/5), (3/5,), x) + """ return SR(self) * SR(other) def __call__(self, x): return SR(self)(x) def _compute_coeffs(self, prec): + r""" + Compute the coefficients of the series representation of the + hypergeometric function up to a given precision, and append them to + ``self._coeffs``. + + INPUT: + + - ``prec`` -- a positive integer. + + EXAMPLES:: + + sage: S. = QQ[] + sage: f = hypergeometric([1/3, 2/3], [1/2], x) + sage: f._coeffs + [1] + sage: f._compute_coeffs(3) + sage: f._coeffs + [1, 4/9, 80/243] + """ coeffs = self._coeffs start = len(coeffs) - 1 c = coeffs[-1] @@ -798,11 +992,42 @@ def _compute_coeffs(self, prec): coeffs.append(c) def series(self, prec): + r""" + Return the power series representation of the hypergeometric function + up to a given precision. + + INPUT: + + - ``prec`` -- a positive integer. + + EXAMPLES:: + + sage: S. = QQ[] + sage: f = hypergeometric([1/3, 2/3], [1/2], x) + sage: f.series(3) + 1 + 4/9*x + 80/243*x^2 + O(x^3) + """ S = self.parent().power_series_ring() self._compute_coeffs(prec) return S(self._coeffs, prec=prec) def shift(self, s): + r""" + Return the hypergeometric function, where each parameter (including the + addional ``1`` as a bottom parameter) is increased by ``s``. + + INPUT: + + - ``s`` -- a rational number. + + EXAMPLES:: + + sage: S. = QQ[] + sage: f = hypergeometric([1/3, 2/3], [1/2], x) + sage: g = f.shift(3/2) + sage: g + hypergeometric((1, 11/6, 13/6), (2, 5/2), x) + """ return self.parent()(self._parameters.shift(s), scalar=self._scalar) @coerce_binop @@ -850,6 +1075,33 @@ def derivative(self): class HypergeometricAlgebraic_QQ(HypergeometricAlgebraic): def __mod__(self, p): + r""" + Return the reduction of the hypergeometric function modulo ``p``. + + INPUT: + + - ``p`` -- a prime number. + + EXAMPLES:: + + sage: from sage.functions.hypergeometric_algebraic import HypergeometricAlgebraic_QQ + sage: from sage.functions.hypergeometric_algebraic import HypergeometricAlgebraic_padic + sage: from sage.functions.hypergeometric_algebraic import HypergeometricAlgebraic_GFp + sage: S. = QQ[] + sage: f = hypergeometric([1/3, 2/3], [1/2], x) + sage: f + hypergeometric((1/3, 2/3), (1/2,), x) + sage: f.parent() + Hypergeometric functions in x over Rational Field + sage: g = f % 5 + sage: g + hypergeometric((1/3, 2/3), (1/2,), x) + sage: g.parent() + Hypergeometric functions in x over Finite Field of size 5 + sage: h = f.__mod__(5) + sage: h == g + True + """ k = FiniteField(p) val = self._scalar.valuation(p) if val == 0: From dee6c7a60e67a1cc320a5c9a74d43cfac0a7dd66 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Tue, 11 Nov 2025 09:56:29 +0100 Subject: [PATCH 40/93] small reformatting --- .../functions/hypergeometric_algebraic.py | 191 ++++++++++++------ 1 file changed, 125 insertions(+), 66 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 71dba84e62e..f34baf47972 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -642,18 +642,28 @@ class HypergeometricAlgebraic(Element): """ def __init__(self, parent, arg1, arg2=None, scalar=None): r""" - Initialize the hypergeometric function. + Initialize this hypergeometric function. INPUT: - - ``parent`` -- + - ``parent`` -- the parent of this function - - ``arg1`` -- + - ``arg1``, ``arg2`` -- arguments defining this hypergeometric + function, they can be: + - the top and bottom paramters + - a hypergeometric function and ``None`` + - an instance of the class :class:`Parameters` and ``None`` - - ``arg 2`` -- - - - ``scalar`` -- - + - ``scalar`` -- an element in the base ring, the scalar by + which the hypergeometric function is multiplied + + TESTS:: + + sage: S. = QQ[] + sage: h = hypergeometric((1/2, 1/3), (1,), x) + sage: type(h) + + sage: TestSuite(h).run() """ Element.__init__(self, parent) base = parent.base_ring() @@ -752,21 +762,33 @@ def _latex_(self): def base_ring(self): r""" - Return the ring over which the hypergeometric function is defined. + Return the ring over which this hypergeometric function is defined. EXAMPLES:: sage: S. = QQ[] - sage: T. = Qp(5)[] - sage: U. = GF(5)[] - sage: V. = CC[] - sage: f, g, h, k = hypergeometric([1/3, 2/3], [1/2], x), hypergeometric([1/3, 2/3], [1/2], y), hypergeometric([1/3, 2/3], [1/2], z), hypergeometric([1/3, 2/3], [1/2], w) + sage: f = hypergeometric([1/3, 2/3], [1/2], x) sage: f.base_ring() Rational Field + + :: + + sage: T. = Qp(5)[] + sage: g = hypergeometric([1/3, 2/3], [1/2], y) sage: g.base_ring() 5-adic Field with capped relative precision 20 + + :: + + sage: U. = GF(5)[] + sage: h = hypergeometric([1/3, 2/3], [1/2], z) sage: h.base_ring() Finite Field of size 5 + + :: + + sage: V. = CC[] + sage: k = hypergeometric([1/3, 2/3], [1/2], w) sage: k.base_ring() Complex Field with 53 bits of precision """ @@ -774,7 +796,7 @@ def base_ring(self): def top(self): r""" - Return the top parameters of the hypergeometric function + Return the top parameters of this hypergeometric function. EXAMPLES:: @@ -787,8 +809,8 @@ def top(self): def bottom(self): r""" - Return the bottom parameters of the hypergeometric function (excluding - the extra ``1``) + Return the bottom parameters of this hypergeometric function (excluding + the extra ``1``). EXAMPLES:: @@ -801,7 +823,7 @@ def bottom(self): def scalar(self): r""" - Return the scalar of the hypergeometric function + Return the scalar of this hypergeometric function. EXAMPLES:: @@ -817,11 +839,11 @@ def scalar(self): def change_ring(self, R): r""" - Return the hypergeometric function with changed base ring + Return this hypergeometric function with changed base ring INPUT: - - ``R`` -- a ring + - ``R`` -- a commutative ring EXAMPLES:: @@ -838,11 +860,11 @@ def change_ring(self, R): def change_variable_name(self, name): r""" - Return the hypergeometric function with changed variable name + Return this hypergeometric function with changed variable name INPUT: - - ``name`` -- string containing the new variable name + - ``name`` -- a string, the new variable name EXAMPLES:: @@ -860,13 +882,12 @@ def change_variable_name(self, name): def _add_(self, other): r""" - Return the (formal) sum of the hypergeometric function and another, - defined over the same ring. + Return the (formal) sum of the hypergeometric function + and ``other``. INPUT: - - ``other`` -- a hypergeometric function defined over the same - ring. + - ``other`` -- a hypergeometric function EXAMPLES:: @@ -878,6 +899,11 @@ def _add_(self, other): 3/2*hypergeometric((1/3, 2/3), (1/2,), x) sage: f + h hypergeometric((1/3, 2/3), (1/2,), x) + hypergeometric((1/5, 2/5), (3/5,), x) + + :: + + sage: f + cos(x) + hypergeometric((1/3, 2/3), (1/2,), x) + cos(x) """ if self._parameters is None: return other @@ -891,7 +917,7 @@ def _add_(self, other): def _neg_(self): r""" - Return the negative of the hypergeometric function. + Return the negative of this hypergeometric function. EXAMPLES:: @@ -906,13 +932,12 @@ def _neg_(self): def _sub_(self, other): r""" - Return the (formal) difference of the hypergeometric function with - another, defined over the same ring. + Return the (formal) difference of the hypergeometric function + with ``other``. INPUT: - - ``other`` -- a hypergeometric function defined over the same - ring. + - ``other`` -- a hypergeometric function or a formal expression EXAMPLES:: @@ -924,6 +949,11 @@ def _sub_(self, other): 1/2*hypergeometric((1/3, 2/3), (1/2,), x) sage: f - h hypergeometric((1/3, 2/3), (1/2,), x) - hypergeometric((1/5, 2/5), (3/5,), x) + + :: + + sage: f - sin(x) + hypergeometric((1/3, 2/3), (1/2,), x) - sin(x) """ if self._parameters is None: return other @@ -937,13 +967,12 @@ def _sub_(self, other): def _mul_(self, other): r""" - Return the (formal) product of the hypergeometric function and - another, defined over the same ring. + Return the (formal) product of the hypergeometric function + and ``other`` INPUT: - - ``other`` -- a hypergeometric function defined over the same - ring. + - ``other`` -- a hypergeometric function or a formal expression EXAMPLES:: @@ -955,21 +984,54 @@ def _mul_(self, other): 1/2*hypergeometric((1/3, 2/3), (1/2,), x)^2 sage: f*h hypergeometric((1/3, 2/3), (1/2,), x)*hypergeometric((1/5, 2/5), (3/5,), x) + + :: + + sage: sin(x)*f + x + hypergeometric((1/3, 2/3), (1/2,), x)*sin(x) + x """ return SR(self) * SR(other) def __call__(self, x): - return SR(self)(x) + r""" + Return the value of this hypergeometric function at ``x``. + + INPUT: + + - ``x`` -- an element + + EXAMPLES:: + + sage: S. = RR[] + sage: f = hypergeometric([1/3, 2/3], [1/2], x) + sage: f(0.5) + 1.36602540378444 + + :: + + sage: g = 2*f + sage: g(0.2) + 2.20941633798502 + """ + scalar = self._scalar + if scalar == 0: + return self.base_ring().zero() + from sage.functions.hypergeometric import hypergeometric + X = SR('X') + h = hypergeometric(self.top(), self.bottom(), X) + if scalar != 1: + h *= scalar + return h(X=x) def _compute_coeffs(self, prec): r""" - Compute the coefficients of the series representation of the - hypergeometric function up to a given precision, and append them to - ``self._coeffs``. + Compute the coefficients of the series representation of this + hypergeometric function up to a given precision, and store + them in ``self._coeffs``. INPUT: - - ``prec`` -- a positive integer. + - ``prec`` -- a positive integer EXAMPLES:: @@ -991,21 +1053,21 @@ def _compute_coeffs(self, prec): c /= b + i coeffs.append(c) - def series(self, prec): + def power_series(self, prec): r""" - Return the power series representation of the hypergeometric function - up to a given precision. + Return the power series representation of this hypergeometric + function up to a given precision. INPUT: - - ``prec`` -- a positive integer. + - ``prec`` -- a positive integer EXAMPLES:: sage: S. = QQ[] sage: f = hypergeometric([1/3, 2/3], [1/2], x) - sage: f.series(3) - 1 + 4/9*x + 80/243*x^2 + O(x^3) + sage: f.power_series(3) + 1 + 4/9*x + 80/243*x^2 + O(x^3) """ S = self.parent().power_series_ring() self._compute_coeffs(prec) @@ -1013,12 +1075,13 @@ def series(self, prec): def shift(self, s): r""" - Return the hypergeometric function, where each parameter (including the - addional ``1`` as a bottom parameter) is increased by ``s``. + Return this hypergeometric function, where each parameter + (including the additional ``1`` as a bottom parameter) is + increased by ``s``. INPUT: - - ``s`` -- a rational number. + - ``s`` -- a rational number EXAMPLES:: @@ -1080,27 +1143,17 @@ def __mod__(self, p): INPUT: - - ``p`` -- a prime number. + - ``p`` -- a prime number. EXAMPLES:: - sage: from sage.functions.hypergeometric_algebraic import HypergeometricAlgebraic_QQ - sage: from sage.functions.hypergeometric_algebraic import HypergeometricAlgebraic_padic - sage: from sage.functions.hypergeometric_algebraic import HypergeometricAlgebraic_GFp sage: S. = QQ[] sage: f = hypergeometric([1/3, 2/3], [1/2], x) - sage: f - hypergeometric((1/3, 2/3), (1/2,), x) - sage: f.parent() - Hypergeometric functions in x over Rational Field sage: g = f % 5 sage: g hypergeometric((1/3, 2/3), (1/2,), x) - sage: g.parent() - Hypergeometric functions in x over Finite Field of size 5 - sage: h = f.__mod__(5) - sage: h == g - True + sage: g.base_ring() + Finite Field of size 5 """ k = FiniteField(p) val = self._scalar.valuation(p) @@ -1255,6 +1308,7 @@ def _val_pos(self): return -infinity, None u = p*u % d # From here, it is absolutely conjectural! + # ... and probably not quite correct val = self._scalar.valuation() pos = 0 q = 1 @@ -1268,14 +1322,19 @@ def _val_pos(self): parameters = parameters.shift(s).dwork_image(p) return val, pos - def valuation(self): - val, _ = self._val_pos() - return val + def log_radius_of_convergence(self): + raise NotImplementedError - def radius_of_convergence(self): + def newton_polygon(self, log_radius): raise NotImplementedError - def newton_polygon(self): + def valuation(self, log_radius=0): + if log_radius != 0: + raise NotImplementedError + val, _ = self._val_pos() + return val + + def tate_series(self): raise NotImplementedError def __call__(self, x): @@ -1292,7 +1351,7 @@ def __init__(self, parent, arg1, arg2=None, scalar=None): self._p = p = self.base_ring().cardinality() self._coeffs = [Qp(p, 1)(self._scalar)] - def series(self, prec): + def power_series(self, prec): S = self.parent().power_series_ring() self._compute_coeffs(prec) try: From d8b9c681386c79f6c46f478638bcdc6cb16a918d Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Tue, 11 Nov 2025 14:58:13 +0100 Subject: [PATCH 41/93] fix doctest --- src/sage/functions/hypergeometric_algebraic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index f34baf47972..810ee7e4ff1 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -903,7 +903,7 @@ def _add_(self, other): :: sage: f + cos(x) - hypergeometric((1/3, 2/3), (1/2,), x) + cos(x) + cos(x) + hypergeometric((1/3, 2/3), (1/2,), x) """ if self._parameters is None: return other From a614bf7252f81805545d7472c86da439d60fee64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20F=C3=BCrnsinn?= Date: Tue, 11 Nov 2025 16:30:44 +0100 Subject: [PATCH 42/93] doc --- .../functions/hypergeometric_algebraic.py | 77 ++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index f34baf47972..058d1a0ac81 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -903,7 +903,7 @@ def _add_(self, other): :: sage: f + cos(x) - hypergeometric((1/3, 2/3), (1/2,), x) + cos(x) + cos(x) + hypergeometric((1/3, 2/3), (1/2,), x) """ if self._parameters is None: return other @@ -1095,6 +1095,22 @@ def shift(self, s): @coerce_binop def hadamard_product(self, other): + r""" + Return the hadamard product of the hypergeometric function + and ``other`` + + INPUT: + + - ``other`` -- a hypergeometric function + + EXAMPLES:: + + sage: S. = QQ[] + sage: f = hypergeometric([1/3, 2/3], [1/2], x) + sage: h = 1/2*hypergeometric([1/5, 2/5], [3/5], x) + sage: f.hadamard_product(h) + 1/2*hypergeometric((1/5, 1/3, 2/5, 2/3), (1/2, 3/5, 1), x) + """ if self._scalar == 0: return self if other._scalar == 0: @@ -1105,12 +1121,71 @@ def hadamard_product(self, other): return self.parent()(top, bottom, scalar=scalar) def _div_(self, other): + r""" + Return the (formal) quotient of the hypergeometric function + and ``other`` + + INPUT: + + - ``other`` -- a hypergeometric function or a formal expression + + EXAMPLES:: + + sage: S. = QQ[] + sage: f = hypergeometric([1/3, 2/3], [1/2], x) + sage: g = 1/2 * hypergeometric([1/3, 2/3], [1/2], x) + sage: h = hypergeometric([1/5, 2/5], [3/5], x) + sage: f/g + 2 + sage: f/h + hypergeometric((1/3, 2/3), (1/2,), x)/hypergeometric((1/5, 2/5), (3/5,), x) + + :: + + sage: f/sin(x) + x + x + hypergeometric((1/3, 2/3), (1/2,), x)/sin(x) + """ return SR(self) / SR(other) def denominator(self): + r""" + Return the smallest common denominator of the parameters. + + EXAMPLES:: + + sage: S. = QQ[] + sage: f = hypergeometric([1/3, 2/3], [1/2], x) + sage: f.denominator() + 6 + """ return self._parameters.d def differential_operator(self, var='d'): + r""" + Return the hypergeometric differential operator that annihilates this + hypergeometric function as an Ore polynomial in the varable ``var``. + + INPUT: + + - ``var`` -- a string, the variable name of the derivation + (default: ``d``) + + EXAMPLES:: + + sage: S. = QQ[] + sage: f = hypergeometric([1/3, 2/3], [1/2], x) + sage: f.differential_operator(var='D') + (-x^2 + x)*D^2 + (-2*x + 1/2)*D - 2/9 + + Note that this does not necessarily give the minimal differential operator + annihilating this hypergeometric function.:: + + sage: g = hypergeometric([1/3, 2/3, 6/5], [1/5, 1/2]) + sage: g.differential_operator() + sage: + + + """ S = self.parent().polynomial_ring() x = S.gen() D = OrePolynomialRing(S, S.derivation(), names=var) From 25ff85445cf6c9f57c963eb25bf982fe6fca5bdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20F=C3=BCrnsinn?= Date: Wed, 12 Nov 2025 16:34:21 +0100 Subject: [PATCH 43/93] documentation, issue with valuation --- src/doc/en/reference/references/index.rst | 7 ++ .../functions/hypergeometric_algebraic.py | 111 ++++++++++++++++-- 2 files changed, 109 insertions(+), 9 deletions(-) diff --git a/src/doc/en/reference/references/index.rst b/src/doc/en/reference/references/index.rst index 1f1cd007290..e66117a2eec 100644 --- a/src/doc/en/reference/references/index.rst +++ b/src/doc/en/reference/references/index.rst @@ -1650,6 +1650,10 @@ REFERENCES: IV. The quotient groups of the lower central series, Ann. of Math. 68 (1958) 81--95. +.. [CFV2025] Xavier Caruso, Florian Fürnsinn, Daniel Vargas-Montoya, + *Galois groups of reductions modulo p of D-finite series*, + :arxiv:`2504.09429`, 2025. + .. [CFZ2000] \J. Cassaigne, S. Ferenczi, L.Q. Zamboni, *Imbalances in Arnoux-Rauzy sequences*, Ann. Inst. Fourier (Grenoble) 50 (2000) 1265--1276. @@ -1724,6 +1728,9 @@ REFERENCES: .. [ChenDB] Eric Chen, Online database of two-weight codes, http://moodle.tec.hkr.se/~chen/research/2-weight-codes/search.php +.. [Chr1986] Gilles Christol, *Fonctions hypergéométriques bornées*. + Groupe de travail d’analyse ultramétrique 14 (1986-87), exp. no 8, p. 1-16 + .. [CHK2001] Keith D. Cooper, Timothy J. Harvey and Ken Kennedy. *A Simple, Fast Dominance Algorithm*, Software practice and Experience, 4:1-10 (2001). diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 058d1a0ac81..c950ac6a037 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -839,7 +839,7 @@ def scalar(self): def change_ring(self, R): r""" - Return this hypergeometric function with changed base ring + Return this hypergeometric function with changed base ring. INPUT: @@ -1179,12 +1179,24 @@ def differential_operator(self, var='d'): Note that this does not necessarily give the minimal differential operator annihilating this hypergeometric function.:: - - sage: g = hypergeometric([1/3, 2/3, 6/5], [1/5, 1/2]) - sage: g.differential_operator() - sage: - - + + sage: S = FractionField(PolynomialRing(QQ, 'x')) + sage: D = OrePolynomialRing(S, S.derivation(), names='d') + sage: g = hypergeometric([1/3, 2/3, 6/5], [1/5, 1/2], x) + sage: g + hypergeometric((1/3, 2/3, 6/5), (1/5, 1/2), x) + sage: L = D(g.differential_operator()) + sage: L.degree() + 3 + sage: d = L.parent('d') + sage: M = L.parent((72*x^3 - 234*x^2 + 162*x)*d^2 + (144*x^2 - 450*x + 81)*d + 16*x - 216) + sage: M.degree() + 2 + sage: M.right_divides(L) + True + sage: gs = g.power_series(100) + sage: (72*x^3 - 234*x^2 + 162*x)*gs.derivative(2) + (144*x^2 - 450*x + 81)*gs.derivative() + (16*x - 216)*gs + O(x^99) """ S = self.parent().polynomial_ring() x = S.gen() @@ -1202,6 +1214,16 @@ def differential_operator(self, var='d'): return D([c//x for c in L.list()]) def derivative(self): + r""" + Return the derivative of this hypergeometric function. + + EXAMPLES:: + + sage: S. = QQ[] + sage: f = hypergeometric([1/3, 2/3], [1/2], x) + sage: f.derivative() + 4/9*hypergeometric((4/3, 5/3), (3/2,), x) + """ top = [a+1 for a in self.top()] bottom = [b+1 for b in self.bottom()] scalar = prod(self._parameters.top) / prod(self._parameters.bottom) @@ -1238,16 +1260,68 @@ def __mod__(self, p): return h.residue() def valuation(self, p): + r""" + Return the p-adic valuation of this hypergeometric function, i.e., the + maximal s, such that p^(-s) times this hypergeometric function has + p-integral coefficients. + + INPUT: + + - ``p`` -- a prime number + + EXAMPLES:: + + sage: S. = QQ[x] + sage: f = hypergeometric([1/3, 2/3], [1/2], x) + sage: f.valuation(5) + 0 + sage: g = 5*f + sage: g.valuation(5) + 1 + """ return self.change_ring(Qp(p, 1)).valuation() def has_good_reduction(self, p): + r""" + Return ``True`` if the p-adic valuation of this hypergeometric function + is non-negative, i.e., if its reduction modulo ``p`` is well-defined.pAdicGeneric + + INPUT: + + - ``p`` -- a prime number + + EXAMPLES:: + + sage: S. = QQ[x] + sage: f = hypergeometric([1/3, 2/3], [1/2], x) + sage: f.valuation(5) + 0 + sage: f.has_good_reduction(5) + True + sage: g = 1/5*f + sage: g.has_good_reduction(5) + 1 + """ return self.valuation(p) >= 0 def good_reduction_primes(self): r""" + Return the set of prime numbers modulo which this hypergeometric + function can be reduced, i.e., the p-adic valuation is positive. + + EXAMPLE:: + + sage: S. = QQ[] + sage: f = hypergeometric([1/3, 2/3], [1/2], x) + sage: f.good_reduction_primes() + ALGORITHM: - We rely on Christol's criterion ([CF2025]_) + We rely on Christol's criterion ([Chr1986]_, Prop. 1) for globally + bounded hypergeometric function, from which a criterion can be deduced + modulo which primes a hypergeometric function can be reduced. + ([CFV2025]_, Thm. 3.1.3). For small primes p we compute the p-adic + valuation of the hypergeometric function individually. """ params = self._parameters d = params.d @@ -1373,7 +1447,26 @@ def _val_pos(self): d = self.denominator() parameters = self._parameters if d.gcd(p) > 1: - return -infinity, None + T = [] + B = [] + difference = 0 + for j in self.top(): + d = j.denominator().valuation(p) + difference += d + if not d: + T += [j] + for j in self.bottom(): + d = j.denominator().valuation(p) + difference -= d + if not d: + B += [j] + if difference > 0: + return -infinity, None + if difference < 0: + # the valuation of the coefficients goes to infinity, but what is its minimum? + raise NotImplementedError('The hypergeometric function has bounded valuation, but value and position are not implemented.') + if difference == 0: + return self.parent()(T, B, self.scalar())._val_pos() u = 1 if not parameters.parenthesis_criterion(u): return -infinity, None From d9af29a550de90dd1fb771b9068d48e849a2ea3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20F=C3=BCrnsinn?= Date: Fri, 14 Nov 2025 11:44:25 +0100 Subject: [PATCH 44/93] Potential fix for valuation --- src/doc/en/reference/references/index.rst | 2 +- .../functions/hypergeometric_algebraic.py | 22 +++++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/doc/en/reference/references/index.rst b/src/doc/en/reference/references/index.rst index e66117a2eec..e2e9021ce16 100644 --- a/src/doc/en/reference/references/index.rst +++ b/src/doc/en/reference/references/index.rst @@ -1729,7 +1729,7 @@ REFERENCES: http://moodle.tec.hkr.se/~chen/research/2-weight-codes/search.php .. [Chr1986] Gilles Christol, *Fonctions hypergéométriques bornées*. - Groupe de travail d’analyse ultramétrique 14 (1986-87), exp. no 8, p. 1-16 + Groupe de travail d’analyse ultramétrique 14 (1986-87), exp. no 8, p. 1-16 .. [CHK2001] Keith D. Cooper, Timothy J. Harvey and Ken Kennedy. *A Simple, Fast Dominance Algorithm*, Software practice and diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index c950ac6a037..afa88b5437d 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -1356,6 +1356,8 @@ def good_reduction_primes(self): for p in Primes(): if p > bound: break + if d % p == 0 and self.valuation(p) > 0: + exceptions[p] = True if d % p == 0 or not goods[p % d]: continue if self.valuation(p) < 0: @@ -1463,8 +1465,24 @@ def _val_pos(self): if difference > 0: return -infinity, None if difference < 0: - # the valuation of the coefficients goes to infinity, but what is its minimum? - raise NotImplementedError('The hypergeometric function has bounded valuation, but value and position are not implemented.') + _, prec = self.parent()(T, B, self.scalar())._val_pos() + #This is the case when the p-adic valuation of the coefficients + #goes to +infinity, but if you remove all the coefficients with p + #in the denominator it goes to -infinity. + #The following claim is almost surely wrong. + if prec == None: + prec = self._parameters.bound + #Here I just check the valuation of the first few coefficients. + #There should be something better using q_g:parenthesis. + L = self.change_ring(Qp(p, 1)).power_series(prec+1).coefficients() + val = + infinity + pos = 0 + for j in range(len(L)): + v = L[j].valuation() + if v < val: + val = v + pos = j + return val, pos if difference == 0: return self.parent()(T, B, self.scalar())._val_pos() u = 1 From 9a2c0eafe781a26ae4ac1e95d6d96f3b5944f913 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Fri, 14 Nov 2025 17:39:05 +0100 Subject: [PATCH 45/93] fix and simplify doctests --- .../functions/hypergeometric_algebraic.py | 46 ++++++++----------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index afa88b5437d..0ed951a5a12 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -1097,7 +1097,7 @@ def shift(self, s): def hadamard_product(self, other): r""" Return the hadamard product of the hypergeometric function - and ``other`` + and ``other``. INPUT: @@ -1110,7 +1110,7 @@ def hadamard_product(self, other): sage: h = 1/2*hypergeometric([1/5, 2/5], [3/5], x) sage: f.hadamard_product(h) 1/2*hypergeometric((1/5, 1/3, 2/5, 2/3), (1/2, 3/5, 1), x) - """ + """ if self._scalar == 0: return self if other._scalar == 0: @@ -1162,13 +1162,14 @@ def denominator(self): def differential_operator(self, var='d'): r""" - Return the hypergeometric differential operator that annihilates this - hypergeometric function as an Ore polynomial in the varable ``var``. + Return the hypergeometric differential operator that annihilates + this hypergeometric function as an Ore polynomial in the variable + ``var``. INPUT: - - ``var`` -- a string, the variable name of the derivation - (default: ``d``) + - ``var`` -- a string (default: ``d``), the variable name of + the derivation EXAMPLES:: @@ -1176,24 +1177,16 @@ def differential_operator(self, var='d'): sage: f = hypergeometric([1/3, 2/3], [1/2], x) sage: f.differential_operator(var='D') (-x^2 + x)*D^2 + (-2*x + 1/2)*D - 2/9 - - Note that this does not necessarily give the minimal differential operator - annihilating this hypergeometric function.:: - - sage: S = FractionField(PolynomialRing(QQ, 'x')) - sage: D = OrePolynomialRing(S, S.derivation(), names='d') + + Note that this does not necessarily give the minimal differential + operator annihilating this hypergeometric function: in the example + below, this method returns an operator of order `3` where `g` is + solution of a differential equation of order `2`:: + sage: g = hypergeometric([1/3, 2/3, 6/5], [1/5, 1/2], x) - sage: g - hypergeometric((1/3, 2/3, 6/5), (1/5, 1/2), x) - sage: L = D(g.differential_operator()) + sage: L = g.differential_operator() sage: L.degree() 3 - sage: d = L.parent('d') - sage: M = L.parent((72*x^3 - 234*x^2 + 162*x)*d^2 + (144*x^2 - 450*x + 81)*d + 16*x - 216) - sage: M.degree() - 2 - sage: M.right_divides(L) - True sage: gs = g.power_series(100) sage: (72*x^3 - 234*x^2 + 162*x)*gs.derivative(2) + (144*x^2 - 450*x + 81)*gs.derivative() + (16*x - 216)*gs O(x^99) @@ -1300,7 +1293,7 @@ def has_good_reduction(self, p): True sage: g = 1/5*f sage: g.has_good_reduction(5) - 1 + False """ return self.valuation(p) >= 0 @@ -1309,18 +1302,19 @@ def good_reduction_primes(self): Return the set of prime numbers modulo which this hypergeometric function can be reduced, i.e., the p-adic valuation is positive. - EXAMPLE:: + EXAMPLES:: sage: S. = QQ[] sage: f = hypergeometric([1/3, 2/3], [1/2], x) sage: f.good_reduction_primes() + Set of all prime numbers with 2, 3 excluded: 5, 7, 11, 13, ... ALGORITHM: We rely on Christol's criterion ([Chr1986]_, Prop. 1) for globally bounded hypergeometric function, from which a criterion can be deduced - modulo which primes a hypergeometric function can be reduced. - ([CFV2025]_, Thm. 3.1.3). For small primes p we compute the p-adic + modulo which primes a hypergeometric function can be reduced + ([CFV2025]_, Thm. 3.1.3). For small primes `p`, we compute the `p`-adic valuation of the hypergeometric function individually. """ params = self._parameters @@ -1357,7 +1351,7 @@ def good_reduction_primes(self): if p > bound: break if d % p == 0 and self.valuation(p) > 0: - exceptions[p] = True + exceptions[p] = True if d % p == 0 or not goods[p % d]: continue if self.valuation(p) < 0: From 956a213d9b49632df4906b8c4bd57b12a3a64296 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20F=C3=BCrnsinn?= Date: Sat, 15 Nov 2025 15:44:12 +0100 Subject: [PATCH 46/93] fix good_reduction_primes --- src/sage/functions/hypergeometric_algebraic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 0ed951a5a12..a0f40d9cf5a 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -1307,7 +1307,7 @@ def good_reduction_primes(self): sage: S. = QQ[] sage: f = hypergeometric([1/3, 2/3], [1/2], x) sage: f.good_reduction_primes() - Set of all prime numbers with 2, 3 excluded: 5, 7, 11, 13, ... + Set of all prime numbers with 3 excluded: 2, 5, 7, 11, ... ALGORITHM: @@ -1350,7 +1350,7 @@ def good_reduction_primes(self): for p in Primes(): if p > bound: break - if d % p == 0 and self.valuation(p) > 0: + if d % p == 0 and self.valuation(p) >= 0: exceptions[p] = True if d % p == 0 or not goods[p % d]: continue From 00548b2d72e3ea7aa06fece19e62cdbf387fcf6b Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Sat, 15 Nov 2025 19:05:13 +0100 Subject: [PATCH 47/93] new algorithm for computing valuation --- .../functions/hypergeometric_algebraic.py | 166 +++++++++++++----- 1 file changed, 122 insertions(+), 44 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index a0f40d9cf5a..87820586ccb 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -27,7 +27,8 @@ from sage.misc.misc_c import prod from sage.misc.functional import log -from sage.functions.other import floor, ceil +from sage.functions.other import ceil +from sage.functions.hypergeometric import hypergeometric from sage.arith.misc import gcd from sage.arith.functions import lcm from sage.matrix.constructor import matrix @@ -36,7 +37,6 @@ from sage.structure.parent import Parent from sage.structure.element import Element from sage.structure.element import coerce_binop -from sage.structure.sequence import Sequence from sage.structure.category_object import normalize_names from sage.categories.action import Action @@ -53,6 +53,7 @@ from sage.sets.primes import Primes from sage.rings.integer_ring import ZZ from sage.rings.rational_field import QQ +from sage.rings.finite_rings.integer_mod_ring import IntegerModRing from sage.rings.finite_rings.finite_field_constructor import FiniteField from sage.rings.padics.padic_generic import pAdicGeneric from sage.rings.padics.factory import Qp @@ -294,12 +295,10 @@ def parenthesis_criterion(self, c): True """ parenthesis = 0 - previous_paren = 1 for _, _, paren in self.christol_sorting(c): parenthesis += paren if parenthesis > 0: return False - previous_paren = paren return parenthesis <= 0 def interlacing_criterion(self, c): @@ -562,6 +561,92 @@ def decimal_part(self): bottom = [1 + b - ceil(b) for b in self.bottom] return Parameters(top, bottom, add_one=False) + def valuation_position(self, p, drift=0): + top = [] + for a in self.top: + v = a.valuation(p) + if v < 0: + drift += v + else: + top.append(a) + bottom = [] + for b in self.bottom: + v = b.valuation(p) + if v < 0: + drift -= v + else: + bottom.append(b) + diff = len(top) - len(bottom) + if ((p-1)*drift + diff, drift) < (0, 0): + return -infinity, None + + parameters = Parameters(top, bottom) + order = IntegerModRing(parameters.d)(p).multiplicative_order() + q = 1 + valuation = position = 0 + breaks = [(0, 0, 0)] + indices = None + count = 0 + while True: + pq = p * q + A = [(1 + (-a) % pq, -1, a) for a in top] + B = [(1 + (-b) % pq, 1, b) for b in bottom] + AB = [(0, 0, 0)] + sorted(A + B) + [(pq, None, None)] + new_breaks = [] + new_indices = {} + w = 0 + for i in range(len(AB) - 1): + x, dw, param = AB[i] + y, _, right = AB[i+1] + w -= dw + new_indices[param] = len(new_breaks) + complete = (y - x >= q) + if complete and drift < 0: + interval = (y // q) - 1 + else: + interval = x // q + if x == y: + val = infinity + pos = x + elif indices is None: + val = drift * interval + pos = q * interval + else: + val = infinity + if complete and drift < 0: + j = j0 = indices[right] + else: + j = j0 = indices[param] + while True: + valj, posj, paramj = breaks[j] + valj += drift * interval + if valj < val: + val = valj + pos = posj + q * interval + j += 1 + if j >= len(breaks): + if right is None: + break + j = 0 + interval += 1 + if (not complete and paramj == right) or (complete and j == j0): + break + new_breaks.append((val + w, pos, param)) + breaks = new_breaks + indices = new_indices + minimum = min(breaks) + if drift >= 0 and q > parameters.bound: + # Not sure at all about this criterion + if minimum == breaks[0] and minimum[0] == valuation and minimum[1] == position: + count += 1 + if count >= order: + return valuation, position + else: + return -infinity, None + q = pq + drift = p*drift + diff + valuation, position, _ = minimum + def dwork_image(self, p): r""" Return the parameters obtained by applying the Dwork map to each of @@ -682,29 +767,12 @@ def __init__(self, parent, arg1, arg2=None, scalar=None): parameters = Parameters(arg1, arg2) char = self.parent()._char if scalar: - if char == 0: - if any(b in ZZ and b < 0 for b in parameters.bottom): - raise ValueError("the parameters %s do not define a hypergeometric function" % parameters) - else: - error = ValueError("the parameters %s do not define a hypergeometric function in characteristic %s" % (parameters, char)) - d = parameters.d - if d.gcd(char) > 1: - raise error - u = 1 - while True: - if not parameters.parenthesis_criterion(u): - raise error - u = char*u % d - if u == 1: - break - # Xavier's conjecture: - if not parameters.q_parenthesis_criterion(char): - raise error - # q = char - # while q <= parameters.bound: - # if not parameters.q_parenthesis_criterion(q): - # raise error - # q *= char + if any(b in ZZ and b < 0 for b in parameters.bottom): + raise ValueError("the parameters %s do not define a hypergeometric function" % parameters) + if char > 0: + val, _ = parameters.valuation_position(char) + if val < 0: + raise ValueError("the parameters %s do not define a hypergeometric function in characteristic %s" % (parameters, char)) self._scalar = scalar self._parameters = parameters self._coeffs = [scalar] @@ -1016,7 +1084,6 @@ def __call__(self, x): scalar = self._scalar if scalar == 0: return self.base_ring().zero() - from sage.functions.hypergeometric import hypergeometric X = SR('X') h = hypergeometric(self.top(), self.bottom(), X) if scalar != 1: @@ -1272,7 +1339,8 @@ def valuation(self, p): sage: g.valuation(5) 1 """ - return self.change_ring(Qp(p, 1)).valuation() + val, _ = self._parameters.valuation_position(p) + return val + self._scalar.valuation(p) def has_good_reduction(self, p): r""" @@ -1322,13 +1390,13 @@ def good_reduction_primes(self): # We check the parenthesis criterion for c=1 if not params.parenthesis_criterion(1): - return d, [], [] + return Primes(modulus=0) # We check the parenthesis criterion for other c # and derive congruence classes with good reduction goods = {c: None for c in range(d) if d.gcd(c) == 1} goods[1] = True - for c in goods: + for c in goods.keys(): if goods[c] is not None: continue cc = c @@ -1427,7 +1495,7 @@ def residue(self): k = self.base_ring().residue_field() if self._scalar.valuation() == 0: return self.change_base(k) - val, pos = self._val_pos() + val, pos = self._parameters.valuation_position(self._p) if val < 0: raise ValueError("bad reduction") if val > 0: @@ -1464,7 +1532,7 @@ def _val_pos(self): #goes to +infinity, but if you remove all the coefficients with p #in the denominator it goes to -infinity. #The following claim is almost surely wrong. - if prec == None: + if prec is None: prec = self._parameters.bound #Here I just check the valuation of the first few coefficients. #There should be something better using q_g:parenthesis. @@ -1503,17 +1571,31 @@ def _val_pos(self): return val, pos def log_radius_of_convergence(self): - raise NotImplementedError - - def newton_polygon(self, log_radius): - raise NotImplementedError + p = self._p + step = self._e / (p - 1) + log_radius = 0 + for a in self._parameters.top: + v = a.valuation(p) + if v < 0: + log_radius += v + else: + log_radius += step + for b in self._parameters.bottom: + v = b.valuation(p) + if v < 0: + log_radius -= v + else: + log_radius -= step + return log_radius def valuation(self, log_radius=0): - if log_radius != 0: - raise NotImplementedError - val, _ = self._val_pos() + drift = -log_radius / self._e + val, _ = self._parameters.valuation_position(self._p, drift) return val + def newton_polygon(self, log_radius): + raise NotImplementedError + def tate_series(self): raise NotImplementedError @@ -1557,7 +1639,6 @@ def is_almost_defined(self): def is_defined(self): p = self._char - d = self.denominator() if not self.is_almost_defined(): return False bound = self._parameters.bound @@ -1572,7 +1653,6 @@ def is_defined(self): def is_defined_conjectural(self): p = self._char - d = self.denominator() if not self.is_almost_defined(): return False bound = self._parameters.bound @@ -1655,7 +1735,6 @@ def annihilating_ore_polynomial(self, var='Frob'): if not parameters.is_balanced(): raise NotImplementedError("the hypergeometric function is not a pFq with q = p-1") - K = self.base_ring() p = self._char S = self.parent().polynomial_ring() zero = S.zero() @@ -1728,7 +1807,6 @@ def is_lucas(self): class HypergeometricToSR(Map): def _call_(self, h): - from sage.functions.hypergeometric import hypergeometric return h.scalar() * hypergeometric(h.top(), h.bottom(), SR.var(h.parent().variable_name())) From d580938bbcf174fae362453847b68d6857a9a6bb Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Sat, 15 Nov 2025 19:19:59 +0100 Subject: [PATCH 48/93] indentation --- src/sage/functions/hypergeometric_algebraic.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 87820586ccb..42248a22827 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -1220,10 +1220,10 @@ def denominator(self): EXAMPLES:: - sage: S. = QQ[] - sage: f = hypergeometric([1/3, 2/3], [1/2], x) - sage: f.denominator() - 6 + sage: S. = QQ[] + sage: f = hypergeometric([1/3, 2/3], [1/2], x) + sage: f.denominator() + 6 """ return self._parameters.d From 064702759e58ca2457403882cfae0f6b1bfe4c89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20F=C3=BCrnsinn?= Date: Sun, 16 Nov 2025 00:06:55 +0100 Subject: [PATCH 49/93] small changes --- src/sage/functions/hypergeometric_algebraic.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index a0f40d9cf5a..c9e96cb20c7 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -1153,10 +1153,10 @@ def denominator(self): EXAMPLES:: - sage: S. = QQ[] - sage: f = hypergeometric([1/3, 2/3], [1/2], x) - sage: f.denominator() - 6 + sage: S. = QQ[] + sage: f = hypergeometric([1/3, 2/3], [1/2], x) + sage: f.denominator() + 6 """ return self._parameters.d From 2e3fbeeba5b22765157a0514c09a81a3c3116aad Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Sun, 16 Nov 2025 06:53:20 +0100 Subject: [PATCH 50/93] add comments --- .../functions/hypergeometric_algebraic.py | 140 ++++++++++++++---- 1 file changed, 110 insertions(+), 30 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 42248a22827..6d15e0e967a 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -562,6 +562,43 @@ def decimal_part(self): return Parameters(top, bottom, add_one=False) def valuation_position(self, p, drift=0): + r""" + If `h_k`s denote the coefficients of the hypergeometric + series corresponding to these parameters, return the smallest + value of + + .. MATH:: + + \text{val}_p(h_k) + k \cdot \text{drift} + + and the first position where this minimum is reached. + + INPUT: + + - ``p`` -- a prime number + + - ``drift`` -- a rational number (default: ``0``) + + EXAMPLES:: + + sage: from sage.functions.hypergeometric_algebraic import Parameters + sage: pa = Parameters([1/5, 1/5, 1/5], [1/3, 3^10/5]) + sage: pa.valuation_position(3) + (-9, 1) + + When the relevant sequence is not bounded from below, the + tuple ``(-Infinity, None)`` is returned:: + + sage: pa.valuation_position(5) + (-Infinity, None) + + An example with a drift:: + + sage: pa.valuation_position(3, drift=-7/5) + (-54/5, 7) + """ + # We treat the case of parameters having p in the denominator + # When it happens, we remove the parameter and update the drift accordingly top = [] for a in self.top: v = a.valuation(p) @@ -576,47 +613,80 @@ def valuation_position(self, p, drift=0): drift -= v else: bottom.append(b) + + # We check that we are inside the disk of convergence diff = len(top) - len(bottom) if ((p-1)*drift + diff, drift) < (0, 0): return -infinity, None + # Main part: computation of the valuation + # We use Christol's formula (see Lemma 3.1.10 of [CFVM2025]) + # with modification in order to take the drift into account parameters = Parameters(top, bottom) order = IntegerModRing(parameters.d)(p).multiplicative_order() - q = 1 valuation = position = 0 - breaks = [(0, 0, 0)] - indices = None + breaks = None + indices = {} count = 0 + q = 1 while True: + # We take into account the contribution of V({k/p^r}, p^r). + # We represent the partial sum until r by the table new_breaks. + # Its entries are triples (valuation, position, parameter): + # - parameter is the parameter corresponding to a point of + # discontinuity of the last summand V({k/p^r}, p^r) + # - valuation is the minimum of the partial sum on the + # range starting at this discontinuity point (included) + # and ending at the next one (excluded) + # - position is the first position the minimum is reached + # (The dictionary new_indices allows for finding rapidly + # an entry in new_breaks with a given parameter.) + # The table breaks and the dictionary indices correspond + # to the same data for r-1. + pq = p * q + + # We compute the point of discontinuity of V({k/p^r}, p^r) + # and store them in AB + # Each entry of AB has the form (x, dw, parameter) where: + # - x is the position of the discontinuity point + # - dw is the *opposite* of the jump of V({k/p^r}, p^r) + # at this point + # (taking the opposite is useful for sorting reasons) + # - parameter is the corresponding parameter A = [(1 + (-a) % pq, -1, a) for a in top] B = [(1 + (-b) % pq, 1, b) for b in bottom] - AB = [(0, 0, 0)] + sorted(A + B) + [(pq, None, None)] + AB = [(0, 0, 0)] + sorted(A + B) + [(pq, None, 0)] + + # We compute new_breaks new_breaks = [] new_indices = {} w = 0 for i in range(len(AB) - 1): - x, dw, param = AB[i] - y, _, right = AB[i+1] - w -= dw + x, dw, param = AB[i] # discontinuity point + y, _, right = AB[i+1] # next discontinuity point + w -= dw # the value of V({k/p^r}, p^r) on this interval new_indices[param] = len(new_breaks) + if x == y: + # Case of empty interval + new_breaks.append((infinity, None, param)) + continue + # The variable complete stores whether the interval + # [x,y] covers all [0, p^(r-1)) modulo p^(r-1) complete = (y - x >= q) if complete and drift < 0: interval = (y // q) - 1 + j = j0 = indices.get(right, 0) else: interval = x // q - if x == y: - val = infinity - pos = x - elif indices is None: + j = j0 = indices.get(param, 0) + if breaks is None: + # Case r = 1 val = drift * interval pos = q * interval else: + # Case r > 1 val = infinity - if complete and drift < 0: - j = j0 = indices[right] - else: - j = j0 = indices[param] while True: valj, posj, paramj = breaks[j] valj += drift * interval @@ -625,26 +695,29 @@ def valuation_position(self, p, drift=0): pos = posj + q * interval j += 1 if j >= len(breaks): - if right is None: - break j = 0 interval += 1 if (not complete and paramj == right) or (complete and j == j0): break new_breaks.append((val + w, pos, param)) - breaks = new_breaks - indices = new_indices - minimum = min(breaks) + + # Now comes the halting criterion + # I'm not sure at all about it and I actually suspect that it is wrong + # I will rework it + minimum = min(new_breaks) if drift >= 0 and q > parameters.bound: - # Not sure at all about this criterion - if minimum == breaks[0] and minimum[0] == valuation and minimum[1] == position: + if minimum == new_breaks[0] and minimum[0] == valuation and minimum[1] == position: count += 1 if count >= order: return valuation, position else: return -infinity, None + + # We update the values for the next r q = pq drift = p*drift + diff + breaks = new_breaks + indices = new_indices valuation, position, _ = minimum def dwork_image(self, p): @@ -1394,9 +1467,10 @@ def good_reduction_primes(self): # We check the parenthesis criterion for other c # and derive congruence classes with good reduction - goods = {c: None for c in range(d) if d.gcd(c) == 1} + cs = [c for c in range(d) if d.gcd(c) == 1] + goods = {c: None for c in cs} goods[1] = True - for c in goods.keys(): + for c in cs: if goods[c] is not None: continue cc = c @@ -1528,14 +1602,14 @@ def _val_pos(self): return -infinity, None if difference < 0: _, prec = self.parent()(T, B, self.scalar())._val_pos() - #This is the case when the p-adic valuation of the coefficients - #goes to +infinity, but if you remove all the coefficients with p - #in the denominator it goes to -infinity. - #The following claim is almost surely wrong. + # This is the case when the p-adic valuation of the coefficients + # goes to +infinity, but if you remove all the coefficients with p + # in the denominator it goes to -infinity. + # The following claim is almost surely wrong. if prec is None: prec = self._parameters.bound - #Here I just check the valuation of the first few coefficients. - #There should be something better using q_g:parenthesis. + # Here I just check the valuation of the first few coefficients. + # There should be something better using q_g:parenthesis. L = self.change_ring(Qp(p, 1)).power_series(prec+1).coefficients() val = + infinity pos = 0 @@ -1570,11 +1644,17 @@ def _val_pos(self): parameters = parameters.shift(s).dwork_image(p) return val, pos + def dwork_image(self): + parameters = self._parameters.dwork_image(self._p) + return self.parent()(parameters, scalar=self._scalar) + def log_radius_of_convergence(self): p = self._p step = self._e / (p - 1) log_radius = 0 for a in self._parameters.top: + if a in ZZ and a <= 0: + return infinity v = a.valuation(p) if v < 0: log_radius += v From d9794916d3492a1ee54c87571130a7b66f76338f Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Sun, 16 Nov 2025 15:49:32 +0100 Subject: [PATCH 51/93] fix halting criterion --- src/sage/functions/hypergeometric_algebraic.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 6d15e0e967a..735a3deb9b6 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -624,7 +624,7 @@ def valuation_position(self, p, drift=0): # with modification in order to take the drift into account parameters = Parameters(top, bottom) order = IntegerModRing(parameters.d)(p).multiplicative_order() - valuation = position = 0 + valuation = position = ZZ(0) breaks = None indices = {} count = 0 @@ -662,6 +662,7 @@ def valuation_position(self, p, drift=0): new_breaks = [] new_indices = {} w = 0 + cont = True for i in range(len(AB) - 1): x, dw, param = AB[i] # discontinuity point y, _, right = AB[i+1] # next discontinuity point @@ -680,6 +681,8 @@ def valuation_position(self, p, drift=0): else: interval = x // q j = j0 = indices.get(param, 0) + if (interval + 1) * drift + w < 0: + cont = False if breaks is None: # Case r = 1 val = drift * interval @@ -701,15 +704,13 @@ def valuation_position(self, p, drift=0): break new_breaks.append((val + w, pos, param)) - # Now comes the halting criterion - # I'm not sure at all about it and I actually suspect that it is wrong - # I will rework it + # The halting criterion minimum = min(new_breaks) - if drift >= 0 and q > parameters.bound: - if minimum == new_breaks[0] and minimum[0] == valuation and minimum[1] == position: - count += 1 + if drift >= 0 and q > parameters.bound and minimum == new_breaks[0]: + if cont: if count >= order: return valuation, position + count += 1 else: return -infinity, None From e8215e3819e0f34d557a93c22bc2ace310d2878d Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Sun, 16 Nov 2025 16:06:58 +0100 Subject: [PATCH 52/93] split in two files --- .../functions/hypergeometric_algebraic.py | 842 +----------------- .../functions/hypergeometric_parameters.py | 708 +++++++++++++++ src/sage/functions/meson.build | 1 + 3 files changed, 756 insertions(+), 795 deletions(-) create mode 100644 src/sage/functions/hypergeometric_parameters.py diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 735a3deb9b6..c6604d4b10b 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -1,5 +1,5 @@ r""" -Algebraic properties of hypergeometric functions. +Algebraic properties of hypergeometric functions [Tutorial] @@ -23,14 +23,12 @@ from sage.misc.latex import latex from sage.misc.latex import latex_variable_name -from sage.misc.cachefunc import cached_method from sage.misc.misc_c import prod from sage.misc.functional import log from sage.functions.other import ceil from sage.functions.hypergeometric import hypergeometric from sage.arith.misc import gcd -from sage.arith.functions import lcm from sage.matrix.constructor import matrix from sage.structure.unique_representation import UniqueRepresentation @@ -46,14 +44,13 @@ from sage.matrix.special import companion_matrix from sage.matrix.special import identity_matrix - -from sage.symbolic.ring import SR from sage.combinat.subset import Subsets + from sage.rings.infinity import infinity +from sage.symbolic.ring import SR from sage.sets.primes import Primes from sage.rings.integer_ring import ZZ from sage.rings.rational_field import QQ -from sage.rings.finite_rings.integer_mod_ring import IntegerModRing from sage.rings.finite_rings.finite_field_constructor import FiniteField from sage.rings.padics.padic_generic import pAdicGeneric from sage.rings.padics.factory import Qp @@ -63,730 +60,9 @@ from sage.rings.power_series_ring import PowerSeriesRing from sage.rings.polynomial.ore_polynomial_ring import OrePolynomialRing - -# Helper functions -################## - -def insert_zeroes(P, n): - cs = P.list() - coeffs = n * len(cs) * [0] - for i in range(len(cs)): - coeffs[n*i] = cs[i] - return P.parent()(coeffs) - - -def kernel(M, repeat=2): - n = M.nrows() - m = M.ncols() - if n > m + 1: - raise RuntimeError - if n <= m: - K = M.base_ring().base_ring() - for _ in range(repeat): - a = K.random_element() - Me = matrix(n, m, [f(a) for f in M.list()]) - if Me.rank() == n: - return - for J in Subsets(range(m), n-1): - MJ = M.matrix_from_columns(J) - minor = MJ.delete_rows([0]).determinant() - if minor.is_zero(): - continue - ker = [minor] - for i in range(1, n): - minor = MJ.delete_rows([i]).determinant() - ker.append((-1)**i * minor) - Z = matrix(ker) * M - if not Z.is_zero(): - return - g = ker[0].leading_coefficient() * gcd(ker) - ker = [c//g for c in ker] - return ker - - -# Parameters of hypergeometric functions -######################################## - -class Parameters(): - r""" - Class for parameters of hypergeometric functions. - """ - def __init__(self, top, bottom, add_one=True): - r""" - Initialize this set of parameters. - - INPUT: - - - ``top`` -- list of top parameters - - - ``bottom`` -- list of bottom parameters - - - ``add_one`` -- boolean (default: ``True``), - if ``True``, add an additional one to the bottom - parameters. - - EXAMPLES:: - - sage: from sage.functions.hypergeometric_algebraic import Parameters - sage: pa = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) - sage: pa - ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) - sage: type(pa) - - - By default, parameters are sorted, duplicates are removed and - a trailing `1` is added to the bottom parameters:: - - sage: Parameters([1/2, 1/3, 2/3], [2/3]) - ((1/3, 1/2), (1,)) - - We can avoid adding the trailing `1` by passing ``add_one=False``:: - - sage: Parameters([1/2, 1/3, 2/3], [2/3], add_one=False) - ((1/3, 1/2, 1), (1,)) - """ - try: - top = sorted([QQ(a) for a in top if a is not None]) - bottom = sorted([QQ(b) for b in bottom if b is not None]) - except TypeError: - raise NotImplementedError("parameters must be rational numbers") - i = j = 0 - while i < len(top) and j < len(bottom): - if top[i] == bottom[j]: - del top[i] - del bottom[j] - elif top[i] > bottom[j]: - j += 1 - else: - i += 1 - if add_one: - bottom.append(QQ(1)) - else: - try: - i = bottom.index(QQ(1)) - bottom.append(QQ(1)) - del bottom[i] - except ValueError: - bottom.append(QQ(1)) - top.append(QQ(1)) - top.sort() - self.top = tuple(top) - self.bottom = tuple(bottom) - if len(top) == 0 and len(bottom) == 0: - self.d = 1 - self.bound = 1 - else: - self.d = lcm([ a.denominator() for a in top ] - + [ b.denominator() for b in bottom ]) - self.bound = 2 * self.d * max(top + bottom) + 1 - - def __repr__(self): - r""" - Return a string representation of these parameters. - - EXAMPLES:: - - sage: from sage.functions.hypergeometric_algebraic import Parameters - sage: pa = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) - sage: pa # indirect doctest - ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) - """ - return "(%s, %s)" % (self.top, self.bottom) - - def __hash__(self): - return hash((self.top, self.bottom)) - - def __eq__(self, other): - return (isinstance(other, Parameters) - and self.top == other.top and self.bottom == other.bottom) - - def is_balanced(self): - r""" - Return ``True`` if there are as many top parameters as bottom - parameters; ``False`` otherwise. - - EXAMPLES:: - - sage: from sage.functions.hypergeometric_algebraic import Parameters - sage: pa = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) - sage: pa - ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) - sage: pa.is_balanced() - True - - :: - - sage: pa = Parameters([1/4, 1/3, 1/2], [2/5, 3/5], add_one=False) - sage: pa - ((1/4, 1/3, 1/2, 1), (2/5, 3/5, 1)) - sage: pa.is_balanced() - False - """ - return len(self.top) == len(self.bottom) - - @cached_method - def christol_sorting(self, c=1): - r""" - Return a sorted list of triples, where each triple is associated to one - of the parameters a, and consists of the decimal part of d*c*a (where - integers are assigned 1 instead of 0), the negative value of a, and a - sign (plus or minus 1), where top parameters are assigned -1 and bottom - parameters +1. Sorting the list lexecographically according to the - first two entries of the tuples sorts the corresponing parameters - according to the total ordering (defined on p.6 in [Chr1986]_). - - INPUT: - - - ``c`` -- an integer (default: ``1``) - - EXAMPLES:: - - sage: from sage.functions.hypergeometric_algebraic import Parameters - sage: pa = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) - sage: pa - ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) - sage: pa.christol_sorting(7) - [(12, -3/5, 1), - (20, -1/3, -1), - (30, -1/2, -1), - (45, -1/4, -1), - (48, -2/5, 1), - (60, -1, 1)] - """ - d = self.d - A = [(d - (-d*c*a) % d, -a, -1) for a in self.top] - B = [(d - (-d*c*b) % d, -b, 1) for b in self.bottom] - return sorted(A + B) - - def parenthesis_criterion(self, c): - r""" - Return ``True`` if in each prefix of the list - ``self.christol_sorting(c)`` there are at least as many triples with - third entry -1 as triples with third entry +1. Return ``False`` - otherwise. - - INPUT: - - - ``c`` -- an integer - - EXAMPLES:: - - sage: from sage.functions.hypergeometric_algebraic import Parameters - sage: pa = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) - sage: pa - ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) - sage: pa.christol_sorting(7) - [(12, -3/5, 1), - (20, -1/3, -1), - (30, -1/2, -1), - (45, -1/4, -1), - (48, -2/5, 1), - (60, -1, 1)] - sage: pa.parenthesis_criterion(7) - False - sage: pa.christol_sorting(1) - [(15, -1/4, -1), - (20, -1/3, -1), - (24, -2/5, 1), - (30, -1/2, -1), - (36, -3/5, 1), - (60, -1, 1)] - sage: pa.parenthesis_criterion(1) - True - """ - parenthesis = 0 - for _, _, paren in self.christol_sorting(c): - parenthesis += paren - if parenthesis > 0: - return False - return parenthesis <= 0 - - def interlacing_criterion(self, c): - r""" - Return ``True`` if the sorted lists of the decimal parts (where integers - are assigned 1 instead of 0) of c*a and c*b for a in the top parameters - and b in the bottom parameters interlace, i.e., the entries in the sorted - union of the two lists alternate between entries from the first and from - the second list. Used to determine algebraicity of the hypergeometric - function with these parameters with the Beukers-Heckman criterion. - - INPUT: - - - ``c`` -- an integer - - EXAMPLES:: - - sage: from sage.functions.hypergeometric_algebraic import Parameters - sage: pa = Parameters([1/3, 2/3], [1/2]) - sage: pa - ((1/3, 2/3), (1/2, 1)) - sage: pa.interlacing_criterion(1) - True - sage: pa.interlacing_criterion(5) - True - - :: - - sage: from sage.functions.hypergeometric_algebraic import Parameters - sage: pa = Parameters([1/8, 3/8, 5/8], [1/4, 1/2]) - sage: pa - ((1/8, 3/8, 5/8), (1/4, 1/2, 1)) - sage: pa.interlacing_criterion(1) - True - sage: pa.interlacing_criterion(3) - False - """ - previous_paren = 1 - for _, _, paren in self.christol_sorting(c): - if paren == previous_paren: - return False - previous_paren = paren - return True - - def q_christol_sorting(self, q): - r""" - Return a sorted list of pairs, one associated to each top parameter a, - and one associated to each bottom parameter b where the pair is either - (1/2 + (-a) % q, -1) or (1 + (-b) % q, 1). - - INPUT: - - - ``q`` -- an integer - - EXAMPLES:: - - sage: from sage.functions.hypergeometric_algebraic import Parameters - sage: pa = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) - sage: pa - ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) - sage: pa.q_christol_sorting(7) - [(2, 1), (2.5, -1), (3.5, -1), (5.5, -1), (6, 1), (7, 1)] - """ - A = [(1/2 + (-a) % q, -1) for a in self.top] - B = [(1 + (-b) % q, 1) for b in self.bottom] - return sorted(A + B) - - def q_parenthesis(self, q): - r""" - Return maximal value of the sum of all the second entries of the pairs - in a prefix of ``self.q_christol_sorting(q)`` and the first entry of - the last pair in the prefix of smallest length where this value is - attained. - - INPUT: - - - ``q`` -- an integer - - EXAMPLES:: - - sage: from sage.functions.hypergeometric_algebraic import Parameters - sage: pa = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) - sage: pa - ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) - sage: pa.q_christol_sorting(7) - [(2, 1), (2.5, -1), (3.5, -1), (5.5, -1), (6, 1), (7, 1)] - sage: pa.q_parenthesis(7) - (2, 1) - """ - parenthesis = maximum = shift = 0 - for s, paren in self.q_christol_sorting(q): - parenthesis += paren - if parenthesis > maximum: - maximum = parenthesis - shift = s - return shift, maximum - - def q_parenthesis_criterion(self, q): - r""" - Return ``True`` if in each prefix of the list - ``self.q_christol_sorting(q)`` there are at least as many pairs with - second entry -1 as pairs with second entry +1. Return ``False`` - otherwise. - - INPUT: - - - ``q`` -- an integer - - EXAMPLES:: - - sage: from sage.functions.hypergeometric_algebraic import Parameters - sage: pa = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) - sage: pa - ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) - sage: pa.q_christol_sorting(7) - [(2, 1), (2.5, -1), (3.5, -1), (5.5, -1), (6, 1), (7, 1)] - sage: pa.q_parenthesis_criterion(7) - False - sage: pa.q_christol_sorting(61) - [(15.5, -1), (20.5, -1), (25, 1), (30.5, -1), (37, 1), (61, 1)] - sage: pa.q_parenthesis_criterion(61) - True - """ - parenthesis = 0 - for _, paren in self.q_christol_sorting(q): - parenthesis += paren - if parenthesis > 0: - return False - return parenthesis <= 0 - - def q_interlacing_number(self, q): - r""" - Return the number of pairs in the list ``self.q_christol_sorting(q)`` - with second entry 1, that were preceded by a pair with second entry - -1. - - INPUT: - - - ``q`` -- an integer. - - EXAMPLES:: - - sage: from sage.functions.hypergeometric_algebraic import Parameters - sage: pa = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) - sage: pa - ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) - sage: pa.q_christol_sorting(7) - [(2, 1), (2.5, -1), (3.5, -1), (5.5, -1), (6, 1), (7, 1)] - sage: pa.q_interlacing_number(7) - 1 - """ - interlacing = 0 - previous_paren = 1 - for _, paren in self.q_christol_sorting(q): - if paren == 1 and previous_paren == -1: - interlacing += 1 - previous_paren = paren - return interlacing - - def remove_positive_integer_differences(self): - r""" - Return parameters, where pairs consisting of a top parameter - and a bottom parameter with positive integer differences are - removed, starting with pairs of minimal positive integer - difference. - - EXAMPLES:: - - sage: from sage.functions.hypergeometric_algebraic import Parameters - sage: pa = Parameters([5/2, -1/2, 5/3], [3/2, 1/3]) - sage: pa - ((-1/2, 5/3, 5/2), (1/3, 3/2, 1)) - sage: pa.remove_positive_integer_differences() - ((-1/2, 5/3), (1/3, 1)) - - The choice of which pair with integer differences to remove first - is important:: - - sage: pa = Parameters([4, 2, 1/2], [1, 3]) - sage: pa - ((1/2, 2, 4), (1, 3, 1)) - sage: pa.remove_positive_integer_differences() - ((1/2,), (1,)) - """ - differences = [] - top = list(self.top) - bottom = list(self.bottom) - for i in range(len(top)): - for j in range(len(bottom)): - diff = top[i] - bottom[j] - if diff in ZZ and diff > 0: - differences.append((diff, i, j)) - for _, i, j in sorted(differences): - if top[i] is not None and bottom[j] is not None: - top[i] = None - bottom[j] = None - return Parameters(top, bottom, add_one=False) - - def has_negative_integer_differences(self): - r""" - Return ``True`` if there exists a pair of a top parameter and a bottom - parameter, such that the top one minus the bottom one is a negative integer; - return ``False`` otherwise. - - EXAMPLES:: - - sage: from sage.functions.hypergeometric_algebraic import Parameters - sage: pa = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) - sage: pa - ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) - sage: pa.has_negative_integer_differences() - False - - :: - - sage: pa = Parameters([1/4, 1/3, 1/2], [2/5, 3/2]) - sage: pa - ((1/4, 1/3, 1/2), (2/5, 3/2, 1)) - sage: pa.has_negative_integer_differences() - True - """ - return any(a - b in ZZ and a < b for a in self.top for b in self.bottom) - - def shift(self, s): - r""" - Return the parameters obtained by adding s to each of them. - - INPUT: - - - ``s`` -- a rational number - - EXAMPLES:: - - sage: from sage.functions.hypergeometric_algebraic import Parameters - sage: pa = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) - sage: pa - ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) - sage: pa.shift(2) - ((1, 9/4, 7/3, 5/2), (12/5, 13/5, 3, 1)) - """ - top = [a+s for a in self.top] - bottom = [b+s for b in self.bottom] - return Parameters(top, bottom, add_one=False) - - def decimal_part(self): - r""" - Return the parameters obtained by taking the decimal part of each of - the parameters, where integers are assigned 1 instead of 0. - - EXAMPLES:: - - sage: from sage.functions.hypergeometric_algebraic import Parameters - sage: pa = Parameters([5/4, 1/3, 2], [2/5, -2/5]) - sage: pa - ((1/3, 5/4, 2), (-2/5, 2/5, 1)) - sage: pa.decimal_part() - ((1/4, 1/3, 1), (2/5, 3/5, 1)) - """ - top = [1 + a - ceil(a) for a in self.top] - bottom = [1 + b - ceil(b) for b in self.bottom] - return Parameters(top, bottom, add_one=False) - - def valuation_position(self, p, drift=0): - r""" - If `h_k`s denote the coefficients of the hypergeometric - series corresponding to these parameters, return the smallest - value of - - .. MATH:: - - \text{val}_p(h_k) + k \cdot \text{drift} - - and the first position where this minimum is reached. - - INPUT: - - - ``p`` -- a prime number - - - ``drift`` -- a rational number (default: ``0``) - - EXAMPLES:: - - sage: from sage.functions.hypergeometric_algebraic import Parameters - sage: pa = Parameters([1/5, 1/5, 1/5], [1/3, 3^10/5]) - sage: pa.valuation_position(3) - (-9, 1) - - When the relevant sequence is not bounded from below, the - tuple ``(-Infinity, None)`` is returned:: - - sage: pa.valuation_position(5) - (-Infinity, None) - - An example with a drift:: - - sage: pa.valuation_position(3, drift=-7/5) - (-54/5, 7) - """ - # We treat the case of parameters having p in the denominator - # When it happens, we remove the parameter and update the drift accordingly - top = [] - for a in self.top: - v = a.valuation(p) - if v < 0: - drift += v - else: - top.append(a) - bottom = [] - for b in self.bottom: - v = b.valuation(p) - if v < 0: - drift -= v - else: - bottom.append(b) - - # We check that we are inside the disk of convergence - diff = len(top) - len(bottom) - if ((p-1)*drift + diff, drift) < (0, 0): - return -infinity, None - - # Main part: computation of the valuation - # We use Christol's formula (see Lemma 3.1.10 of [CFVM2025]) - # with modification in order to take the drift into account - parameters = Parameters(top, bottom) - order = IntegerModRing(parameters.d)(p).multiplicative_order() - valuation = position = ZZ(0) - breaks = None - indices = {} - count = 0 - q = 1 - while True: - # We take into account the contribution of V({k/p^r}, p^r). - # We represent the partial sum until r by the table new_breaks. - # Its entries are triples (valuation, position, parameter): - # - parameter is the parameter corresponding to a point of - # discontinuity of the last summand V({k/p^r}, p^r) - # - valuation is the minimum of the partial sum on the - # range starting at this discontinuity point (included) - # and ending at the next one (excluded) - # - position is the first position the minimum is reached - # (The dictionary new_indices allows for finding rapidly - # an entry in new_breaks with a given parameter.) - # The table breaks and the dictionary indices correspond - # to the same data for r-1. - - pq = p * q - - # We compute the point of discontinuity of V({k/p^r}, p^r) - # and store them in AB - # Each entry of AB has the form (x, dw, parameter) where: - # - x is the position of the discontinuity point - # - dw is the *opposite* of the jump of V({k/p^r}, p^r) - # at this point - # (taking the opposite is useful for sorting reasons) - # - parameter is the corresponding parameter - A = [(1 + (-a) % pq, -1, a) for a in top] - B = [(1 + (-b) % pq, 1, b) for b in bottom] - AB = [(0, 0, 0)] + sorted(A + B) + [(pq, None, 0)] - - # We compute new_breaks - new_breaks = [] - new_indices = {} - w = 0 - cont = True - for i in range(len(AB) - 1): - x, dw, param = AB[i] # discontinuity point - y, _, right = AB[i+1] # next discontinuity point - w -= dw # the value of V({k/p^r}, p^r) on this interval - new_indices[param] = len(new_breaks) - if x == y: - # Case of empty interval - new_breaks.append((infinity, None, param)) - continue - # The variable complete stores whether the interval - # [x,y] covers all [0, p^(r-1)) modulo p^(r-1) - complete = (y - x >= q) - if complete and drift < 0: - interval = (y // q) - 1 - j = j0 = indices.get(right, 0) - else: - interval = x // q - j = j0 = indices.get(param, 0) - if (interval + 1) * drift + w < 0: - cont = False - if breaks is None: - # Case r = 1 - val = drift * interval - pos = q * interval - else: - # Case r > 1 - val = infinity - while True: - valj, posj, paramj = breaks[j] - valj += drift * interval - if valj < val: - val = valj - pos = posj + q * interval - j += 1 - if j >= len(breaks): - j = 0 - interval += 1 - if (not complete and paramj == right) or (complete and j == j0): - break - new_breaks.append((val + w, pos, param)) - - # The halting criterion - minimum = min(new_breaks) - if drift >= 0 and q > parameters.bound and minimum == new_breaks[0]: - if cont: - if count >= order: - return valuation, position - count += 1 - else: - return -infinity, None - - # We update the values for the next r - q = pq - drift = p*drift + diff - breaks = new_breaks - indices = new_indices - valuation, position, _ = minimum - - def dwork_image(self, p): - r""" - Return the parameters obtained by applying the Dwork map to each of - the parameters. The Dwork map `D_p(x)` of a p-adic integer x is defined - as the unique p-adic integer such that `p D_p(x) - x` is a nonnegative - integer smaller than p. - - INPUT: - - - ``p`` -- a prime number - - EXAMPLE:: - - sage: from sage.functions.hypergeometric_algebraic import Parameters - sage: pa = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) - sage: pa - ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) - sage: pa.dwork_image(7) - ((1/3, 1/2, 3/4), (1/5, 4/5, 1)) - - If `p` is not coprime to the common denominators of the parameters, - a ``ValueError`` is raised:: - - sage: pa.dwork_image(3) - Traceback (most recent call last): - ... - ValueError: denominators of parameters are not coprime to p - """ - try: - top = [(a + (-a) % p) / p for a in self.top] - bottom = [(b + (-b) % p) / p for b in self.bottom] - except ZeroDivisionError: - raise ValueError("denominators of parameters are not coprime to p") - return Parameters(top, bottom, add_one=False) - - def frobenius_order(self, p): - r""" - Return the Frobenius order of the hypergeometric function with this set - of parameters, that is the order of the Dwork map acting on the decimal - parts of the parameters. - - INPUT: - - - ``p`` -- a prime number - - EXAMPLES:: - - sage: from sage.functions.hypergeometric_algebraic import Parameters - sage: pa = Parameters([1/4, 1/3, 1/2], [2/5, 3/5]) - sage: pa - ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) - sage: pa.frobenius_order(7) - 2 - """ - param = self.decimal_part() - iter = param.dwork_image(p) - i = 1 - while param != iter: - iter = iter.dwork_image(p) - i += 1 - return i +from sage.functions.hypergeometric_parameters import HypergeometricParameters -# Hypergeometric functions -########################## - # Do we want to implement polynomial linear combinaison # of hypergeometric functions? # Advantages: @@ -811,7 +87,7 @@ def __init__(self, parent, arg1, arg2=None, scalar=None): function, they can be: - the top and bottom paramters - a hypergeometric function and ``None`` - - an instance of the class :class:`Parameters` and ``None`` + - an instance of the class :class:`HypergeometricParameters` and ``None`` - ``scalar`` -- an element in the base ring, the scalar by which the hypergeometric function is multiplied @@ -835,10 +111,10 @@ def __init__(self, parent, arg1, arg2=None, scalar=None): elif isinstance(arg1, HypergeometricAlgebraic): parameters = arg1._parameters scalar *= base(arg1._scalar) - elif isinstance(arg1, Parameters): + elif isinstance(arg1, HypergeometricParameters): parameters = arg1 else: - parameters = Parameters(arg1, arg2) + parameters = HypergeometricParameters(arg1, arg2) char = self.parent()._char if scalar: if any(b in ZZ and b < 0 for b in parameters.bottom): @@ -1581,70 +857,6 @@ def residue(self): # . s = pos # . h = self.shift(s) - def _val_pos(self): - p = self._p - d = self.denominator() - parameters = self._parameters - if d.gcd(p) > 1: - T = [] - B = [] - difference = 0 - for j in self.top(): - d = j.denominator().valuation(p) - difference += d - if not d: - T += [j] - for j in self.bottom(): - d = j.denominator().valuation(p) - difference -= d - if not d: - B += [j] - if difference > 0: - return -infinity, None - if difference < 0: - _, prec = self.parent()(T, B, self.scalar())._val_pos() - # This is the case when the p-adic valuation of the coefficients - # goes to +infinity, but if you remove all the coefficients with p - # in the denominator it goes to -infinity. - # The following claim is almost surely wrong. - if prec is None: - prec = self._parameters.bound - # Here I just check the valuation of the first few coefficients. - # There should be something better using q_g:parenthesis. - L = self.change_ring(Qp(p, 1)).power_series(prec+1).coefficients() - val = + infinity - pos = 0 - for j in range(len(L)): - v = L[j].valuation() - if v < val: - val = v - pos = j - return val, pos - if difference == 0: - return self.parent()(T, B, self.scalar())._val_pos() - u = 1 - if not parameters.parenthesis_criterion(u): - return -infinity, None - u = p % d - while u != 1: - if not parameters.parenthesis_criterion(u): - return -infinity, None - u = p*u % d - # From here, it is absolutely conjectural! - # ... and probably not quite correct - val = self._scalar.valuation() - pos = 0 - q = 1 - while True: - s, v = parameters.q_parenthesis(p) - if v == 0: - break - val -= self._e * v - pos += q*s - q *= p - parameters = parameters.shift(s).dwork_image(p) - return val, pos - def dwork_image(self): parameters = self._parameters.dwork_image(self._p) return self.parent()(parameters, scalar=self._scalar) @@ -1956,3 +1168,43 @@ def polynomial_ring(self): def power_series_ring(self, default_prec=None): return PowerSeriesRing(self.base_ring(), self._name, default_prec=default_prec) + + +# Helper functions +################## + +def insert_zeroes(P, n): + cs = P.list() + coeffs = n * len(cs) * [0] + for i in range(len(cs)): + coeffs[n*i] = cs[i] + return P.parent()(coeffs) + + +def kernel(M, repeat=2): + n = M.nrows() + m = M.ncols() + if n > m + 1: + raise RuntimeError + if n <= m: + K = M.base_ring().base_ring() + for _ in range(repeat): + a = K.random_element() + Me = matrix(n, m, [f(a) for f in M.list()]) + if Me.rank() == n: + return + for J in Subsets(range(m), n-1): + MJ = M.matrix_from_columns(J) + minor = MJ.delete_rows([0]).determinant() + if minor.is_zero(): + continue + ker = [minor] + for i in range(1, n): + minor = MJ.delete_rows([i]).determinant() + ker.append((-1)**i * minor) + Z = matrix(ker) * M + if not Z.is_zero(): + return + g = ker[0].leading_coefficient() * gcd(ker) + ker = [c//g for c in ker] + return ker diff --git a/src/sage/functions/hypergeometric_parameters.py b/src/sage/functions/hypergeometric_parameters.py new file mode 100644 index 00000000000..de594abbe26 --- /dev/null +++ b/src/sage/functions/hypergeometric_parameters.py @@ -0,0 +1,708 @@ +r""" +Parameters for hypergeometric functions + +AUTHORS: + +- Xavier Caruso, Florian Fürnsinn (2025-10): initial version +""" + +# *************************************************************************** +# Copyright (C) 2025 Xavier Caruso +# Florian Fürnsinn +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# https://www.gnu.org/licenses/ +# *************************************************************************** + +from sage.misc.cachefunc import cached_method + +from sage.functions.other import ceil +from sage.arith.functions import lcm + +from sage.rings.infinity import infinity +from sage.rings.integer_ring import ZZ +from sage.rings.rational_field import QQ +from sage.rings.finite_rings.integer_mod_ring import IntegerModRing + + +# HypergeometricParameters of hypergeometric functions +######################################## + +class HypergeometricParameters(): + r""" + Class for parameters of hypergeometric functions. + """ + def __init__(self, top, bottom, add_one=True): + r""" + Initialize this set of parameters. + + INPUT: + + - ``top`` -- list of top parameters + + - ``bottom`` -- list of bottom parameters + + - ``add_one`` -- boolean (default: ``True``), + if ``True``, add an additional one to the bottom + parameters. + + EXAMPLES:: + + sage: from sage.functions.hypergeometric_algebraic import HypergeometricParameters + sage: pa = HypergeometricParameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: pa + ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) + sage: type(pa) + + + By default, parameters are sorted, duplicates are removed and + a trailing `1` is added to the bottom parameters:: + + sage: HypergeometricParameters([1/2, 1/3, 2/3], [2/3]) + ((1/3, 1/2), (1,)) + + We can avoid adding the trailing `1` by passing ``add_one=False``:: + + sage: HypergeometricParameters([1/2, 1/3, 2/3], [2/3], add_one=False) + ((1/3, 1/2, 1), (1,)) + """ + try: + top = sorted([QQ(a) for a in top if a is not None]) + bottom = sorted([QQ(b) for b in bottom if b is not None]) + except TypeError: + raise NotImplementedError("parameters must be rational numbers") + i = j = 0 + while i < len(top) and j < len(bottom): + if top[i] == bottom[j]: + del top[i] + del bottom[j] + elif top[i] > bottom[j]: + j += 1 + else: + i += 1 + if add_one: + bottom.append(QQ(1)) + else: + try: + i = bottom.index(QQ(1)) + bottom.append(QQ(1)) + del bottom[i] + except ValueError: + bottom.append(QQ(1)) + top.append(QQ(1)) + top.sort() + self.top = tuple(top) + self.bottom = tuple(bottom) + if len(top) == 0 and len(bottom) == 0: + self.d = 1 + self.bound = 1 + else: + self.d = lcm([ a.denominator() for a in top ] + + [ b.denominator() for b in bottom ]) + self.bound = 2 * self.d * max(top + bottom) + 1 + + def __repr__(self): + r""" + Return a string representation of these parameters. + + EXAMPLES:: + + sage: from sage.functions.hypergeometric_algebraic import HypergeometricParameters + sage: pa = HypergeometricParameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: pa # indirect doctest + ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) + """ + return "(%s, %s)" % (self.top, self.bottom) + + def __hash__(self): + return hash((self.top, self.bottom)) + + def __eq__(self, other): + return (isinstance(other, HypergeometricParameters) + and self.top == other.top and self.bottom == other.bottom) + + def is_balanced(self): + r""" + Return ``True`` if there are as many top parameters as bottom + parameters; ``False`` otherwise. + + EXAMPLES:: + + sage: from sage.functions.hypergeometric_algebraic import HypergeometricParameters + sage: pa = HypergeometricParameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: pa + ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) + sage: pa.is_balanced() + True + + :: + + sage: pa = HypergeometricParameters([1/4, 1/3, 1/2], [2/5, 3/5], add_one=False) + sage: pa + ((1/4, 1/3, 1/2, 1), (2/5, 3/5, 1)) + sage: pa.is_balanced() + False + """ + return len(self.top) == len(self.bottom) + + @cached_method + def christol_sorting(self, c=1): + r""" + Return a sorted list of triples, where each triple is associated to one + of the parameters a, and consists of the decimal part of d*c*a (where + integers are assigned 1 instead of 0), the negative value of a, and a + sign (plus or minus 1), where top parameters are assigned -1 and bottom + parameters +1. Sorting the list lexecographically according to the + first two entries of the tuples sorts the corresponing parameters + according to the total ordering (defined on p.6 in [Chr1986]_). + + INPUT: + + - ``c`` -- an integer (default: ``1``) + + EXAMPLES:: + + sage: from sage.functions.hypergeometric_algebraic import HypergeometricParameters + sage: pa = HypergeometricParameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: pa + ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) + sage: pa.christol_sorting(7) + [(12, -3/5, 1), + (20, -1/3, -1), + (30, -1/2, -1), + (45, -1/4, -1), + (48, -2/5, 1), + (60, -1, 1)] + """ + d = self.d + A = [(d - (-d*c*a) % d, -a, -1) for a in self.top] + B = [(d - (-d*c*b) % d, -b, 1) for b in self.bottom] + return sorted(A + B) + + def parenthesis_criterion(self, c): + r""" + Return ``True`` if in each prefix of the list + ``self.christol_sorting(c)`` there are at least as many triples with + third entry -1 as triples with third entry +1. Return ``False`` + otherwise. + + INPUT: + + - ``c`` -- an integer + + EXAMPLES:: + + sage: from sage.functions.hypergeometric_algebraic import HypergeometricParameters + sage: pa = HypergeometricParameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: pa + ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) + sage: pa.christol_sorting(7) + [(12, -3/5, 1), + (20, -1/3, -1), + (30, -1/2, -1), + (45, -1/4, -1), + (48, -2/5, 1), + (60, -1, 1)] + sage: pa.parenthesis_criterion(7) + False + sage: pa.christol_sorting(1) + [(15, -1/4, -1), + (20, -1/3, -1), + (24, -2/5, 1), + (30, -1/2, -1), + (36, -3/5, 1), + (60, -1, 1)] + sage: pa.parenthesis_criterion(1) + True + """ + parenthesis = 0 + for _, _, paren in self.christol_sorting(c): + parenthesis += paren + if parenthesis > 0: + return False + return parenthesis <= 0 + + def interlacing_criterion(self, c): + r""" + Return ``True`` if the sorted lists of the decimal parts (where integers + are assigned 1 instead of 0) of c*a and c*b for a in the top parameters + and b in the bottom parameters interlace, i.e., the entries in the sorted + union of the two lists alternate between entries from the first and from + the second list. Used to determine algebraicity of the hypergeometric + function with these parameters with the Beukers-Heckman criterion. + + INPUT: + + - ``c`` -- an integer + + EXAMPLES:: + + sage: from sage.functions.hypergeometric_algebraic import HypergeometricParameters + sage: pa = HypergeometricParameters([1/3, 2/3], [1/2]) + sage: pa + ((1/3, 2/3), (1/2, 1)) + sage: pa.interlacing_criterion(1) + True + sage: pa.interlacing_criterion(5) + True + + :: + + sage: from sage.functions.hypergeometric_algebraic import HypergeometricParameters + sage: pa = HypergeometricParameters([1/8, 3/8, 5/8], [1/4, 1/2]) + sage: pa + ((1/8, 3/8, 5/8), (1/4, 1/2, 1)) + sage: pa.interlacing_criterion(1) + True + sage: pa.interlacing_criterion(3) + False + """ + previous_paren = 1 + for _, _, paren in self.christol_sorting(c): + if paren == previous_paren: + return False + previous_paren = paren + return True + + def q_christol_sorting(self, q): + r""" + Return a sorted list of pairs, one associated to each top parameter a, + and one associated to each bottom parameter b where the pair is either + (1/2 + (-a) % q, -1) or (1 + (-b) % q, 1). + + INPUT: + + - ``q`` -- an integer + + EXAMPLES:: + + sage: from sage.functions.hypergeometric_algebraic import HypergeometricParameters + sage: pa = HypergeometricParameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: pa + ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) + sage: pa.q_christol_sorting(7) + [(2, 1), (2.5, -1), (3.5, -1), (5.5, -1), (6, 1), (7, 1)] + """ + A = [(1/2 + (-a) % q, -1) for a in self.top] + B = [(1 + (-b) % q, 1) for b in self.bottom] + return sorted(A + B) + + def q_parenthesis(self, q): + r""" + Return maximal value of the sum of all the second entries of the pairs + in a prefix of ``self.q_christol_sorting(q)`` and the first entry of + the last pair in the prefix of smallest length where this value is + attained. + + INPUT: + + - ``q`` -- an integer + + EXAMPLES:: + + sage: from sage.functions.hypergeometric_algebraic import HypergeometricParameters + sage: pa = HypergeometricParameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: pa + ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) + sage: pa.q_christol_sorting(7) + [(2, 1), (2.5, -1), (3.5, -1), (5.5, -1), (6, 1), (7, 1)] + sage: pa.q_parenthesis(7) + (2, 1) + """ + parenthesis = maximum = shift = 0 + for s, paren in self.q_christol_sorting(q): + parenthesis += paren + if parenthesis > maximum: + maximum = parenthesis + shift = s + return shift, maximum + + def q_parenthesis_criterion(self, q): + r""" + Return ``True`` if in each prefix of the list + ``self.q_christol_sorting(q)`` there are at least as many pairs with + second entry -1 as pairs with second entry +1. Return ``False`` + otherwise. + + INPUT: + + - ``q`` -- an integer + + EXAMPLES:: + + sage: from sage.functions.hypergeometric_algebraic import HypergeometricParameters + sage: pa = HypergeometricParameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: pa + ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) + sage: pa.q_christol_sorting(7) + [(2, 1), (2.5, -1), (3.5, -1), (5.5, -1), (6, 1), (7, 1)] + sage: pa.q_parenthesis_criterion(7) + False + sage: pa.q_christol_sorting(61) + [(15.5, -1), (20.5, -1), (25, 1), (30.5, -1), (37, 1), (61, 1)] + sage: pa.q_parenthesis_criterion(61) + True + """ + parenthesis = 0 + for _, paren in self.q_christol_sorting(q): + parenthesis += paren + if parenthesis > 0: + return False + return parenthesis <= 0 + + def q_interlacing_number(self, q): + r""" + Return the number of pairs in the list ``self.q_christol_sorting(q)`` + with second entry 1, that were preceded by a pair with second entry + -1. + + INPUT: + + - ``q`` -- an integer. + + EXAMPLES:: + + sage: from sage.functions.hypergeometric_algebraic import HypergeometricParameters + sage: pa = HypergeometricParameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: pa + ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) + sage: pa.q_christol_sorting(7) + [(2, 1), (2.5, -1), (3.5, -1), (5.5, -1), (6, 1), (7, 1)] + sage: pa.q_interlacing_number(7) + 1 + """ + interlacing = 0 + previous_paren = 1 + for _, paren in self.q_christol_sorting(q): + if paren == 1 and previous_paren == -1: + interlacing += 1 + previous_paren = paren + return interlacing + + def remove_positive_integer_differences(self): + r""" + Return parameters, where pairs consisting of a top parameter + and a bottom parameter with positive integer differences are + removed, starting with pairs of minimal positive integer + difference. + + EXAMPLES:: + + sage: from sage.functions.hypergeometric_algebraic import HypergeometricParameters + sage: pa = HypergeometricParameters([5/2, -1/2, 5/3], [3/2, 1/3]) + sage: pa + ((-1/2, 5/3, 5/2), (1/3, 3/2, 1)) + sage: pa.remove_positive_integer_differences() + ((-1/2, 5/3), (1/3, 1)) + + The choice of which pair with integer differences to remove first + is important:: + + sage: pa = HypergeometricParameters([4, 2, 1/2], [1, 3]) + sage: pa + ((1/2, 2, 4), (1, 3, 1)) + sage: pa.remove_positive_integer_differences() + ((1/2,), (1,)) + """ + differences = [] + top = list(self.top) + bottom = list(self.bottom) + for i in range(len(top)): + for j in range(len(bottom)): + diff = top[i] - bottom[j] + if diff in ZZ and diff > 0: + differences.append((diff, i, j)) + for _, i, j in sorted(differences): + if top[i] is not None and bottom[j] is not None: + top[i] = None + bottom[j] = None + return HypergeometricParameters(top, bottom, add_one=False) + + def has_negative_integer_differences(self): + r""" + Return ``True`` if there exists a pair of a top parameter and a bottom + parameter, such that the top one minus the bottom one is a negative integer; + return ``False`` otherwise. + + EXAMPLES:: + + sage: from sage.functions.hypergeometric_algebraic import HypergeometricParameters + sage: pa = HypergeometricParameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: pa + ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) + sage: pa.has_negative_integer_differences() + False + + :: + + sage: pa = HypergeometricParameters([1/4, 1/3, 1/2], [2/5, 3/2]) + sage: pa + ((1/4, 1/3, 1/2), (2/5, 3/2, 1)) + sage: pa.has_negative_integer_differences() + True + """ + return any(a - b in ZZ and a < b for a in self.top for b in self.bottom) + + def shift(self, s): + r""" + Return the parameters obtained by adding s to each of them. + + INPUT: + + - ``s`` -- a rational number + + EXAMPLES:: + + sage: from sage.functions.hypergeometric_algebraic import HypergeometricParameters + sage: pa = HypergeometricParameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: pa + ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) + sage: pa.shift(2) + ((1, 9/4, 7/3, 5/2), (12/5, 13/5, 3, 1)) + """ + top = [a+s for a in self.top] + bottom = [b+s for b in self.bottom] + return HypergeometricParameters(top, bottom, add_one=False) + + def decimal_part(self): + r""" + Return the parameters obtained by taking the decimal part of each of + the parameters, where integers are assigned 1 instead of 0. + + EXAMPLES:: + + sage: from sage.functions.hypergeometric_algebraic import HypergeometricParameters + sage: pa = HypergeometricParameters([5/4, 1/3, 2], [2/5, -2/5]) + sage: pa + ((1/3, 5/4, 2), (-2/5, 2/5, 1)) + sage: pa.decimal_part() + ((1/4, 1/3, 1), (2/5, 3/5, 1)) + """ + top = [1 + a - ceil(a) for a in self.top] + bottom = [1 + b - ceil(b) for b in self.bottom] + return HypergeometricParameters(top, bottom, add_one=False) + + def valuation_position(self, p, drift=0): + r""" + If `h_k`s denote the coefficients of the hypergeometric + series corresponding to these parameters, return the smallest + value of + + .. MATH:: + + \text{val}_p(h_k) + k \cdot \text{drift} + + and the first position where this minimum is reached. + + INPUT: + + - ``p`` -- a prime number + + - ``drift`` -- a rational number (default: ``0``) + + EXAMPLES:: + + sage: from sage.functions.hypergeometric_algebraic import HypergeometricParameters + sage: pa = HypergeometricParameters([1/5, 1/5, 1/5], [1/3, 3^10/5]) + sage: pa.valuation_position(3) + (-9, 1) + + When the relevant sequence is not bounded from below, the + tuple ``(-Infinity, None)`` is returned:: + + sage: pa.valuation_position(5) + (-Infinity, None) + + An example with a drift:: + + sage: pa.valuation_position(3, drift=-7/5) + (-54/5, 7) + """ + # We treat the case of parameters having p in the denominator + # When it happens, we remove the parameter and update the drift accordingly + top = [] + for a in self.top: + v = a.valuation(p) + if v < 0: + drift += v + else: + top.append(a) + bottom = [] + for b in self.bottom: + v = b.valuation(p) + if v < 0: + drift -= v + else: + bottom.append(b) + + # We check that we are inside the disk of convergence + diff = len(top) - len(bottom) + if ((p-1)*drift + diff, drift) < (0, 0): + return -infinity, None + + # Main part: computation of the valuation + # We use Christol's formula (see Lemma 3.1.10 of [CFVM2025]) + # with modification in order to take the drift into account + parameters = HypergeometricParameters(top, bottom) + order = IntegerModRing(parameters.d)(p).multiplicative_order() + valuation = position = ZZ(0) + breaks = None + indices = {} + count = 0 + q = 1 + while True: + # We take into account the contribution of V({k/p^r}, p^r). + # We represent the partial sum until r by the table new_breaks. + # Its entries are triples (valuation, position, parameter): + # - parameter is the parameter corresponding to a point of + # discontinuity of the last summand V({k/p^r}, p^r) + # - valuation is the minimum of the partial sum on the + # range starting at this discontinuity point (included) + # and ending at the next one (excluded) + # - position is the first position the minimum is reached + # (The dictionary new_indices allows for finding rapidly + # an entry in new_breaks with a given parameter.) + # The table breaks and the dictionary indices correspond + # to the same data for r-1. + + pq = p * q + + # We compute the point of discontinuity of V({k/p^r}, p^r) + # and store them in AB + # Each entry of AB has the form (x, dw, parameter) where: + # - x is the position of the discontinuity point + # - dw is the *opposite* of the jump of V({k/p^r}, p^r) + # at this point + # (taking the opposite is useful for sorting reasons) + # - parameter is the corresponding parameter + A = [(1 + (-a) % pq, -1, a) for a in top] + B = [(1 + (-b) % pq, 1, b) for b in bottom] + AB = [(0, 0, 0)] + sorted(A + B) + [(pq, None, 0)] + + # We compute new_breaks + new_breaks = [] + new_indices = {} + w = 0 + cont = True + for i in range(len(AB) - 1): + x, dw, param = AB[i] # discontinuity point + y, _, right = AB[i+1] # next discontinuity point + w -= dw # the value of V({k/p^r}, p^r) on this interval + new_indices[param] = len(new_breaks) + if x == y: + # Case of empty interval + new_breaks.append((infinity, None, param)) + continue + # The variable complete stores whether the interval + # [x,y] covers all [0, p^(r-1)) modulo p^(r-1) + complete = (y - x >= q) + if complete and drift < 0: + interval = (y // q) - 1 + j = j0 = indices.get(right, 0) + else: + interval = x // q + j = j0 = indices.get(param, 0) + if (interval + 1) * drift + w < 0: + cont = False + if breaks is None: + # Case r = 1 + val = drift * interval + pos = q * interval + else: + # Case r > 1 + val = infinity + while True: + valj, posj, paramj = breaks[j] + valj += drift * interval + if valj < val: + val = valj + pos = posj + q * interval + j += 1 + if j >= len(breaks): + j = 0 + interval += 1 + if (not complete and paramj == right) or (complete and j == j0): + break + new_breaks.append((val + w, pos, param)) + + # The halting criterion + minimum = min(new_breaks) + if drift >= 0 and q > parameters.bound and minimum == new_breaks[0]: + if cont: + if count >= order: + return valuation, position + count += 1 + else: + return -infinity, None + + # We update the values for the next r + q = pq + drift = p*drift + diff + breaks = new_breaks + indices = new_indices + valuation, position, _ = minimum + + def dwork_image(self, p): + r""" + Return the parameters obtained by applying the Dwork map to each of + the parameters. The Dwork map `D_p(x)` of a p-adic integer x is defined + as the unique p-adic integer such that `p D_p(x) - x` is a nonnegative + integer smaller than p. + + INPUT: + + - ``p`` -- a prime number + + EXAMPLE:: + + sage: from sage.functions.hypergeometric_algebraic import HypergeometricParameters + sage: pa = HypergeometricParameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: pa + ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) + sage: pa.dwork_image(7) + ((1/3, 1/2, 3/4), (1/5, 4/5, 1)) + + If `p` is not coprime to the common denominators of the parameters, + a ``ValueError`` is raised:: + + sage: pa.dwork_image(3) + Traceback (most recent call last): + ... + ValueError: denominators of parameters are not coprime to p + """ + try: + top = [(a + (-a) % p) / p for a in self.top] + bottom = [(b + (-b) % p) / p for b in self.bottom] + except ZeroDivisionError: + raise ValueError("denominators of parameters are not coprime to p") + return HypergeometricParameters(top, bottom, add_one=False) + + def frobenius_order(self, p): + r""" + Return the Frobenius order of the hypergeometric function with this set + of parameters, that is the order of the Dwork map acting on the decimal + parts of the parameters. + + INPUT: + + - ``p`` -- a prime number + + EXAMPLES:: + + sage: from sage.functions.hypergeometric_algebraic import HypergeometricParameters + sage: pa = HypergeometricParameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: pa + ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) + sage: pa.frobenius_order(7) + 2 + """ + param = self.decimal_part() + iter = param.dwork_image(p) + i = 1 + while param != iter: + iter = iter.dwork_image(p) + i += 1 + return i diff --git a/src/sage/functions/meson.build b/src/sage/functions/meson.build index 4172571c240..4b781dadfcf 100644 --- a/src/sage/functions/meson.build +++ b/src/sage/functions/meson.build @@ -10,6 +10,7 @@ py.install_sources( 'hyperbolic.py', 'hypergeometric.py', 'hypergeometric_algebraic.py', + 'hypergeometric_parameters.py', 'jacobi.py', 'log.py', 'min_max.py', From 27d4d28e96ffbf020b608005d6726cdf3ffb9ba6 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Sun, 16 Nov 2025 16:14:07 +0100 Subject: [PATCH 53/93] include in the documentation --- src/doc/en/reference/references/index.rst | 4 ++-- src/sage/functions/hypergeometric.py | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/doc/en/reference/references/index.rst b/src/doc/en/reference/references/index.rst index e2e9021ce16..dd761d424d0 100644 --- a/src/doc/en/reference/references/index.rst +++ b/src/doc/en/reference/references/index.rst @@ -1650,7 +1650,7 @@ REFERENCES: IV. The quotient groups of the lower central series, Ann. of Math. 68 (1958) 81--95. -.. [CFV2025] Xavier Caruso, Florian Fürnsinn, Daniel Vargas-Montoya, +.. [CFV2025] Xavier Caruso, Florian Fürnsinn, Daniel Vargas-Montoya, *Galois groups of reductions modulo p of D-finite series*, :arxiv:`2504.09429`, 2025. @@ -1728,7 +1728,7 @@ REFERENCES: .. [ChenDB] Eric Chen, Online database of two-weight codes, http://moodle.tec.hkr.se/~chen/research/2-weight-codes/search.php -.. [Chr1986] Gilles Christol, *Fonctions hypergéométriques bornées*. +.. [Chr1986] Gilles Christol, *Fonctions hypergéométriques bornées*. Groupe de travail d’analyse ultramétrique 14 (1986-87), exp. no 8, p. 1-16 .. [CHK2001] Keith D. Cooper, Timothy J. Harvey and Ken Kennedy. *A diff --git a/src/sage/functions/hypergeometric.py b/src/sage/functions/hypergeometric.py index 9dfd34bca7a..6bf1fb51f42 100644 --- a/src/sage/functions/hypergeometric.py +++ b/src/sage/functions/hypergeometric.py @@ -4,6 +4,10 @@ This module implements manipulation of infinite hypergeometric series represented in standard parametric form (as `\,_pF_q` functions). +For a more algebraic treatment of hypergeometric functions +(including reduction modulo primes and `p`-adic properties), +we refer to :mod:`sage.functions.hypergeometric_algebraic`. + AUTHORS: - Fredrik Johansson (2010): initial version From 7a6606908344ffea7660a9784431058d041dbd3f Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Sun, 16 Nov 2025 16:19:52 +0100 Subject: [PATCH 54/93] small rewriting --- src/sage/functions/hypergeometric_parameters.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/sage/functions/hypergeometric_parameters.py b/src/sage/functions/hypergeometric_parameters.py index de594abbe26..dc4a2cc65f8 100644 --- a/src/sage/functions/hypergeometric_parameters.py +++ b/src/sage/functions/hypergeometric_parameters.py @@ -487,15 +487,15 @@ def decimal_part(self): def valuation_position(self, p, drift=0): r""" - If `h_k`s denote the coefficients of the hypergeometric - series corresponding to these parameters, return the smallest - value of + If the `h_k`s are the coefficients of the hypergeometric + series corresponding to these parameters and `\delta` is + the drift, return the smallest value of .. MATH:: - \text{val}_p(h_k) + k \cdot \text{drift} + \text{val}_p(h_k) + \delta k - and the first position where this minimum is reached. + and the first index `k` where this minimum is reached. INPUT: From 0517f0bc35d2b5b05873f2a00c5867b647d6f554 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Sun, 16 Nov 2025 17:16:39 +0100 Subject: [PATCH 55/93] fix again halting criterion --- src/sage/functions/hypergeometric_parameters.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/sage/functions/hypergeometric_parameters.py b/src/sage/functions/hypergeometric_parameters.py index dc4a2cc65f8..f1a19cddf32 100644 --- a/src/sage/functions/hypergeometric_parameters.py +++ b/src/sage/functions/hypergeometric_parameters.py @@ -540,7 +540,8 @@ def valuation_position(self, p, drift=0): # We check that we are inside the disk of convergence diff = len(top) - len(bottom) - if ((p-1)*drift + diff, drift) < (0, 0): + growth = (p-1)*drift + diff + if (growth, drift) < (0, 0): return -infinity, None # Main part: computation of the valuation @@ -605,7 +606,7 @@ def valuation_position(self, p, drift=0): else: interval = x // q j = j0 = indices.get(param, 0) - if (interval + 1) * drift + w < 0: + if growth == 0 and (interval+1)*drift + w < 0: cont = False if breaks is None: # Case r = 1 @@ -630,7 +631,7 @@ def valuation_position(self, p, drift=0): # The halting criterion minimum = min(new_breaks) - if drift >= 0 and q > parameters.bound and minimum == new_breaks[0]: + if drift >= 0 and q > parameters.bound: if cont: if count >= order: return valuation, position From 714f0e137a1730fc3fdd827d4888670663f124b5 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Sun, 16 Nov 2025 17:35:00 +0100 Subject: [PATCH 56/93] raise an error if p is not prime --- src/doc/en/reference/functions/index.rst | 1 + src/sage/functions/hypergeometric_algebraic.py | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/doc/en/reference/functions/index.rst b/src/doc/en/reference/functions/index.rst index 3916eb0c260..2a0991472e3 100644 --- a/src/doc/en/reference/functions/index.rst +++ b/src/doc/en/reference/functions/index.rst @@ -21,6 +21,7 @@ Built-in Functions sage/functions/other sage/functions/special sage/functions/hypergeometric + sage/functions/hypergeometric_algebraic sage/functions/jacobi sage/functions/airy sage/functions/bessel diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index c6604d4b10b..0eefb7a700b 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -1,5 +1,5 @@ r""" -Algebraic properties of hypergeometric functions +Hypergeometric functions over arbitrary rings [Tutorial] @@ -688,7 +688,16 @@ def valuation(self, p): sage: g = 5*f sage: g.valuation(5) 1 + + TESTS:: + + sage: g.valuation(9) + Traceback (most recent call last): + ... + ValueError: p must be a prime number """ + if not p.is_prime(): + raise ValueError("p must be a prime number") val, _ = self._parameters.valuation_position(p) return val + self._scalar.valuation(p) From e3e9fc92a0b93fe6d9e650bc4938d36c6fe60d0b Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Mon, 17 Nov 2025 06:03:49 +0100 Subject: [PATCH 57/93] more bugs fixed --- .../functions/hypergeometric_algebraic.py | 44 ++++++++++--- .../functions/hypergeometric_parameters.py | 63 +++++++++++-------- 2 files changed, 72 insertions(+), 35 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 0eefb7a700b..7c9107a0d87 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -470,7 +470,7 @@ def _compute_coeffs(self, prec): c /= b + i coeffs.append(c) - def power_series(self, prec): + def power_series(self, prec=20): r""" Return the power series representation of this hypergeometric function up to a given precision. @@ -669,7 +669,7 @@ def __mod__(self, p): h = self.change_ring(Qp(p, 1)) return h.residue() - def valuation(self, p): + def valuation(self, p, position=False): r""" Return the p-adic valuation of this hypergeometric function, i.e., the maximal s, such that p^(-s) times this hypergeometric function has @@ -679,6 +679,10 @@ def valuation(self, p): - ``p`` -- a prime number + - ``position`` -- a boolean (default: ``False``); if ``True``, return + also the first index in the series expansion at which the valuation + is attained. + EXAMPLES:: sage: S. = QQ[x] @@ -689,6 +693,21 @@ def valuation(self, p): sage: g.valuation(5) 1 + An example where we ask for the position:: + + sage: h = hypergeometric([1/5, 1/5, 1/5], [1/3, 9/5], x) + sage: h.valuation(3, position=True) + (-1, 1) + + We can check that the coefficient in `x` in the series expansion + has indeed valuation `-1`:: + + sage: s = h.power_series() + sage: s + 1 + 1/75*x + 27/8750*x^2 + ... + O(x^20) + sage: s[1].valuation(3) + -1 + TESTS:: sage: g.valuation(9) @@ -698,8 +717,12 @@ def valuation(self, p): """ if not p.is_prime(): raise ValueError("p must be a prime number") - val, _ = self._parameters.valuation_position(p) - return val + self._scalar.valuation(p) + val, pos = self._parameters.valuation_position(p) + val += self._scalar.valuation(p) + if position: + return val, pos + else: + return val def has_good_reduction(self, p): r""" @@ -890,10 +913,13 @@ def log_radius_of_convergence(self): log_radius -= step return log_radius - def valuation(self, log_radius=0): + def valuation(self, log_radius=0, position=False): drift = -log_radius / self._e - val, _ = self._parameters.valuation_position(self._p, drift) - return val + val, pos = self._parameters.valuation_position(self._p, drift) + if position: + return val, pos + else: + return val def newton_polygon(self, log_radius): raise NotImplementedError @@ -915,7 +941,7 @@ def __init__(self, parent, arg1, arg2=None, scalar=None): self._p = p = self.base_ring().cardinality() self._coeffs = [Qp(p, 1)(self._scalar)] - def power_series(self, prec): + def power_series(self, prec=20): S = self.parent().power_series_ring() self._compute_coeffs(prec) try: @@ -1016,7 +1042,7 @@ def dwork_relation(self): Ps = {} for r in range(p): params = parameters.shift(r).dwork_image(p) - _, s = Hp(params)._val_pos() + _, s = params.valuation_position(p) h = H(params.shift(s)) e = s*p + r if e >= len(coeffs): diff --git a/src/sage/functions/hypergeometric_parameters.py b/src/sage/functions/hypergeometric_parameters.py index f1a19cddf32..326660412e3 100644 --- a/src/sage/functions/hypergeometric_parameters.py +++ b/src/sage/functions/hypergeometric_parameters.py @@ -525,6 +525,8 @@ def valuation_position(self, p, drift=0): # When it happens, we remove the parameter and update the drift accordingly top = [] for a in self.top: + if a in ZZ and a <= 0: + raise NotImplementedError # TODO v = a.valuation(p) if v < 0: drift += v @@ -547,6 +549,7 @@ def valuation_position(self, p, drift=0): # Main part: computation of the valuation # We use Christol's formula (see Lemma 3.1.10 of [CFVM2025]) # with modification in order to take the drift into account + n = len(top) + len(bottom) + 1 parameters = HypergeometricParameters(top, bottom) order = IntegerModRing(parameters.d)(p).multiplicative_order() valuation = position = ZZ(0) @@ -587,33 +590,36 @@ def valuation_position(self, p, drift=0): new_breaks = [] new_indices = {} w = 0 - cont = True - for i in range(len(AB) - 1): + unbounded = False + for i in range(n): x, dw, param = AB[i] # discontinuity point y, _, right = AB[i+1] # next discontinuity point w -= dw # the value of V({k/p^r}, p^r) on this interval new_indices[param] = len(new_breaks) if x == y: # Case of empty interval - new_breaks.append((infinity, None, param)) - continue - # The variable complete stores whether the interval - # [x,y] covers all [0, p^(r-1)) modulo p^(r-1) - complete = (y - x >= q) - if complete and drift < 0: - interval = (y // q) - 1 - j = j0 = indices.get(right, 0) - else: - interval = x // q - j = j0 = indices.get(param, 0) - if growth == 0 and (interval+1)*drift + w < 0: - cont = False - if breaks is None: + val = infinity + pos = None + elif breaks is None: # Case r = 1 - val = drift * interval - pos = q * interval + if drift < 0: + pos = y - 1 + else: + pos = x + val = drift * pos else: # Case r > 1 + # The variable complete stores whether the interval + # [x,y] covers all [0, p^(r-1)) modulo p^(r-1) + complete = (y - x >= q) + if complete and drift < 0: + interval = ((y-1) // q) - 1 + j = j0 = indices[right] + else: + interval = max(0, (x-1) // q) + j = j0 = indices[param] + if growth == 0 and interval*drift + w < 0: + unbounded = True val = infinity while True: valj, posj, paramj = breaks[j] @@ -625,26 +631,31 @@ def valuation_position(self, p, drift=0): if j >= len(breaks): j = 0 interval += 1 - if (not complete and paramj == right) or (complete and j == j0): + if j == j0 or (not complete and breaks[j][2] == right): break new_breaks.append((val + w, pos, param)) # The halting criterion minimum = min(new_breaks) + valuation, position, _ = minimum if drift >= 0 and q > parameters.bound: - if cont: - if count >= order: - return valuation, position - count += 1 - else: - return -infinity, None + if growth == 0: + if unbounded: + return -infinity, None + if all(new_breaks[j][0] >= breaks[j][0] for j in range(n)): + if count >= order: + return valuation, position + count += 1 + else: + count = 0 + elif drift >= len(top) and minimum == new_breaks[0]: + return valuation, position # We update the values for the next r q = pq drift = p*drift + diff breaks = new_breaks indices = new_indices - valuation, position, _ = minimum def dwork_image(self, p): r""" From a4afba8c718a3dfcb66002b9f511176d5f7425af Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Mon, 17 Nov 2025 11:49:26 +0100 Subject: [PATCH 58/93] Tate series and evaluation --- .../functions/hypergeometric_algebraic.py | 47 +++++++++++++++++-- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 7c9107a0d87..5bb081ecc0c 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -26,7 +26,7 @@ from sage.misc.misc_c import prod from sage.misc.functional import log -from sage.functions.other import ceil +from sage.functions.other import floor, ceil from sage.functions.hypergeometric import hypergeometric from sage.arith.misc import gcd from sage.matrix.constructor import matrix @@ -58,6 +58,7 @@ from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing from sage.rings.power_series_ring import PowerSeriesRing +from sage.rings.tate_algebra import TateAlgebra from sage.rings.polynomial.ore_polynomial_ring import OrePolynomialRing from sage.functions.hypergeometric_parameters import HypergeometricParameters @@ -924,11 +925,49 @@ def valuation(self, log_radius=0, position=False): def newton_polygon(self, log_radius): raise NotImplementedError - def tate_series(self): - raise NotImplementedError + def _truncation_bound(self, log_radius, prec): + convergence = self.log_radius_of_convergence() + margin = convergence - log_radius + if margin <= 0: + raise ValueError("outside the domain of convergence") + # We choose an intermediate log_radius + # It can be anything between convergence and log_radius + # but it seems that the following works well (in the sense + # that it gives good bounds at the end). + lr = convergence - margin / max(prec, 2) + val = self.valuation(lr) + # Now, we know that + # val(h_k) >= -lr*k + val + # and we want to find k such that + # val(h_k) >= -log_radius*k + prec + # So we just solve the equation. + k = (prec - val) / (lr - log_radius) + return 1 + max(0, floor(k)) + + def tate_series(self, log_radius, prec=None): + K = self.base_ring() + name = self.parent().variable_name() + S = TateAlgebra(K, log_radii=[log_radius], names=name) + if prec is None: + prec = self.base_ring().precision_cap() + trunc = self._truncation_bound(log_radius, prec) + self._compute_coeffs(trunc) + coeffs = {(i,): self._coeffs[i] for i in range(trunc)} + return self._scalar * S(coeffs, prec) def __call__(self, x): - raise NotImplementedError + K = self.base_ring() + x = K(x) + val = min(x.valuation(), x.precision_absolute()) + if val is infinity: + return K.one() + w = self.valuation(-val) + prec = w + K.precision_cap() + trunc = self._truncation_bound(-val, prec) + self._compute_coeffs(trunc) + ans = sum(self._coeffs[i] * x**i for i in range(trunc)) + ans = ans.add_bigoh(prec) + return self._scalar * ans # Over prime finite fields From 67a2a690be3cc871f3535e8f86c323ae3cf56cda Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Mon, 17 Nov 2025 16:58:33 +0100 Subject: [PATCH 59/93] fix O(...) in printing --- src/sage/rings/tate_algebra_element.pyx | 29 ++++++++++++++++--------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/sage/rings/tate_algebra_element.pyx b/src/sage/rings/tate_algebra_element.pyx index ade9ea418ad..c109c2a967c 100644 --- a/src/sage/rings/tate_algebra_element.pyx +++ b/src/sage/rings/tate_algebra_element.pyx @@ -1123,7 +1123,7 @@ cdef class TateAlgebraElement(CommutativeAlgebraElement): sage: S. = TateAlgebra(Qp(5), log_radii=(1,0)) sage: f = 5*x sage: f.add_bigoh(1) - (5 + O(5^2))*x + O(5 * ) + (5 + O(5^2))*x + O(5 * <5*x, y>) """ self._is_normalized = True if self._prec is Infinity: @@ -1169,6 +1169,15 @@ cdef class TateAlgebraElement(CommutativeAlgebraElement): sage: A(x + 2*x^2 + x^3, prec=5) ...00001*x^3 + ...00001*x + ...00010*x^2 + O(2^5 * ) + + TESTS:: + + sage: S. = TateAlgebra(R, log_radii=[-1]) + sage: S(x, 5) + ...0001*x + O(2^5 * ) + sage: S. = TateAlgebra(R, log_radii=[1]) + sage: S(x, 5) + ...000001*x + O(2^5 * <2*x>) """ vars = self._parent.variable_names() s = "" @@ -1191,14 +1200,14 @@ cdef class TateAlgebraElement(CommutativeAlgebraElement): for i in range(len(vars)): if lr[i] == 0: sv.append(vars[i]) - elif lr[i] == -1: - sv.append("%s*%s" % (su, vars[i])) elif lr[i] == 1: + sv.append("%s*%s" % (su, vars[i])) + elif lr[i] == -1: sv.append("%s/%s" % (vars[i], su)) - elif lr[i] < 0: - sv.append("%s^%s*%s" % (su, -lr[i], vars[i])) + elif lr[i] > 0: + sv.append("%s^%s*%s" % (su, lr[i], vars[i])) else: - sv.append("%s/%s^%s" % (vars[i], su, lr[i])) + sv.append("%s/%s^%s" % (vars[i], su, -lr[i])) sv = ", ".join(sv) if self._prec == 0: s += "O(<%s>)" % sv @@ -2534,8 +2543,8 @@ cdef class TateAlgebraElement(CommutativeAlgebraElement): However `\log(1+x)` converges on a smaller disk:: sage: f.restriction(-1).log() - ...000000001*x + ...0000000.1*x^3 + ...111111*x^2 + ... - + O(3^10 * <3*x, 3*y>) + ...000000001*x + ...0000000.1*x^3 + ...11111111*x^2 + ... + + O(3^10 * ) TESTS:: @@ -2692,8 +2701,8 @@ cdef class TateAlgebraElement(CommutativeAlgebraElement): However `\exp(x)` converges on a smaller disk:: sage: f.restriction(-1).exp() - ...0000000001 + ...000000001*x + ...1111111.2*x^3 + ...111112*x^2 - + ... + O(3^10 * <3*x, 3*y>) + ...0000000001 + ...000000001*x + ...1111111.2*x^3 + ...11111112*x^2 + + ... + O(3^10 * ) TESTS:: From 0a878d626528f53e13591dd288f7d0cb0ffd1d61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20F=C3=BCrnsinn?= Date: Tue, 18 Nov 2025 18:10:15 +0100 Subject: [PATCH 60/93] doc --- src/doc/en/reference/references/index.rst | 5 +++++ .../functions/hypergeometric_algebraic.py | 22 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/doc/en/reference/references/index.rst b/src/doc/en/reference/references/index.rst index dd761d424d0..8d65c26404e 100644 --- a/src/doc/en/reference/references/index.rst +++ b/src/doc/en/reference/references/index.rst @@ -2888,6 +2888,11 @@ REFERENCES: toric varieties defined by atomic lattices*. Inventiones Mathematicae. **155** (2004), no. 3, pp. 515-536. +.. [FY2024] Florian Fürnsinn, Sergey Yurkevich. + *Algebraicity of hypergeometric functions with arbitrary parameters*, + Bulletin of the London Mathematical Society. **56** (2024), + pp. 2824-2846. :doi:`10.1112/blms.13103`, :arxiv:`2308.12855` (2023). + .. [FZ2001] \S. Fomin and A. Zelevinsky. *Cluster algebras I. Foundations*, \J. Amer. Math. Soc. **15** (2002), no. 2, pp. 497-529. :arxiv:`math/0104151` (2001). diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 5bb081ecc0c..2efe4c5be2a 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -813,6 +813,28 @@ def good_reduction_primes(self): return Primes(modulus=d, classes=goods, exceptions=exceptions) def is_algebraic(self): + r""" + Return ``True`` if this hypergeometric function is algebraic over + the rational functions, return ``False`` otherwise. + + EXAMPLES:: + + sage: S. = QQ[] + sage: f = hypergeometric([1/3, 2/3], [1/2], x) + sage: f.is_algebraic() + True + sage: g = hypergeometric([1/3, 2/3, 1/4], [5/4, 1/2], x) + sage: g.is_algebraic() + False + + ALGORITHM: + + We rely on the (Christol-)Beukers-Heckmann interlacing criterion + (see [Chr1986]_, p.15, Cor.; [BeukersHeckman]_, Thm. 4.5). For integer + differences between parameters we follow the flowchart in + [FY2024]_, Fig. 1. + + """ if any(a in ZZ and a <= 0 for a in self.top()): return True if not self._parameters.is_balanced(): From 1f53e212ad523039f780097ce3b18bbd656f784e Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Wed, 19 Nov 2025 14:04:03 +0100 Subject: [PATCH 61/93] mean weight and transitive closure --- src/sage/matrix/action.pyx | 2 +- src/sage/matrix/matrix_space.py | 31 +++++++++---- src/sage/rings/semirings/meson.build | 1 + src/sage/rings/semirings/tropical_matrix.py | 50 +++++++++++++++++++++ 4 files changed, 75 insertions(+), 9 deletions(-) create mode 100644 src/sage/rings/semirings/tropical_matrix.py diff --git a/src/sage/matrix/action.pyx b/src/sage/matrix/action.pyx index 732a1312ca1..f40cf212066 100644 --- a/src/sage/matrix/action.pyx +++ b/src/sage/matrix/action.pyx @@ -279,7 +279,7 @@ cdef class MatrixMatrixAction(MatrixMulAction): B = B.dense_matrix() else: A = A.dense_matrix() - assert type(A) is type(B), (type(A), type(B)) + # assert type(A) is type(B), (type(A), type(B)) prod = A._matrix_times_matrix_(B) if A._subdivisions is not None or B._subdivisions is not None: Asubs = A.subdivisions() diff --git a/src/sage/matrix/matrix_space.py b/src/sage/matrix/matrix_space.py index eafab99b0e9..32f4162c85c 100644 --- a/src/sage/matrix/matrix_space.py +++ b/src/sage/matrix/matrix_space.py @@ -50,6 +50,8 @@ from sage.misc.lazy_attribute import lazy_attribute from sage.misc.superseded import deprecated_function_alias from sage.misc.persist import register_unpickle_override +from sage.categories.sets_cat import Sets +from sage.categories.semirings import Semirings from sage.categories.rings import Rings from sage.categories.fields import Fields from sage.categories.enumerated_sets import EnumeratedSets @@ -60,7 +62,6 @@ feature=Meataxe()) lazy_import('sage.groups.matrix_gps.matrix_group', ['MatrixGroup_base']) -_Rings = Rings() _Fields = Fields() @@ -320,6 +321,15 @@ def get_matrix_class(R, nrows, ncols, sparse, implementation): else: return matrix_laurent_mpolynomial_dense.Matrix_laurent_mpolynomial_dense + try: + from sage.rings.semirings.tropical_semiring import TropicalSemiring + except ImportError: + pass + else: + if isinstance(R, TropicalSemiring): + from sage.rings.semirings import tropical_matrix + return tropical_matrix.Matrix_tropical_dense + # The fallback from sage.matrix.matrix_generic_dense import Matrix_generic_dense return Matrix_generic_dense @@ -725,8 +735,8 @@ def __classcall__(cls, base_ring, sage: MS2._my_option False """ - if base_ring not in _Rings: - raise TypeError("base_ring (=%s) must be a ring" % base_ring) + if base_ring not in Semirings(): + raise TypeError("base_ring (=%s) must be a ring or a semiring" % base_ring) if ncols_or_column_keys is not None: try: @@ -898,12 +908,17 @@ def __init__(self, base_ring, nrows, ncols, sparse, implementation): from sage.categories.modules import Modules from sage.categories.algebras import Algebras - if nrows == ncols: - category = Algebras(base_ring.category()) + if base_ring in Rings(): + if nrows == ncols: + category = Algebras(base_ring.category()) + else: + category = Modules(base_ring.category()) + category = category.WithBasis().FiniteDimensional() else: - category = Modules(base_ring.category()) - - category = category.WithBasis().FiniteDimensional() + if nrows == ncols: + category = Semirings() + else: + category = Sets() if not self.__nrows or not self.__ncols: is_finite = True diff --git a/src/sage/rings/semirings/meson.build b/src/sage/rings/semirings/meson.build index b92ea837850..0a9762b756d 100644 --- a/src/sage/rings/semirings/meson.build +++ b/src/sage/rings/semirings/meson.build @@ -2,6 +2,7 @@ py.install_sources( '__init__.py', 'all.py', 'non_negative_integer_semiring.py', + 'tropical_matrix.py', 'tropical_mpolynomial.py', 'tropical_polynomial.py', 'tropical_semiring.pyx', diff --git a/src/sage/rings/semirings/tropical_matrix.py b/src/sage/rings/semirings/tropical_matrix.py new file mode 100644 index 00000000000..3a5061ccaec --- /dev/null +++ b/src/sage/rings/semirings/tropical_matrix.py @@ -0,0 +1,50 @@ +from sage.rings.infinity import infinity +from sage.matrix.constructor import matrix +from sage.matrix.matrix_generic_dense import Matrix_generic_dense + +class Matrix_tropical_dense(Matrix_generic_dense): + def extremum_mean_weight(self): + # Karp algorithm + T = self.base_ring() + n = self.ncols() + if self.nrows() != n: + raise TypeError("matrix must be square") + v = matrix(1, n, n*[T.one()]) # ??? + vs = [v] + for _ in range(n): + v = v * self + vs.append(v) + w = [vs[n][0,j].lift() for j in range(n)] + if T._use_min: + return min(max((w[j] - vs[k][0,j].lift()) / (n-k) for k in range(n)) + for j in range(n) if w[j] is not infinity) + else: + return max(min((w[j] - vs[k][0,j].lift()) / (n-k) for k in range(n)) + for j in range(n) if w[j] is not infinity) + + def weak_transitive_closure(self): + # Floyd-Warshall algorithm + T = self.base_ring() + n = self.ncols() + if self.nrows() != n: + raise TypeError("matrix must be square") + G = self.__copy__() + for p in range(n): + for i in range(n): + if i == p: + continue + for j in range(n): + if j == p: + continue + G[i,j] += G[i,p] * G[p,j] + if i == j: + if T._use_min and G[i,i].lift() < 0: + raise ValueError("negative cycle exists") + if not T._use_min and G[i,i].lift() > 0: + raise ValueError("positive cycle exists") + return G + + def strong_transitive_closure(self): + return self.parent().identity_matrix() + self.weak_transitive_closure() + + kleene_star = strong_transitive_closure From e4df0397357040697d5d28ddfc2a7e9edbd0205b Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Wed, 19 Nov 2025 17:16:15 +0100 Subject: [PATCH 62/93] extremum cycle mean, documentation --- src/doc/en/reference/references/index.rst | 3 + src/doc/en/reference/semirings/index.rst | 4 + src/sage/rings/semirings/tropical_matrix.py | 159 +++++++++++++++++++- 3 files changed, 161 insertions(+), 5 deletions(-) diff --git a/src/doc/en/reference/references/index.rst b/src/doc/en/reference/references/index.rst index 1f1cd007290..1e687b6b192 100644 --- a/src/doc/en/reference/references/index.rst +++ b/src/doc/en/reference/references/index.rst @@ -1432,6 +1432,9 @@ REFERENCES: An Algorithmic Approach, Algorithms and Computation in Mathematics, Volume 20, Springer (2007) +.. [But2010] Peter Butkovič, *Max-linear systems. Theory and algorithms.* + Springer Monographs in Mathematics. London: Springer. xvii, 272 p. (2010). + .. [Buell89] Duncan A. Buell. *Binary Quadratic Forms: Classical Theory and Modern Computations.* Springer, 1989. diff --git a/src/doc/en/reference/semirings/index.rst b/src/doc/en/reference/semirings/index.rst index b40e71c54e1..3bbb10edd0c 100644 --- a/src/doc/en/reference/semirings/index.rst +++ b/src/doc/en/reference/semirings/index.rst @@ -6,5 +6,9 @@ Standard Semirings sage/rings/semirings/non_negative_integer_semiring sage/rings/semirings/tropical_semiring + sage/rings/semirings/tropical_polynomial + sage/rings/semirings/tropical_mpolynomial + sage/rings/semirings/tropical_matrix + sage/rings/semirings/tropical_variety .. include:: ../footer.txt diff --git a/src/sage/rings/semirings/tropical_matrix.py b/src/sage/rings/semirings/tropical_matrix.py index 3a5061ccaec..838956589e5 100644 --- a/src/sage/rings/semirings/tropical_matrix.py +++ b/src/sage/rings/semirings/tropical_matrix.py @@ -1,15 +1,73 @@ -from sage.rings.infinity import infinity +r""" +Matrices over tropical semirings + +AUTHORS: + +- Xavier Caruso (2025-11): initial version +""" + +# **************************************************************************** +# Copyright (C) 2025 Xavier Caruso +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# https://www.gnu.org/licenses/ +# **************************************************************************** + from sage.matrix.constructor import matrix from sage.matrix.matrix_generic_dense import Matrix_generic_dense +from sage.rings.infinity import infinity + class Matrix_tropical_dense(Matrix_generic_dense): - def extremum_mean_weight(self): - # Karp algorithm + r""" + A class for dense matrices over a tropical semiring. + + EXAMPLES:: + + sage: from sage.rings.semirings.tropical_matrix import Matrix_tropical_dense + sage: T = TropicalSemiring(QQ) + sage: M = matrix(T, [[1, 2], [3, 4]]) + sage: isinstance(M, Matrix_tropical_dense) + True + """ + def extremum_cycle_mean(self): + r""" + Return the extremal (that is, minimal if the addition is max + and maximum is the addition is min) mean weight of this matrix + It is also the smallest/largest eigenvalue of this matrix. + + ALGORITHM: + + We implement Karp's algorithm described in []_, Section 1.6.1. + + EXAMPLES:: + + sage: T = TropicalSemiring(QQ, use_min=False) + sage: M = matrix(T, [[-2, 1, -3], + ....: [ 3, 0, 3], + ....: [ 5, 2, 1]]) + sage: M.extremum_cycle_mean() + 3 + + :: + + sage: T = TropicalSemiring(QQ) + sage: z = T.zero() + sage: M = matrix(T, [[z, 1, 10, z], + ....: [z, z, 3, z], + ....: [z, z, z, 2], + ....: [8, 0, z, z]]) + sage: M.extremum_cycle_mean() + 5/3 + """ T = self.base_ring() n = self.ncols() if self.nrows() != n: raise TypeError("matrix must be square") - v = matrix(1, n, n*[T.one()]) # ??? + v = matrix(1, n, n*[T.one()]) vs = [v] for _ in range(n): v = v * self @@ -23,7 +81,57 @@ def extremum_mean_weight(self): for j in range(n) if w[j] is not infinity) def weak_transitive_closure(self): - # Floyd-Warshall algorithm + r""" + Return the weak transitive closure of this matrix `M`, + that is, by definition + + .. MATH:: + + A \oplus A^2 \oplus A^3 \oplus A^4 \oplus \cdots + + or raise an error if this sum does not converge. + + ALGORITHM: + + We implement the Floyd-Warshall algorithm described in + [But2010]_, Algorithm 1.6.21. + + EXAMPLES:: + + sage: T = TropicalSemiring(QQ) + sage: z = T.zero() + sage: M = matrix(T, [[z, 1, 10, z], + ....: [z, z, 3, z], + ....: [z, z, z, 2], + ....: [8, 0, z, z]]) + sage: M.weak_transitive_closure() + [14 1 4 6] + [13 5 3 5] + [10 2 5 2] + [ 8 0 3 5] + + We check that the minimal cycle mean of `M` is the largest + value `a` such that `(-a) \otimes M` has a weak transitive + closure:: + + sage: M.extremum_cycle_mean() + 5/3 + sage: aM = T(-5/3) * M + sage: aM.weak_transitive_closure() + [22/3 -2/3 2/3 1] + [ 8 0 4/3 5/3] + [20/3 -4/3 0 1/3] + [19/3 -5/3 -1/3 0] + sage: bM = T(-2) * M + sage: bM.weak_transitive_closure() + Traceback (most recent call last): + ... + ValueError: negative cycle exists + + .. SEEALSO:: + + :meth:`strong_transitive_closure` + """ T = self.base_ring() n = self.ncols() if self.nrows() != n: @@ -45,6 +153,47 @@ def weak_transitive_closure(self): return G def strong_transitive_closure(self): + r""" + Return the string transitive closure of this matrix `M`, + that is, by definition + + .. MATH:: + + I \oplus A \oplus A^2 \oplus A^3 \oplus A^4 \oplus \cdots + + or raise an error if this sum does not converge. + + ALGORITHM: + + We implement the Floyd-Warshall algorithm described in + [But2010]_, Algorithm 1.6.21. + + EXAMPLES:: + + sage: T = TropicalSemiring(QQ, use_min=False) + sage: M = matrix(T, [[-5, -2, -6], + ....: [ 0, -3, 0], + ....: [ 2, -1, -2]]) + sage: M.strong_transitive_closure() + [ 1 -2 -2] + [ 2 1 0] + [ 2 0 1] + + :: + + sage: T = TropicalSemiring(QQ) + sage: M = matrix(T, [[-5, -2, -6], + ....: [ 0, -3, 0], + ....: [ 2, -1, -2]]) + sage: M.strong_transitive_closure() + Traceback (most recent call last): + ... + ValueError: negative cycle exists + + .. SEEALSO:: + + :meth:`weak_transitive_closure` + """ return self.parent().identity_matrix() + self.weak_transitive_closure() kleene_star = strong_transitive_closure From a98d0bb09661b1c84b08c978055a5d0c746acceb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20F=C3=BCrnsinn?= Date: Wed, 19 Nov 2025 18:06:02 +0100 Subject: [PATCH 63/93] doc --- .../functions/hypergeometric_algebraic.py | 73 ++++++++++++++++++- 1 file changed, 70 insertions(+), 3 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 2efe4c5be2a..8627ed07d45 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -814,7 +814,7 @@ def good_reduction_primes(self): def is_algebraic(self): r""" - Return ``True`` if this hypergeometric function is algebraic over + Return ``True`` if this hypergeometric function is algebraic over the rational functions, return ``False`` otherwise. EXAMPLES:: @@ -829,9 +829,9 @@ def is_algebraic(self): ALGORITHM: - We rely on the (Christol-)Beukers-Heckmann interlacing criterion + We rely on the (Christol-)Beukers-Heckmann interlacing criterion (see [Chr1986]_, p.15, Cor.; [BeukersHeckman]_, Thm. 4.5). For integer - differences between parameters we follow the flowchart in + differences between parameters we follow the flowchart in [FY2024]_, Fig. 1. """ @@ -847,6 +847,32 @@ def is_algebraic(self): for c in range(d) if d.gcd(c) == 1) def is_globally_bounded(self, include_infinity=True): + r""" + Return ``True`` when this hypergeometric function is globally bounded + (if ``include_infinity`` is ``False`` it is noct checked whether the + radius of convergence is finite) and ``False`` otherwise. + + INPUT: + + - ``include_infinity`` -- Boolean (default: ``True``) + + EXAMPLES: + + sage: S. = QQ[] + sage: f = hypergeometric([1/9, 4/9, 5/9], [1/3, 1], x) + sage: f.is_globally_bounded() + True + sage: g = hypergeometric([1/9, 4/9, 5/9], [1/3], x) + sage: g.is_globally_bounded() + False + sage: g.is_globally_bounded(include_infinity=False) + True + + ALGORITHM: + + We rely on Christol's classification of globally bounded hypergeometric + functions (see [Chr1986]_, Prop. 1). + """ if include_infinity and len(self.top()) > len(self.bottom()) + 1: return False d = self.denominator() @@ -857,9 +883,50 @@ def is_globally_bounded(self, include_infinity=True): return True def p_curvature_ranks(self): + # Should this return the coranks of the p-curvature depending on the + congruence class of a prime? raise NotImplementedError def monodromy(self, x=0, var='z'): + r''' + Return a local monodromy matrix of the hypergeometric differential + equation associated to this hypergeoemtric function at the popint + ``x``, where ``var`` represents a d-th root of unity for d being the + least common multiple of the parameters. + + INPUT: + + - ``x`` -- a complex number (default: ``0``) + - ``var`` -- a string (default: ``z``), the name of a d-trh root of unity + + EXAMPLES:: + + sage: S. = QQ[] + sage: f = hypergeometric([1/3, 2/3], [1/2], x) + sage: f.monodromy() + [0 1] + [1 0] + sage: f.monodromy() + + :: + + The bases of the solution space are chosen in a compatible way + across the three singularities of the differential equation: + + sage: g = hypergeometric([1/9, 4/9, 5/9], [1/3, 1], x) + sage: g.monodromy(var='a') + [ -a^3 + 1 1 0] + [2*a^3 + 1 0 1] + [ -a^3 - 1 0 0] + sage: g.monodromy(x=Infinity) * g.monodromy(x=1) * g.monodromy() + [1 0 0] + [0 1 0] + [0 0 1] + + ALGORITHM: + We use the explicit formulas for the monodromy matrices presented in + [BeukersHeckman]_, Thm. 3.5, attributed to Levelt. + ''' params = self._parameters if not params.is_balanced(): raise ValueError("hypergeometric equation is not Fuchsian") From c7bf5a66af6a22101354b36e2eab4f4e93aefb44 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Wed, 19 Nov 2025 22:18:48 +0100 Subject: [PATCH 64/93] use tropical matrices --- .../functions/hypergeometric_parameters.py | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/sage/functions/hypergeometric_parameters.py b/src/sage/functions/hypergeometric_parameters.py index 326660412e3..5d6507e535b 100644 --- a/src/sage/functions/hypergeometric_parameters.py +++ b/src/sage/functions/hypergeometric_parameters.py @@ -26,6 +26,10 @@ from sage.rings.integer_ring import ZZ from sage.rings.rational_field import QQ from sage.rings.finite_rings.integer_mod_ring import IntegerModRing +from sage.rings.semirings.tropical_semiring import TropicalSemiring + +from sage.matrix.constructor import matrix +from sage.matrix.special import identity_matrix # HypergeometricParameters of hypergeometric functions @@ -549,14 +553,16 @@ def valuation_position(self, p, drift=0): # Main part: computation of the valuation # We use Christol's formula (see Lemma 3.1.10 of [CFVM2025]) # with modification in order to take the drift into account + TSR = TropicalSemiring(QQ) n = len(top) + len(bottom) + 1 parameters = HypergeometricParameters(top, bottom) order = IntegerModRing(parameters.d)(p).multiplicative_order() valuation = position = ZZ(0) - breaks = None + valfinal = breaks = None indices = {} count = 0 q = 1 + TM = identity_matrix(TSR, n) while True: # We take into account the contribution of V({k/p^r}, p^r). # We represent the partial sum until r by the table new_breaks. @@ -590,7 +596,7 @@ def valuation_position(self, p, drift=0): new_breaks = [] new_indices = {} w = 0 - unbounded = False + TMstep = matrix(TSR, n) for i in range(n): x, dw, param = AB[i] # discontinuity point y, _, right = AB[i+1] # next discontinuity point @@ -618,12 +624,11 @@ def valuation_position(self, p, drift=0): else: interval = max(0, (x-1) // q) j = j0 = indices[param] - if growth == 0 and interval*drift + w < 0: - unbounded = True val = infinity while True: valj, posj, paramj = breaks[j] valj += drift * interval + TMstep[i,j] = TSR(drift*interval + w) if valj < val: val = valj pos = posj + q * interval @@ -640,14 +645,18 @@ def valuation_position(self, p, drift=0): valuation, position, _ = minimum if drift >= 0 and q > parameters.bound: if growth == 0: - if unbounded: - return -infinity, None - if all(new_breaks[j][0] >= breaks[j][0] for j in range(n)): - if count >= order: - return valuation, position - count += 1 - else: - count = 0 + if count < order: + TM = TMstep * TM + count += 1 + if count == order: + try: + TM = TM.weak_transitive_closure() + except ValueError: + return -infinity, None + valfinal = min(TM[i,j].lift() + new_breaks[j][0] + for i in range(n) for j in range(n)) + if valuation == valfinal: + return valuation, position elif drift >= len(top) and minimum == new_breaks[0]: return valuation, position From c28ee4ad8d1b87afd2c03264ae5dd5c8480c2661 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Wed, 19 Nov 2025 22:23:58 +0100 Subject: [PATCH 65/93] identity matrix --- src/sage/matrix/matrix_space.py | 3 ++- src/sage/matrix/special.py | 10 +++++++++- src/sage/rings/semirings/tropical_matrix.py | 6 +++--- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/sage/matrix/matrix_space.py b/src/sage/matrix/matrix_space.py index 32f4162c85c..ed27f7a41bb 100644 --- a/src/sage/matrix/matrix_space.py +++ b/src/sage/matrix/matrix_space.py @@ -2039,8 +2039,9 @@ def identity_matrix(self): if self.__nrows != self.__ncols: raise TypeError("identity matrix must be square") A = self.zero_matrix().__copy__() + one = self.base_ring().one() for i in range(self.__nrows): - A[i, i] = 1 + A[i, i] = one A.set_immutable() return A diff --git a/src/sage/matrix/special.py b/src/sage/matrix/special.py index 7182c27662f..b0b5484a655 100644 --- a/src/sage/matrix/special.py +++ b/src/sage/matrix/special.py @@ -936,11 +936,19 @@ def identity_matrix(ring, n=0, sparse=False): Full MatrixSpace of 3 by 3 sparse matrices over Integer Ring sage: M.is_mutable() True + + TESTS:: + + sage: T = TropicalSemiring(QQ) + sage: identity_matrix(T, 3) + [ 0 +infinity +infinity] + [+infinity 0 +infinity] + [+infinity +infinity 0] """ if isinstance(ring, (Integer, int)): n = ring ring = ZZ - return matrix_space.MatrixSpace(ring, n, n, sparse)(1) + return matrix_space.MatrixSpace(ring, n, n, sparse).identity_matrix() @matrix_method diff --git a/src/sage/rings/semirings/tropical_matrix.py b/src/sage/rings/semirings/tropical_matrix.py index 838956589e5..12761978673 100644 --- a/src/sage/rings/semirings/tropical_matrix.py +++ b/src/sage/rings/semirings/tropical_matrix.py @@ -175,9 +175,9 @@ def strong_transitive_closure(self): ....: [ 0, -3, 0], ....: [ 2, -1, -2]]) sage: M.strong_transitive_closure() - [ 1 -2 -2] - [ 2 1 0] - [ 2 0 1] + [ 0 -2 -2] + [ 2 0 0] + [ 2 0 0] :: From 3f17a1643f60c52d5ecbdd848c99cd648ae9c07d Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Wed, 19 Nov 2025 22:41:42 +0100 Subject: [PATCH 66/93] use the maximal log_radius for evaluation --- src/sage/functions/hypergeometric_algebraic.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 2efe4c5be2a..8a3fe0157ca 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -952,12 +952,16 @@ def _truncation_bound(self, log_radius, prec): margin = convergence - log_radius if margin <= 0: raise ValueError("outside the domain of convergence") - # We choose an intermediate log_radius - # It can be anything between convergence and log_radius - # but it seems that the following works well (in the sense - # that it gives good bounds at the end). - lr = convergence - margin / max(prec, 2) - val = self.valuation(lr) + val = self.valuation(convergence) + if val is not -infinity: + lr = convergence + else: + # We choose an intermediate log_radius + # It can be anything between convergence and log_radius + # but it seems that the following works well (in the sense + # that it gives good bounds at the end). + lr = convergence - margin / max(prec, 2) + val = self.valuation(lr) # Now, we know that # val(h_k) >= -lr*k + val # and we want to find k such that From 9b2f0fc1e0d36f74ab5fc75b08d0d56a1944837a Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Wed, 19 Nov 2025 22:54:11 +0100 Subject: [PATCH 67/93] answer to Florian's question --- src/sage/functions/hypergeometric_algebraic.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 71f0fed5501..633c1593039 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -884,7 +884,8 @@ def is_globally_bounded(self, include_infinity=True): def p_curvature_ranks(self): # Should this return the coranks of the p-curvature depending on the - congruence class of a prime? + # congruence class of a prime? + # YES! raise NotImplementedError def monodromy(self, x=0, var='z'): From a3648e130b570fb1e1db33f452b7c20b52a9fdd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20F=C3=BCrnsinn?= Date: Thu, 20 Nov 2025 17:02:10 +0100 Subject: [PATCH 68/93] doc --- .../functions/hypergeometric_algebraic.py | 141 +++++++++++++++++- 1 file changed, 136 insertions(+), 5 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 71f0fed5501..af36237e33d 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -471,6 +471,29 @@ def _compute_coeffs(self, prec): c /= b + i coeffs.append(c) + def nth_coefficient(self, n): + r""" + Return the ``n``-th coefficient of the series representation of this + hypergeoimetric function. + + INPUT: + + - ``n`` -- a non-negative integer + + EXAMPLES: + + sage: S. = QQ[] + sage: f = hypergeometric([1/3, 2/3], [1/2], x) + sage: f.nth_coefficient(9) + 409541017600/2541865828329 + sage: g = f % 5 + sage: g.nth_coefficient(9) + 0 + """ + self._compute_coeffs(n+1) + S = self.base_ring() + return S(self._coeffs[n]) + def power_series(self, prec=20): r""" Return the power series representation of this hypergeometric @@ -884,19 +907,20 @@ def is_globally_bounded(self, include_infinity=True): def p_curvature_ranks(self): # Should this return the coranks of the p-curvature depending on the - congruence class of a prime? + # congruence class of a prime? raise NotImplementedError def monodromy(self, x=0, var='z'): r''' Return a local monodromy matrix of the hypergeometric differential - equation associated to this hypergeoemtric function at the popint + equation associated to this hypergeoemtric function at the popint ``x``, where ``var`` represents a d-th root of unity for d being the least common multiple of the parameters. INPUT: - ``x`` -- a complex number (default: ``0``) + - ``var`` -- a string (default: ``z``), the name of a d-trh root of unity EXAMPLES:: @@ -906,7 +930,6 @@ def monodromy(self, x=0, var='z'): sage: f.monodromy() [0 1] [1 0] - sage: f.monodromy() :: @@ -924,7 +947,7 @@ def monodromy(self, x=0, var='z'): [0 0 1] ALGORITHM: - We use the explicit formulas for the monodromy matrices presented in + We use the explicit formulas for the monodromy matrices presented in [BeukersHeckman]_, Thm. 3.5, attributed to Levelt. ''' params = self._parameters @@ -950,6 +973,20 @@ def monodromy(self, x=0, var='z'): return identity_matrix(QQ, n) def is_maximum_unipotent_monodromy(self): + r""" + Return ``True`` if the hypergeometric differential operator associated + to this hypergeometric function has maximal unipotent monodromy (MUM). + + EXAMPLES:: + + sage: S. = QQ[] + sage: f = hypergeometric([1/3, 2/3], [1/2], x) + sage: f.is_maximum_unipotent_monodromy() + False + sage: g = hypergeometric([1/9, 4/9, 5/9], [1, 2], x) + sage: g.is_maximum_unipotent_monodromy() + True + """ return all(b in ZZ for b in self.bottom()) is_mum = is_maximum_unipotent_monodromy @@ -959,15 +996,56 @@ def is_maximum_unipotent_monodromy(self): class HypergeometricAlgebraic_padic(HypergeometricAlgebraic): def __init__(self, parent, arg1, arg2=None, scalar=None): + r""" + Initialize this hypergeometric function. + + INPUT: + + - ``parent`` -- the parent of this function, which has to be defined + over the p-adics + + - ``arg1``, ``arg2`` -- arguments defining this hypergeometric + function, they can be: + - the top and bottom paramters + - a hypergeometric function and ``None`` + - an instance of the class :class:`HypergeometricParameters` and ``None`` + + - ``scalar`` -- an element in the base ring, the scalar by + which the hypergeometric function is multiplied + + TESTS:: + + sage: S. = Qp(5, 3)[] + sage: h = hypergeometric((1/2, 1/3), (1,), x) + sage: type(h) + + sage: TestSuite(h).run() + """ HypergeometricAlgebraic.__init__(self, parent, arg1, arg2, scalar) K = self.base_ring() self._p = K.prime() self._e = K.e() def residue(self): + r''' + Return the reduction of this hypergeometric function in the residue + field of the p-adics over which this hypergeometric function is + defined. + + EXAMPLES:: + + sage: S. = Qp(5)[] + sage: f = hypergeometric([1/3, 2/3], [1/2], x) + sage: f.parent() + Hypergeometric functions in x over 5-adic Field with capped relative precision 20 + sage: g = f.residue() + sage: g.parent() + Hypergeometric functions in x over Finite Field of size 5 + + ''' k = self.base_ring().residue_field() if self._scalar.valuation() == 0: - return self.change_base(k) + return self.change_ring(k) val, pos = self._parameters.valuation_position(self._p) if val < 0: raise ValueError("bad reduction") @@ -980,10 +1058,36 @@ def residue(self): # . h = self.shift(s) def dwork_image(self): + r""" + Return the hypergeometric function obtained from this one by applying + the Dwork map to each of its parameters. + + EXAMPLES:: + + sage: S. = QQ[] + sage: f = hypergeometric([1/4, 1/3, 1/2], [2/5, 3/5, 1], x) + sage: f.dwork_image() + hypergeometric((1/3, 1/2, 3/4), (1/5, 4/5, 1), x) + + """ parameters = self._parameters.dwork_image(self._p) return self.parent()(parameters, scalar=self._scalar) def log_radius_of_convergence(self): + r''' + Return the logarithmic p-adic radius of convergence of this + hypergeometric function. + + EXAMPLES:: + + sage: S. = Qp(5)[] + sage: f = hypergeometric([1/3, 2/3], [1/2], x) + sage: f.log_radius_of_convergence() + 0 + sage: g = hypergeometric([1/3, 2/3], [1/5], x) + sage: g.log_radius_of_convergence() + 5/4 + ''' p = self._p step = self._e / (p - 1) log_radius = 0 @@ -1004,6 +1108,33 @@ def log_radius_of_convergence(self): return log_radius def valuation(self, log_radius=0, position=False): + r''' + Return the p-adic valuation of this hypergeometric function on the + disk of logarithmic radius ``log_radius``, and, if ``position`` is + ``True`` the index of the first coefficient of the series that + attains this valuation. + + INPUT: + + - ``log_radius`` -- a rational number + + - ``position`` -- a boolean (default: ``False``), if ``True`` the index + of the first coefficient attaining the valuation is also returned + + EXAMPLES:: + + sage: S. = Qp(5)[] + sage: f = hypergeometric([1/3, 2/3], [1/2], x) + sage: f.valuation() + 0 + + :: + + sage: S. = Qp(5)[] + sage: g = 1/5 * hypergeometric([1/3, 2/3], [5^3/3], x) + sage: g.valuation(-1, position = True) + (-2, 1) + ''' drift = -log_radius / self._e val, pos = self._parameters.valuation_position(self._p, drift) if position: From 264796ec859b7a45cdf1eb10c8da158921ba55d1 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Thu, 20 Nov 2025 18:11:42 +0100 Subject: [PATCH 69/93] identity_matrix should return a mutable matrix --- src/sage/matrix/special.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sage/matrix/special.py b/src/sage/matrix/special.py index b0b5484a655..52b3e865ed0 100644 --- a/src/sage/matrix/special.py +++ b/src/sage/matrix/special.py @@ -948,7 +948,7 @@ def identity_matrix(ring, n=0, sparse=False): if isinstance(ring, (Integer, int)): n = ring ring = ZZ - return matrix_space.MatrixSpace(ring, n, n, sparse).identity_matrix() + return matrix_space.MatrixSpace(ring, n, n, sparse)(ring.one()) @matrix_method From f1bc7ecb89e5fec369c9dbc462520da7e660834b Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Thu, 20 Nov 2025 21:09:45 +0100 Subject: [PATCH 70/93] doctest is sagedoc.py --- src/sage/matrix/special.py | 2 +- src/sage/misc/sagedoc.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sage/matrix/special.py b/src/sage/matrix/special.py index 52b3e865ed0..f5eb173479c 100644 --- a/src/sage/matrix/special.py +++ b/src/sage/matrix/special.py @@ -937,7 +937,7 @@ def identity_matrix(ring, n=0, sparse=False): sage: M.is_mutable() True - TESTS:: + :: sage: T = TropicalSemiring(QQ) sage: identity_matrix(T, 3) diff --git a/src/sage/misc/sagedoc.py b/src/sage/misc/sagedoc.py index ab5ff639dd6..7ff39a8a7ee 100644 --- a/src/sage/misc/sagedoc.py +++ b/src/sage/misc/sagedoc.py @@ -1454,7 +1454,7 @@ class _sage_doc: sage: browse_sage_doc._open("reference", testing=True)[0] # needs sagemath_doc_html 'http://localhost:8000/doc/live/reference/index.html' - sage: browse_sage_doc(identity_matrix, 'rst')[-107:-47] # needs sage.modules + sage: browse_sage_doc(identity_matrix, 'rst')[-311:-251] # needs sage.modules '...Full MatrixSpace of 3 by 3 sparse matrices...' """ def __init__(self): From a9f88ff8ce95619a21936dc25afd9ea7959cc099 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Fri, 21 Nov 2025 02:58:06 +0100 Subject: [PATCH 71/93] minor edits --- .../functions/hypergeometric_algebraic.py | 65 ++++++++++--------- 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index d1c46533f47..ecbd015eb4c 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -537,7 +537,7 @@ def shift(self, s): @coerce_binop def hadamard_product(self, other): r""" - Return the hadamard product of the hypergeometric function + Return the hadamard product of this hypergeometric function and ``other``. INPUT: @@ -564,7 +564,7 @@ def hadamard_product(self, other): def _div_(self, other): r""" Return the (formal) quotient of the hypergeometric function - and ``other`` + and ``other``. INPUT: @@ -695,8 +695,8 @@ def __mod__(self, p): def valuation(self, p, position=False): r""" - Return the p-adic valuation of this hypergeometric function, i.e., the - maximal s, such that p^(-s) times this hypergeometric function has + Return the `p`-adic valuation of this hypergeometric function, i.e., the + maximal `s`, such that `p^{-s}` times this hypergeometric function has p-integral coefficients. INPUT: @@ -750,8 +750,9 @@ def valuation(self, p, position=False): def has_good_reduction(self, p): r""" - Return ``True`` if the p-adic valuation of this hypergeometric function - is non-negative, i.e., if its reduction modulo ``p`` is well-defined.pAdicGeneric + Return whether the `p`-adic valuation of this hypergeometric + function is nonnegative, i.e., if its reduction modulo ``p`` + is well-defined. INPUT: @@ -774,7 +775,7 @@ def has_good_reduction(self, p): def good_reduction_primes(self): r""" Return the set of prime numbers modulo which this hypergeometric - function can be reduced, i.e., the p-adic valuation is positive. + function can be reduced, i.e., the p-adic valuation is nonnegative. EXAMPLES:: @@ -856,7 +857,6 @@ def is_algebraic(self): (see [Chr1986]_, p.15, Cor.; [BeukersHeckman]_, Thm. 4.5). For integer differences between parameters we follow the flowchart in [FY2024]_, Fig. 1. - """ if any(a in ZZ and a <= 0 for a in self.top()): return True @@ -871,9 +871,9 @@ def is_algebraic(self): def is_globally_bounded(self, include_infinity=True): r""" - Return ``True`` when this hypergeometric function is globally bounded - (if ``include_infinity`` is ``False`` it is noct checked whether the - radius of convergence is finite) and ``False`` otherwise. + Return whether this hypergeometric function is globally bounded + (if ``include_infinity`` is ``False`` it is not checked whether + the radius of convergence is finite). INPUT: @@ -912,17 +912,18 @@ def p_curvature_ranks(self): raise NotImplementedError def monodromy(self, x=0, var='z'): - r''' + r""" Return a local monodromy matrix of the hypergeometric differential - equation associated to this hypergeoemtric function at the popint - ``x``, where ``var`` represents a d-th root of unity for d being the - least common multiple of the parameters. + equation associated to this hypergeoemtric function at the point + ``x``. INPUT: - ``x`` -- a complex number (default: ``0``) - - ``var`` -- a string (default: ``z``), the name of a d-trh root of unity + - ``var`` -- a string (default: ``z``), the name of the variable + representing a `d`-th root of unity for `d` being least common + multiple of the parameters. EXAMPLES:: @@ -948,9 +949,10 @@ def monodromy(self, x=0, var='z'): [0 0 1] ALGORITHM: + We use the explicit formulas for the monodromy matrices presented in [BeukersHeckman]_, Thm. 3.5, attributed to Levelt. - ''' + """ params = self._parameters if not params.is_balanced(): raise ValueError("hypergeometric equation is not Fuchsian") @@ -975,7 +977,7 @@ def monodromy(self, x=0, var='z'): def is_maximum_unipotent_monodromy(self): r""" - Return ``True`` if the hypergeometric differential operator associated + Return whether the hypergeometric differential operator associated to this hypergeometric function has maximal unipotent monodromy (MUM). EXAMPLES:: @@ -1028,7 +1030,7 @@ def __init__(self, parent, arg1, arg2=None, scalar=None): self._e = K.e() def residue(self): - r''' + r""" Return the reduction of this hypergeometric function in the residue field of the p-adics over which this hypergeometric function is defined. @@ -1042,8 +1044,7 @@ def residue(self): sage: g = f.residue() sage: g.parent() Hypergeometric functions in x over Finite Field of size 5 - - ''' + """ k = self.base_ring().residue_field() if self._scalar.valuation() == 0: return self.change_ring(k) @@ -1060,8 +1061,8 @@ def residue(self): def dwork_image(self): r""" - Return the hypergeometric function obtained from this one by applying - the Dwork map to each of its parameters. + Return the hypergeometric function obtained from this one + by applying the Dwork map to each of its parameters. EXAMPLES:: @@ -1069,13 +1070,12 @@ def dwork_image(self): sage: f = hypergeometric([1/4, 1/3, 1/2], [2/5, 3/5, 1], x) sage: f.dwork_image() hypergeometric((1/3, 1/2, 3/4), (1/5, 4/5, 1), x) - """ parameters = self._parameters.dwork_image(self._p) return self.parent()(parameters, scalar=self._scalar) def log_radius_of_convergence(self): - r''' + r""" Return the logarithmic p-adic radius of convergence of this hypergeometric function. @@ -1088,7 +1088,7 @@ def log_radius_of_convergence(self): sage: g = hypergeometric([1/3, 2/3], [1/5], x) sage: g.log_radius_of_convergence() 5/4 - ''' + """ p = self._p step = self._e / (p - 1) log_radius = 0 @@ -1109,7 +1109,7 @@ def log_radius_of_convergence(self): return log_radius def valuation(self, log_radius=0, position=False): - r''' + r""" Return the p-adic valuation of this hypergeometric function on the disk of logarithmic radius ``log_radius``, and, if ``position`` is ``True`` the index of the first coefficient of the series that @@ -1119,8 +1119,9 @@ def valuation(self, log_radius=0, position=False): - ``log_radius`` -- a rational number - - ``position`` -- a boolean (default: ``False``), if ``True`` the index - of the first coefficient attaining the valuation is also returned + - ``position`` -- a boolean (default: ``False``), if ``True`` the + index of the first coefficient attaining the valuation is also + returned EXAMPLES:: @@ -1128,14 +1129,14 @@ def valuation(self, log_radius=0, position=False): sage: f = hypergeometric([1/3, 2/3], [1/2], x) sage: f.valuation() 0 - + :: sage: S. = Qp(5)[] sage: g = 1/5 * hypergeometric([1/3, 2/3], [5^3/3], x) - sage: g.valuation(-1, position = True) + sage: g.valuation(-1, position=True) (-2, 1) - ''' + """ drift = -log_radius / self._e val, pos = self._parameters.valuation_position(self._p, drift) if position: From 396169af6189602f80e9fa2f2d49cf3cd4a9d23f Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Fri, 21 Nov 2025 08:15:13 +0100 Subject: [PATCH 72/93] some improvements in valuation_position --- .../functions/hypergeometric_parameters.py | 95 ++++++++++--------- 1 file changed, 48 insertions(+), 47 deletions(-) diff --git a/src/sage/functions/hypergeometric_parameters.py b/src/sage/functions/hypergeometric_parameters.py index 5d6507e535b..310fc59eb7c 100644 --- a/src/sage/functions/hypergeometric_parameters.py +++ b/src/sage/functions/hypergeometric_parameters.py @@ -527,7 +527,7 @@ def valuation_position(self, p, drift=0): """ # We treat the case of parameters having p in the denominator # When it happens, we remove the parameter and update the drift accordingly - top = [] + params = {} for a in self.top: if a in ZZ and a <= 0: raise NotImplementedError # TODO @@ -535,78 +535,79 @@ def valuation_position(self, p, drift=0): if v < 0: drift += v else: - top.append(a) - bottom = [] + params[a] = params.get(a, 0) + 1 for b in self.bottom: v = b.valuation(p) if v < 0: drift -= v else: - bottom.append(b) + params[b] = params.get(b, 0) - 1 + params = [(pa, dw) for pa, dw in params.items() if dw != 0] # We check that we are inside the disk of convergence - diff = len(top) - len(bottom) + diff = sum(dw for _, dw in params) growth = (p-1)*drift + diff - if (growth, drift) < (0, 0): + if growth < 0: return -infinity, None # Main part: computation of the valuation # We use Christol's formula (see Lemma 3.1.10 of [CFVM2025]) # with modification in order to take the drift into account + n = len(params) + 1 TSR = TropicalSemiring(QQ) - n = len(top) + len(bottom) + 1 - parameters = HypergeometricParameters(top, bottom) - order = IntegerModRing(parameters.d)(p).multiplicative_order() + TM = identity_matrix(TSR, n) + + d = lcm(pa.denominator() for pa, _ in params) + order = IntegerModRing(d)(p).multiplicative_order() + bound = 1 + max(pa.abs() for pa, _ in params) + thresold = d * sum(dw for _, dw in params if dw > 0) + valuation = position = ZZ(0) - valfinal = breaks = None + valfinal = signature_prev = None indices = {} count = 0 q = 1 - TM = identity_matrix(TSR, n) while True: # We take into account the contribution of V({k/p^r}, p^r). - # We represent the partial sum until r by the table new_breaks. + # We represent the partial sum until r by the list signature. # Its entries are triples (valuation, position, parameter): # - parameter is the parameter corresponding to a point of # discontinuity of the last summand V({k/p^r}, p^r) # - valuation is the minimum of the partial sum on the # range starting at this discontinuity point (included) # and ending at the next one (excluded) - # - position is the first position the minimum is reached - # (The dictionary new_indices allows for finding rapidly - # an entry in new_breaks with a given parameter.) - # The table breaks and the dictionary indices correspond - # to the same data for r-1. + # - position is the first position where the minimum is reached + # (The dictionary indices allows for finding rapidly + # an entry in signature with a given parameter.) + # The list signature_prev and the dictionary indices_prev + # correspond to the same data for r-1. pq = p * q - # We compute the point of discontinuity of V({k/p^r}, p^r) - # and store them in AB - # Each entry of AB has the form (x, dw, parameter) where: + # We compute the points of discontinuity of V({k/p^r}, p^r) + # and store them in the list jumps + # Each entry of jumps has the form (x, dw, parameter) where: # - x is the position of the discontinuity point - # - dw is the *opposite* of the jump of V({k/p^r}, p^r) - # at this point - # (taking the opposite is useful for sorting reasons) + # - dw is the jump of V({k/p^r}, p^r) at this point # - parameter is the corresponding parameter - A = [(1 + (-a) % pq, -1, a) for a in top] - B = [(1 + (-b) % pq, 1, b) for b in bottom] - AB = [(0, 0, 0)] + sorted(A + B) + [(pq, None, 0)] + jumps = [(1 + (-pa) % pq, dw, pa) for pa, dw in params] + jumps = [(0, 0, 0)] + sorted(jumps) + [(pq, None, 0)] - # We compute new_breaks - new_breaks = [] - new_indices = {} + # We compute the signature + signature = [] + indices = {} w = 0 TMstep = matrix(TSR, n) for i in range(n): - x, dw, param = AB[i] # discontinuity point - y, _, right = AB[i+1] # next discontinuity point - w -= dw # the value of V({k/p^r}, p^r) on this interval - new_indices[param] = len(new_breaks) + x, dw, param = jumps[i] # discontinuity point + y, _, right = jumps[i+1] # next discontinuity point + w += dw # the value of V({k/p^r}, p^r) on this interval + indices[param] = len(signature) if x == y: # Case of empty interval val = infinity pos = None - elif breaks is None: + elif signature_prev is None: # Case r = 1 if drift < 0: pos = y - 1 @@ -620,30 +621,32 @@ def valuation_position(self, p, drift=0): complete = (y - x >= q) if complete and drift < 0: interval = ((y-1) // q) - 1 - j = j0 = indices[right] + j = j0 = indices_prev[right] else: interval = max(0, (x-1) // q) - j = j0 = indices[param] + j = j0 = indices_prev[param] val = infinity while True: - valj, posj, paramj = breaks[j] + valj, posj, paramj = signature_prev[j] valj += drift * interval TMstep[i,j] = TSR(drift*interval + w) if valj < val: val = valj pos = posj + q * interval j += 1 - if j >= len(breaks): + if j >= n: j = 0 interval += 1 - if j == j0 or (not complete and breaks[j][2] == right): + if j == j0 or (not complete and signature_prev[j][2] == right): break - new_breaks.append((val + w, pos, param)) + signature.append((val + w, pos, param)) # The halting criterion - minimum = min(new_breaks) + minimum = min(signature) valuation, position, _ = minimum - if drift >= 0 and q > parameters.bound: + if q > bound: + if drift > 2*thresold and all(signature[i][0] > valuation + thresold for i in range(1, n)): + return valuation, position if growth == 0: if count < order: TM = TMstep * TM @@ -653,18 +656,16 @@ def valuation_position(self, p, drift=0): TM = TM.weak_transitive_closure() except ValueError: return -infinity, None - valfinal = min(TM[i,j].lift() + new_breaks[j][0] + valfinal = min(TM[i,j].lift() + signature[j][0] for i in range(n) for j in range(n)) if valuation == valfinal: return valuation, position - elif drift >= len(top) and minimum == new_breaks[0]: - return valuation, position # We update the values for the next r q = pq drift = p*drift + diff - breaks = new_breaks - indices = new_indices + signature_prev = signature + indices_prev = indices def dwork_image(self, p): r""" From 64ddd8972381a02def053cc003bfb296816e6db4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20F=C3=BCrnsinn?= Date: Fri, 21 Nov 2025 14:44:16 +0100 Subject: [PATCH 73/93] Implemented p_curvature_coranks --- .../functions/hypergeometric_algebraic.py | 63 +++++++++++++++---- .../functions/hypergeometric_parameters.py | 37 ++++++++++- 2 files changed, 88 insertions(+), 12 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index ecbd015eb4c..e141afba0cf 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -471,7 +471,7 @@ def _compute_coeffs(self, prec): c /= b + i coeffs.append(c) - def nth_coefficient(self, n): + def coefficient(self, n): r""" Return the ``n``-th coefficient of the series representation of this hypergeoimetric function. @@ -484,10 +484,10 @@ def nth_coefficient(self, n): sage: S. = QQ[] sage: f = hypergeometric([1/3, 2/3], [1/2], x) - sage: f.nth_coefficient(9) + sage: f.coefficient(9) 409541017600/2541865828329 sage: g = f % 5 - sage: g.nth_coefficient(9) + sage: g.coefficient(9) 0 """ self._compute_coeffs(n+1) @@ -602,6 +602,11 @@ def denominator(self): return self._parameters.d def differential_operator(self, var='d'): + # Differential equation might not be defined in positive characteristic + # sage: f = hypergeometric([1/5, 1/5, 1/5], [1/3, 3/5], x) + # sage: g = f % 3 + # sage: g.differential_operator() + # Gives error message r""" Return the hypergeometric differential operator that annihilates this hypergeometric function as an Ore polynomial in the variable @@ -905,16 +910,52 @@ def is_globally_bounded(self, include_infinity=True): return False return True - def p_curvature_ranks(self): - # Should this return the coranks of the p-curvature depending on the - # congruence class of a prime? - # YES! - raise NotImplementedError + def p_curvature_coranks(self): + r""" + Return a dictonary, where the integers from ``1`` to the number of + parameters of this hypergeometric function, are assigned the set of + prime numbers for which the ``p``-curvature has this given corank. + + EXAMPLES:: + + sage: S. = QQ[] + sage: g = hypergeometric([1/8, 3/8, 1/2], [1/4, 5/8], x) + sage: g.p_curvature_coranks() + {1: Set of prime numbers congruent to 3, 5 modulo 8: 3, 5, 11, 13, ..., + 2: Set of prime numbers congruent to 1, 7 modulo 8: 7, 17, 23, 31, ..., + 3: Empty set of prime numbers} + + """ + # Do we have an example with exceptional primes? + if not self._parameters.is_balanced(): + raise NotImplementedError("Only implemented for nFn-1") + d = ZZ(self.denominator()) + classes = dict.fromkeys(range(1, len(self.top())+1), Primes(modulus = 0)) + for c in range(d): + if gcd(c, d) == 1: + Delta = QQ(1/c) % d + j = self._parameters.interlacing_number(Delta) + classes[j] = classes[j].union(Primes(modulus = d, classes = [c])) + for p in Primes(): + # I am sure one can avoid computing the interlacing number again for + # all primes here. + if p > self._parameters.bound: + break + if gcd(p, d) > 1: + # Do we exclude too many primes here? For which p is the + # hypergeometric differential equation defined? + continue + qinterlacing = self._parameters.q_interlacing_number(p) + cinterlacing = self._parameters.interlacing_number(QQ(1/p) % d) + if qinterlacing != cinterlacing: + classes[qinterlacing].include(p) + classes[cinterlacing].exclude(p) + return classes def monodromy(self, x=0, var='z'): r""" Return a local monodromy matrix of the hypergeometric differential - equation associated to this hypergeoemtric function at the point + equation associated to this hypergeometric function at the point ``x``. INPUT: @@ -922,8 +963,8 @@ def monodromy(self, x=0, var='z'): - ``x`` -- a complex number (default: ``0``) - ``var`` -- a string (default: ``z``), the name of the variable - representing a `d`-th root of unity for `d` being least common - multiple of the parameters. + representing a `d`-th root of unity for `d` being the least + common multiple of the parameters. EXAMPLES:: diff --git a/src/sage/functions/hypergeometric_parameters.py b/src/sage/functions/hypergeometric_parameters.py index 310fc59eb7c..09e954fad33 100644 --- a/src/sage/functions/hypergeometric_parameters.py +++ b/src/sage/functions/hypergeometric_parameters.py @@ -106,7 +106,7 @@ def __init__(self, top, bottom, add_one=True): else: self.d = lcm([ a.denominator() for a in top ] + [ b.denominator() for b in bottom ]) - self.bound = 2 * self.d * max(top + bottom) + 1 + self.bound = 2 * self.d * max(abs(a) for a in top + bottom) + 1 def __repr__(self): r""" @@ -271,6 +271,41 @@ def interlacing_criterion(self, c): previous_paren = paren return True + def interlacing_number(self, c): + r""" + Return the number of triples in the list ``self.christol_sorting(c)`` + with third entry 1, that were preceded by a triple with third entry + -1. + + INPUT: + + - ``c`` -- an integer + + EXAMPLES:: + + sage: from sage.functions.hypergeometric_algebraic import HypergeometricParameters + sage: pa = HypergeometricParameters([1/4, 1/3, 1/2], [2/5, 3/5]) + sage: pa + ((1/4, 1/3, 1/2), (2/5, 3/5, 1)) + sage: pa.christol_sorting(7) + [(12, -3/5, 1), + (20, -1/3, -1), + (30, -1/2, -1), + (45, -1/4, -1), + (48, -2/5, 1), + (60, -1, 1)] + sage: pa.interlacing_number(7) + 1 + """ + interlacing = 0 + previous_paren = 1 + for _, _, paren in self.christol_sorting(c): + if paren == 1 and previous_paren == -1: + interlacing += 1 + previous_paren = paren + return interlacing + + def q_christol_sorting(self, q): r""" Return a sorted list of pairs, one associated to each top parameter a, From 5416efdcaeaf926771fa9b3d1e3293abb97158a8 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Fri, 21 Nov 2025 20:24:29 +0100 Subject: [PATCH 74/93] Newton polygon --- .../functions/hypergeometric_algebraic.py | 24 ++- .../functions/hypergeometric_parameters.py | 199 +++++++++++++++--- 2 files changed, 190 insertions(+), 33 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index ecbd015eb4c..77955ddada5 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -45,6 +45,7 @@ from sage.matrix.special import companion_matrix from sage.matrix.special import identity_matrix from sage.combinat.subset import Subsets +from sage.geometry.newton_polygon import NewtonPolygon from sage.rings.infinity import infinity from sage.symbolic.ring import SR @@ -121,7 +122,7 @@ def __init__(self, parent, arg1, arg2=None, scalar=None): if any(b in ZZ and b < 0 for b in parameters.bottom): raise ValueError("the parameters %s do not define a hypergeometric function" % parameters) if char > 0: - val, _ = parameters.valuation_position(char) + val, _, _ = parameters.valuation_position(char) if val < 0: raise ValueError("the parameters %s do not define a hypergeometric function in characteristic %s" % (parameters, char)) self._scalar = scalar @@ -741,7 +742,7 @@ def valuation(self, p, position=False): """ if not p.is_prime(): raise ValueError("p must be a prime number") - val, pos = self._parameters.valuation_position(p) + val, pos, _ = self._parameters.valuation_position(p) val += self._scalar.valuation(p) if position: return val, pos @@ -1048,7 +1049,7 @@ def residue(self): k = self.base_ring().residue_field() if self._scalar.valuation() == 0: return self.change_ring(k) - val, pos = self._parameters.valuation_position(self._p) + val, pos, _ = self._parameters.valuation_position(self._p) if val < 0: raise ValueError("bad reduction") if val > 0: @@ -1138,14 +1139,23 @@ def valuation(self, log_radius=0, position=False): (-2, 1) """ drift = -log_radius / self._e - val, pos = self._parameters.valuation_position(self._p, drift) + val, pos, _ = self._parameters.valuation_position(self._p, drift) if position: return val, pos else: return val - def newton_polygon(self, log_radius): - raise NotImplementedError + def newton_polygon(self, log_radius=None): + convergence = self.log_radius_of_convergence() + if log_radius is None: + log_radius = convergence + start = -log_radius / self._e + try: + val = self._parameters.valuation_function(self._p, start) + except ValueError: + raise ValueError("infinite Newton polygon; try to truncate it by giving a log radius less than %s" % convergence) + vertices, _ = val.defn() + return NewtonPolygon(vertices, last_slope=log_radius) def _truncation_bound(self, log_radius, prec): convergence = self.log_radius_of_convergence() @@ -1307,7 +1317,7 @@ def dwork_relation(self): Ps = {} for r in range(p): params = parameters.shift(r).dwork_image(p) - _, s = params.valuation_position(p) + _, s, _ = params.valuation_position(p) h = H(params.shift(s)) e = s*p + r if e >= len(coeffs): diff --git a/src/sage/functions/hypergeometric_parameters.py b/src/sage/functions/hypergeometric_parameters.py index 310fc59eb7c..5cc405fc223 100644 --- a/src/sage/functions/hypergeometric_parameters.py +++ b/src/sage/functions/hypergeometric_parameters.py @@ -19,20 +19,97 @@ from sage.misc.cachefunc import cached_method -from sage.functions.other import ceil +from sage.functions.other import floor, ceil from sage.arith.functions import lcm from sage.rings.infinity import infinity from sage.rings.integer_ring import ZZ from sage.rings.rational_field import QQ from sage.rings.finite_rings.integer_mod_ring import IntegerModRing +from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing from sage.rings.semirings.tropical_semiring import TropicalSemiring +from sage.geometry.polyhedron.constructor import Polyhedron +from sage.geometry.polyhedron.base0 import Polyhedron_base0 + from sage.matrix.constructor import matrix from sage.matrix.special import identity_matrix -# HypergeometricParameters of hypergeometric functions +# Functions defined as min of affine functions +############################################## + +class MinFunction(): + def __init__(self, subgraph=None, start=None): + if subgraph is None: + subgraph = Polyhedron(ieqs=[[0, 0, 0]]) + if start is not None: + P = Polyhedron(ieqs=[[-start, 1, 0]]) + subgraph = subgraph.intersection(P) + self._subgraph = subgraph + + def defn(self): + affine = [] + start = None + for u, v, w in self._subgraph.inequalities_list(): + if w == 0: + start = -u/v + else: + affine.append((-v/w, -u/w)) + return affine, start + + def __repr__(self): + affine, start = self.defn() + S = PolynomialRing(QQ, 'x') + fs = [str(a*S.gen() + b) for a, b, in affine] + if len(fs) == 0: + s = "+infinity" + elif len(fs) == 1: + s = fs[0] + else: + s = "min(" + ", ".join(fs) + ")" + if start is not None: + s += " on [%s, +infty)" % start + return s + + def inf(self, gs): + subgraph = self._subgraph.intersection(gs._subgraph) + return MinFunction(subgraph) + + def __mul__(self, c): + if c in QQ and c >= 0: + ieqs = [(c*u, c*v, w) for u, v, w in self._subgraph.inequalities_list()] + return MinFunction(Polyhedron(ieqs=ieqs)) + + def __add__(self, other): + if other in QQ: + ieqs = [(u - other*w, v, w) for u, v, w in self._subgraph.inequalities_list()] + if isinstance(other, MinFunction): + ieqs = [(-u*wp - up*w, -v*wp - vp*w, -w*wp) + for u, v, w in self._subgraph.inequalities_list() + for up, vp, wp in other._subgraph.inequalities_list()] + return MinFunction(Polyhedron(ieqs=ieqs)) + + def __call__(self, x): + L = Polyhedron(eqns=[[-x, 1, 0]]).intersection(self._subgraph) + if L.is_empty(): + raise ValueError("not defined") + v = L.inequalities_list() + if not v: + return infinity + return -v[0][0] / v[0][2] + + +def affine_function(a=None, b=None, start=None): + ieqs = [] + if a is not None: + ieqs.append([b, a, -1]) + if start is not None: + ieqs.append([-start, 1, 0]) + return MinFunction(Polyhedron(ieqs=ieqs)) + + +# Parameters of hypergeometric functions ######################################## class HypergeometricParameters(): @@ -489,6 +566,26 @@ def decimal_part(self): bottom = [1 + b - ceil(b) for b in self.bottom] return HypergeometricParameters(top, bottom, add_one=False) + def prepare_parameters(self, p): + params = {} + shift = 0 + for a in self.top: + if a in ZZ and a <= 0: + raise NotImplementedError + v = a.valuation(p) + if v < 0: + shift += v + else: + params[a] = params.get(a, 0) + 1 + for b in self.bottom: + v = b.valuation(p) + if v < 0: + shift -= v + else: + params[b] = params.get(b, 0) - 1 + params = [(pa, dw) for pa, dw in params.items() if dw != 0] + return params, shift + def valuation_position(self, p, drift=0): r""" If the `h_k`s are the coefficients of the hypergeometric @@ -525,30 +622,13 @@ def valuation_position(self, p, drift=0): sage: pa.valuation_position(3, drift=-7/5) (-54/5, 7) """ - # We treat the case of parameters having p in the denominator - # When it happens, we remove the parameter and update the drift accordingly - params = {} - for a in self.top: - if a in ZZ and a <= 0: - raise NotImplementedError # TODO - v = a.valuation(p) - if v < 0: - drift += v - else: - params[a] = params.get(a, 0) + 1 - for b in self.bottom: - v = b.valuation(p) - if v < 0: - drift -= v - else: - params[b] = params.get(b, 0) - 1 - params = [(pa, dw) for pa, dw in params.items() if dw != 0] - # We check that we are inside the disk of convergence + params, shift = self.prepare_parameters(p) + drift += shift diff = sum(dw for _, dw in params) growth = (p-1)*drift + diff if growth < 0: - return -infinity, None + return -infinity, None, 0 # Main part: computation of the valuation # We use Christol's formula (see Lemma 3.1.10 of [CFVM2025]) @@ -565,7 +645,7 @@ def valuation_position(self, p, drift=0): valuation = position = ZZ(0) valfinal = signature_prev = None indices = {} - count = 0 + count = step = 0 q = 1 while True: # We take into account the contribution of V({k/p^r}, p^r). @@ -582,6 +662,7 @@ def valuation_position(self, p, drift=0): # The list signature_prev and the dictionary indices_prev # correspond to the same data for r-1. + step += 1 pq = p * q # We compute the points of discontinuity of V({k/p^r}, p^r) @@ -646,7 +727,7 @@ def valuation_position(self, p, drift=0): valuation, position, _ = minimum if q > bound: if drift > 2*thresold and all(signature[i][0] > valuation + thresold for i in range(1, n)): - return valuation, position + return valuation, position, step if growth == 0: if count < order: TM = TMstep * TM @@ -655,11 +736,11 @@ def valuation_position(self, p, drift=0): try: TM = TM.weak_transitive_closure() except ValueError: - return -infinity, None + return -infinity, None, step valfinal = min(TM[i,j].lift() + signature[j][0] for i in range(n) for j in range(n)) if valuation == valfinal: - return valuation, position + return valuation, position, step # We update the values for the next r q = pq @@ -667,6 +748,72 @@ def valuation_position(self, p, drift=0): signature_prev = signature indices_prev = indices + def valuation_function(self, p, start=0): + valstart, _, step = self.valuation_position(p, start) + if valstart is -infinity: + raise ValueError + + params, shift = self.prepare_parameters(p) + diff = sum(dw for _, dw in params) + n = len(params) + 1 + infty = affine_function(start=start) + drift = affine_function(1, shift, start) + signature_prev = None + indices = {} + count = 0 + q = 1 + for _ in range(step): + pq = p * q + + jumps = [(1 + (-pa) % pq, dw, pa) for pa, dw in params] + jumps = [(0, 0, 0)] + sorted(jumps) + [(pq, None, 0)] + + signature = [] + indices = {} + w = 0 + for i in range(n): + x, dw, param = jumps[i] # discontinuity point + y, _, right = jumps[i+1] # next discontinuity point + w += dw # the value of V({k/p^r}, p^r) on this interval + indices[param] = len(signature) + if x == y: + # Case of empty interval + val = infty + elif signature_prev is None: + # Case r = 1 + val = (drift * x).inf(drift * (y - 1)) + else: + # Case r > 1 + val = infty + for j in range(n): + valj, left, paramj = signature_prev[j] + left_interval = max(0, ceil((x - left) / q)) + right_interval = max(0, floor((y - 1 - left) / q)) + if left_interval > right_interval: + continue + val = val.inf(drift * left_interval + valj) + if left_interval < right_interval: + val = val.inf(drift * right_interval + valj) + val = val + w + signature.append((val, x, param)) + #valuation = valuation.inf(val) + + # The halting criterion + # if q > bound: + # valthresold = valuation + thresold + # if drift > thresold*2 and all(signature[i][0] > valthresold for i in range(1, n)): + # return valuation + + q = pq + drift = drift * p + diff + signature_prev = signature + indices_prev = indices + + valuation = affine_function(0, 0, start) + for val, _, _ in signature: + valuation = valuation.inf(val) + return valuation + def dwork_image(self, p): r""" Return the parameters obtained by applying the Dwork map to each of From b257e4c67553b5f224aee4367729316359d3a828 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Sat, 22 Nov 2025 07:50:18 +0100 Subject: [PATCH 75/93] simplify code; use Newton polygons directly --- .../functions/hypergeometric_algebraic.py | 3 +- .../functions/hypergeometric_parameters.py | 105 +++--------------- 2 files changed, 16 insertions(+), 92 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 3083dbcd711..2db46047754 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -1195,8 +1195,7 @@ def newton_polygon(self, log_radius=None): val = self._parameters.valuation_function(self._p, start) except ValueError: raise ValueError("infinite Newton polygon; try to truncate it by giving a log radius less than %s" % convergence) - vertices, _ = val.defn() - return NewtonPolygon(vertices, last_slope=log_radius) + return NewtonPolygon(val, last_slope=log_radius) def _truncation_bound(self, log_radius, prec): convergence = self.log_radius_of_convergence() diff --git a/src/sage/functions/hypergeometric_parameters.py b/src/sage/functions/hypergeometric_parameters.py index 21e171a1eea..2def4ef4746 100644 --- a/src/sage/functions/hypergeometric_parameters.py +++ b/src/sage/functions/hypergeometric_parameters.py @@ -36,79 +36,6 @@ from sage.matrix.special import identity_matrix -# Functions defined as min of affine functions -############################################## - -class MinFunction(): - def __init__(self, subgraph=None, start=None): - if subgraph is None: - subgraph = Polyhedron(ieqs=[[0, 0, 0]]) - if start is not None: - P = Polyhedron(ieqs=[[-start, 1, 0]]) - subgraph = subgraph.intersection(P) - self._subgraph = subgraph - - def defn(self): - affine = [] - start = None - for u, v, w in self._subgraph.inequalities_list(): - if w == 0: - start = -u/v - else: - affine.append((-v/w, -u/w)) - return affine, start - - def __repr__(self): - affine, start = self.defn() - S = PolynomialRing(QQ, 'x') - fs = [str(a*S.gen() + b) for a, b, in affine] - if len(fs) == 0: - s = "+infinity" - elif len(fs) == 1: - s = fs[0] - else: - s = "min(" + ", ".join(fs) + ")" - if start is not None: - s += " on [%s, +infty)" % start - return s - - def inf(self, gs): - subgraph = self._subgraph.intersection(gs._subgraph) - return MinFunction(subgraph) - - def __mul__(self, c): - if c in QQ and c >= 0: - ieqs = [(c*u, c*v, w) for u, v, w in self._subgraph.inequalities_list()] - return MinFunction(Polyhedron(ieqs=ieqs)) - - def __add__(self, other): - if other in QQ: - ieqs = [(u - other*w, v, w) for u, v, w in self._subgraph.inequalities_list()] - if isinstance(other, MinFunction): - ieqs = [(-u*wp - up*w, -v*wp - vp*w, -w*wp) - for u, v, w in self._subgraph.inequalities_list() - for up, vp, wp in other._subgraph.inequalities_list()] - return MinFunction(Polyhedron(ieqs=ieqs)) - - def __call__(self, x): - L = Polyhedron(eqns=[[-x, 1, 0]]).intersection(self._subgraph) - if L.is_empty(): - raise ValueError("not defined") - v = L.inequalities_list() - if not v: - return infinity - return -v[0][0] / v[0][2] - - -def affine_function(a=None, b=None, start=None): - ieqs = [] - if a is not None: - ieqs.append([b, a, -1]) - if start is not None: - ieqs.append([-start, 1, 0]) - return MinFunction(Polyhedron(ieqs=ieqs)) - - # Parameters of hypergeometric functions ######################################## @@ -379,7 +306,7 @@ def interlacing_number(self, c): for _, _, paren in self.christol_sorting(c): if paren == 1 and previous_paren == -1: interlacing += 1 - previous_paren = paren + previous_paren = paren return interlacing @@ -791,10 +718,12 @@ def valuation_function(self, p, start=0): params, shift = self.prepare_parameters(p) diff = sum(dw for _, dw in params) n = len(params) + 1 - infty = affine_function(start=start) - drift = affine_function(1, shift, start) + + rays = [[0, 1], [1, -start]] + infty = Polyhedron(ambient_dim=2) + drift = Polyhedron(vertices=[[1, shift]], rays=rays) + signature_prev = None - indices = {} count = 0 q = 1 for _ in range(step): @@ -804,19 +733,17 @@ def valuation_function(self, p, start=0): jumps = [(0, 0, 0)] + sorted(jumps) + [(pq, None, 0)] signature = [] - indices = {} w = 0 for i in range(n): x, dw, param = jumps[i] # discontinuity point y, _, right = jumps[i+1] # next discontinuity point w += dw # the value of V({k/p^r}, p^r) on this interval - indices[param] = len(signature) if x == y: # Case of empty interval val = infty elif signature_prev is None: # Case r = 1 - val = (drift * x).inf(drift * (y - 1)) + val = (x * drift).convex_hull((y - 1) * drift) else: # Case r > 1 val = infty @@ -824,14 +751,13 @@ def valuation_function(self, p, start=0): valj, left, paramj = signature_prev[j] left_interval = max(0, ceil((x - left) / q)) right_interval = max(0, floor((y - 1 - left) / q)) - if left_interval > right_interval: - continue - val = val.inf(drift * left_interval + valj) + if left_interval <= right_interval: + val = val.convex_hull(left_interval * drift + valj) if left_interval < right_interval: - val = val.inf(drift * right_interval + valj) - val = val + w + val = val.convex_hull(right_interval * drift) + val = val.translation((0, w)) signature.append((val, x, param)) - #valuation = valuation.inf(val) + # valuation = valuation.convex_hull(val) # The halting criterion # if q > bound: @@ -842,12 +768,11 @@ def valuation_function(self, p, start=0): q = pq drift = drift * p + diff signature_prev = signature - indices_prev = indices - valuation = affine_function(0, 0, start) + valuation = Polyhedron(rays=rays) for val, _, _ in signature: - valuation = valuation.inf(val) - return valuation + valuation = valuation.convex_hull(val) + return valuation.vertices_list() def dwork_image(self, p): r""" From 2d55147421530ae980a0daef1807a044872b971d Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Sat, 22 Nov 2025 09:21:31 +0100 Subject: [PATCH 76/93] small fixes --- .../functions/hypergeometric_algebraic.py | 16 ++--- .../functions/hypergeometric_parameters.py | 58 +++++++++---------- 2 files changed, 36 insertions(+), 38 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 2db46047754..7f86a274a21 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -931,12 +931,12 @@ def p_curvature_coranks(self): if not self._parameters.is_balanced(): raise NotImplementedError("Only implemented for nFn-1") d = ZZ(self.denominator()) - classes = dict.fromkeys(range(1, len(self.top())+1), Primes(modulus = 0)) + classes = dict.fromkeys(range(1, len(self.top())+1), Primes(modulus=0)) for c in range(d): if gcd(c, d) == 1: Delta = QQ(1/c) % d j = self._parameters.interlacing_number(Delta) - classes[j] = classes[j].union(Primes(modulus = d, classes = [c])) + classes[j] = classes[j].union(Primes(modulus=d, classes=[c])) for p in Primes(): # I am sure one can avoid computing the interlacing number again for # all primes here. @@ -975,10 +975,8 @@ def monodromy(self, x=0, var='z'): [0 1] [1 0] - :: - The bases of the solution space are chosen in a compatible way - across the three singularities of the differential equation: + across the three singularities of the differential equation:: sage: g = hypergeometric([1/9, 4/9, 5/9], [1/3, 1], x) sage: g.monodromy(var='a') @@ -1051,8 +1049,11 @@ def __init__(self, parent, arg1, arg2=None, scalar=None): - ``arg1``, ``arg2`` -- arguments defining this hypergeometric function, they can be: + - the top and bottom paramters + - a hypergeometric function and ``None`` + - an instance of the class :class:`HypergeometricParameters` and ``None`` - ``scalar`` -- an element in the base ring, the scalar by @@ -1108,7 +1109,7 @@ def dwork_image(self): EXAMPLES:: - sage: S. = QQ[] + sage: S. = Qp(7)[] sage: f = hypergeometric([1/4, 1/3, 1/2], [2/5, 3/5, 1], x) sage: f.dwork_image() hypergeometric((1/3, 1/2, 3/4), (1/5, 4/5, 1), x) @@ -1328,7 +1329,7 @@ def p_curvature(self): L = S(L.list()) d = S.gen() p = self._char - rows = [ ] + rows = [] n = L.degree() for i in range(p, p + n): Li = d**i % L @@ -1351,7 +1352,6 @@ def dwork_relation(self): p = self._char H = self.parent() F = H.base_ring() - Hp = H.change_ring(Qp(p, 1)) x = H.polynomial_ring().gen() coeffs = self._coeffs Ps = {} diff --git a/src/sage/functions/hypergeometric_parameters.py b/src/sage/functions/hypergeometric_parameters.py index 2def4ef4746..aad337f77a3 100644 --- a/src/sage/functions/hypergeometric_parameters.py +++ b/src/sage/functions/hypergeometric_parameters.py @@ -26,12 +26,9 @@ from sage.rings.integer_ring import ZZ from sage.rings.rational_field import QQ from sage.rings.finite_rings.integer_mod_ring import IntegerModRing -from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing from sage.rings.semirings.tropical_semiring import TropicalSemiring from sage.geometry.polyhedron.constructor import Polyhedron -from sage.geometry.polyhedron.base0 import Polyhedron_base0 - from sage.matrix.constructor import matrix from sage.matrix.special import identity_matrix @@ -108,8 +105,8 @@ def __init__(self, top, bottom, add_one=True): self.d = 1 self.bound = 1 else: - self.d = lcm([ a.denominator() for a in top ] - + [ b.denominator() for b in bottom ]) + self.d = lcm([a.denominator() for a in top] + + [b.denominator() for b in bottom]) self.bound = 2 * self.d * max(abs(a) for a in top + bottom) + 1 def __repr__(self): @@ -309,7 +306,6 @@ def interlacing_number(self, c): previous_paren = paren return interlacing - def q_christol_sorting(self, q): r""" Return a sorted list of pairs, one associated to each top parameter a, @@ -559,6 +555,9 @@ def valuation_position(self, p, drift=0): \text{val}_p(h_k) + \delta k and the first index `k` where this minimum is reached. + Return in addition (for internal use), the number of summands + in Christol's formula which was taken into account in order to + compute the result. INPUT: @@ -571,18 +570,18 @@ def valuation_position(self, p, drift=0): sage: from sage.functions.hypergeometric_algebraic import HypergeometricParameters sage: pa = HypergeometricParameters([1/5, 1/5, 1/5], [1/3, 3^10/5]) sage: pa.valuation_position(3) - (-9, 1) + (-9, 1, 11) When the relevant sequence is not bounded from below, the tuple ``(-Infinity, None)`` is returned:: sage: pa.valuation_position(5) - (-Infinity, None) + (-Infinity, None, 0) An example with a drift:: sage: pa.valuation_position(3, drift=-7/5) - (-54/5, 7) + (-54/5, 7, 11) """ # We check that we are inside the disk of convergence params, shift = self.prepare_parameters(p) @@ -605,9 +604,10 @@ def valuation_position(self, p, drift=0): thresold = d * sum(dw for _, dw in params if dw > 0) valuation = position = ZZ(0) - valfinal = signature_prev = None + valfinal = None + signature_prev = indices_prev = None indices = {} - count = step = 0 + count = r = 0 q = 1 while True: # We take into account the contribution of V({k/p^r}, p^r). @@ -624,7 +624,7 @@ def valuation_position(self, p, drift=0): # The list signature_prev and the dictionary indices_prev # correspond to the same data for r-1. - step += 1 + r += 1 pq = p * q # We compute the points of discontinuity of V({k/p^r}, p^r) @@ -640,7 +640,7 @@ def valuation_position(self, p, drift=0): signature = [] indices = {} w = 0 - TMstep = matrix(TSR, n) + TMr = matrix(TSR, n) for i in range(n): x, dw, param = jumps[i] # discontinuity point y, _, right = jumps[i+1] # next discontinuity point @@ -672,7 +672,7 @@ def valuation_position(self, p, drift=0): while True: valj, posj, paramj = signature_prev[j] valj += drift * interval - TMstep[i,j] = TSR(drift*interval + w) + TMr[i, j] = TSR(drift*interval + w) if valj < val: val = valj pos = posj + q * interval @@ -689,20 +689,20 @@ def valuation_position(self, p, drift=0): valuation, position, _ = minimum if q > bound: if drift > 2*thresold and all(signature[i][0] > valuation + thresold for i in range(1, n)): - return valuation, position, step + return valuation, position, r if growth == 0: if count < order: - TM = TMstep * TM + TM = TMr * TM count += 1 if count == order: try: TM = TM.weak_transitive_closure() except ValueError: - return -infinity, None, step - valfinal = min(TM[i,j].lift() + signature[j][0] + return -infinity, None, r + valfinal = min(TM[i, j].lift() + signature[j][0] for i in range(n) for j in range(n)) if valuation == valfinal: - return valuation, position, step + return valuation, position, r # We update the values for the next r q = pq @@ -711,7 +711,7 @@ def valuation_position(self, p, drift=0): indices_prev = indices def valuation_function(self, p, start=0): - valstart, _, step = self.valuation_position(p, start) + valstart, _, r = self.valuation_position(p, start) if valstart is -infinity: raise ValueError @@ -724,9 +724,8 @@ def valuation_function(self, p, start=0): drift = Polyhedron(vertices=[[1, shift]], rays=rays) signature_prev = None - count = 0 q = 1 - for _ in range(step): + for _ in range(r): pq = p * q jumps = [(1 + (-pa) % pq, dw, pa) for pa, dw in params] @@ -751,10 +750,12 @@ def valuation_function(self, p, start=0): valj, left, paramj = signature_prev[j] left_interval = max(0, ceil((x - left) / q)) right_interval = max(0, floor((y - 1 - left) / q)) - if left_interval <= right_interval: - val = val.convex_hull(left_interval * drift + valj) + if left_interval > right_interval: + continue + drift_scaled = left_interval * drift if left_interval < right_interval: - val = val.convex_hull(right_interval * drift) + drift_scaled = drift_scaled.convex_hull(right_interval * drift) + val = val.convex_hull(drift_scaled + valj) val = val.translation((0, w)) signature.append((val, x, param)) # valuation = valuation.convex_hull(val) @@ -766,13 +767,10 @@ def valuation_function(self, p, start=0): # return valuation q = pq - drift = drift * p + diff + drift = (p*drift).translation((0, diff)) signature_prev = signature - valuation = Polyhedron(rays=rays) - for val, _, _ in signature: - valuation = valuation.convex_hull(val) - return valuation.vertices_list() + return signature[0][0].vertices_list() def dwork_image(self, p): r""" From 899bb2c112264b05d1305937e1b3347c1a60ecbb Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Sat, 22 Nov 2025 10:05:41 +0100 Subject: [PATCH 77/93] fix lint; inherit from SageObject --- src/sage/functions/hypergeometric_algebraic.py | 8 +++++--- src/sage/functions/hypergeometric_parameters.py | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 7f86a274a21..0ceeafcf373 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -1044,8 +1044,8 @@ def __init__(self, parent, arg1, arg2=None, scalar=None): INPUT: - - ``parent`` -- the parent of this function, which has to be defined - over the p-adics + - ``parent`` -- the parent of this function, which has to be + defined over the p-adics - ``arg1``, ``arg2`` -- arguments defining this hypergeometric function, they can be: @@ -1054,7 +1054,9 @@ def __init__(self, parent, arg1, arg2=None, scalar=None): - a hypergeometric function and ``None`` - - an instance of the class :class:`HypergeometricParameters` and ``None`` + - an instance of the class + :class:`sage.functions.hypergeometric_parameters.HypergeometricParameters` + and ``None`` - ``scalar`` -- an element in the base ring, the scalar by which the hypergeometric function is multiplied diff --git a/src/sage/functions/hypergeometric_parameters.py b/src/sage/functions/hypergeometric_parameters.py index aad337f77a3..0e2291eec26 100644 --- a/src/sage/functions/hypergeometric_parameters.py +++ b/src/sage/functions/hypergeometric_parameters.py @@ -18,6 +18,7 @@ # *************************************************************************** from sage.misc.cachefunc import cached_method +from sage.structure.sage_object import SageObject from sage.functions.other import floor, ceil from sage.arith.functions import lcm @@ -36,7 +37,7 @@ # Parameters of hypergeometric functions ######################################## -class HypergeometricParameters(): +class HypergeometricParameters(SageObject): r""" Class for parameters of hypergeometric functions. """ From 4407ae9687d25de205fec86a6bd45c410d44ee2c Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Sat, 22 Nov 2025 11:06:12 +0100 Subject: [PATCH 78/93] take scalar into account --- .../functions/hypergeometric_algebraic.py | 98 +++++++++++++------ 1 file changed, 66 insertions(+), 32 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 0ceeafcf373..64ccc28d69e 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -797,34 +797,43 @@ def good_reduction_primes(self): modulo which primes a hypergeometric function can be reduced ([CFV2025]_, Thm. 3.1.3). For small primes `p`, we compute the `p`-adic valuation of the hypergeometric function individually. + + TESTS:: + + sage: h = hypergeometric([1/5, 2/5, 3/5], [1/2, 1/7, 1/11], x) + sage: h.good_reduction_primes() + Finite set of prime numbers: 2, 7, 11 """ params = self._parameters d = params.d - # We check the parenthesis criterion for c=1 if not params.parenthesis_criterion(1): - return Primes(modulus=0) - - # We check the parenthesis criterion for other c - # and derive congruence classes with good reduction - cs = [c for c in range(d) if d.gcd(c) == 1] - goods = {c: None for c in cs} - goods[1] = True - for c in cs: - if goods[c] is not None: - continue - cc = c - goods[c] = True - while cc != 1: - if goods[cc] is False or not params.parenthesis_criterion(cc): - goods[c] = False - break - cc = (cc * c) % d - if goods[c]: + # Easy case: + # the parenthesis criterion is not fulfilled for c=1 + goods = {} + + else: + + # We check the parenthesis criterion for other c + # and derive congruence classes with good reduction + cs = [c for c in range(d) if d.gcd(c) == 1] + goods = {c: None for c in cs} + goods[1] = True + for c in cs: + if goods[c] is not None: + continue cc = c + goods[c] = True while cc != 1: - goods[cc] = True + if goods[cc] is False or not params.parenthesis_criterion(cc): + goods[c] = False + break cc = (cc * c) % d + if goods[c]: + cc = c + while cc != 1: + goods[cc] = True + cc = (cc * c) % d # We treat exceptional primes bound = params.bound @@ -832,11 +841,10 @@ def good_reduction_primes(self): for p in Primes(): if p > bound: break - if d % p == 0 and self.valuation(p) >= 0: + val = self.valuation(p) + if val >= 0: exceptions[p] = True - if d % p == 0 or not goods[p % d]: - continue - if self.valuation(p) < 0: + elif val < 0 and goods.get(p % d, False): exceptions[p] = False goods = [c for c, v in goods.items() if v] @@ -1091,9 +1099,11 @@ def residue(self): Hypergeometric functions in x over Finite Field of size 5 """ k = self.base_ring().residue_field() - if self._scalar.valuation() == 0: + valscalar = self._scalar.valuation() + if valscalar == 0: return self.change_ring(k) val, pos, _ = self._parameters.valuation_position(self._p) + val += valscalar if val < 0: raise ValueError("bad reduction") if val > 0: @@ -1101,7 +1111,7 @@ def residue(self): return H(self._parameters, scalar=0) raise NotImplementedError("the reduction is not a hypergeometric function") # In fact, it is x^s * h[s] * h, with - # . s = pos + # . s is pos # . h = self.shift(s) def dwork_image(self): @@ -1184,21 +1194,39 @@ def valuation(self, log_radius=0, position=False): """ drift = -log_radius / self._e val, pos, _ = self._parameters.valuation_position(self._p, drift) + val += self._scalar.valuation() if position: return val, pos else: return val def newton_polygon(self, log_radius=None): + r""" + TESTS:: + + sage: S. = Qp(19)[] + sage: h = hypergeometric([1/5, 2/5, 3/5, 1/11], [1/2, 1/7], x) + sage: h.newton_polygon() + Traceback (most recent call last): + ... + ValueError: infinite Newton polygon; try to truncate it by giving a log radius less than 1/18 + sage: h.newton_polygon(1/18 - 1/2^10) + Infinite Newton polygon with 4 vertices: (0, 0), (10, -1), (11, -1), (144, 6) ending by an infinite line of slope 503/9216 + """ + scalar = self._scalar + if scalar == 0: + raise ValueError convergence = self.log_radius_of_convergence() if log_radius is None: log_radius = convergence start = -log_radius / self._e try: - val = self._parameters.valuation_function(self._p, start) + vertices = self._parameters.valuation_function(self._p, start) except ValueError: raise ValueError("infinite Newton polygon; try to truncate it by giving a log radius less than %s" % convergence) - return NewtonPolygon(val, last_slope=log_radius) + valscalar = self._scalar.valuation() + vertices = [[k, v + valscalar] for k, v in vertices] + return NewtonPolygon(vertices, last_slope=log_radius) def _truncation_bound(self, log_radius, prec): convergence = self.log_radius_of_convergence() @@ -1227,26 +1255,32 @@ def tate_series(self, log_radius, prec=None): K = self.base_ring() name = self.parent().variable_name() S = TateAlgebra(K, log_radii=[log_radius], names=name) + scalar = self._scalar + if scalar == 0: + return S.zero() if prec is None: prec = self.base_ring().precision_cap() - trunc = self._truncation_bound(log_radius, prec) + trunc = self._truncation_bound(log_radius, prec - scalar.valuation()) self._compute_coeffs(trunc) coeffs = {(i,): self._coeffs[i] for i in range(trunc)} - return self._scalar * S(coeffs, prec) + return scalar * S(coeffs, prec) def __call__(self, x): K = self.base_ring() + scalar = self._scalar + if scalar == 0: + return K.zero() x = K(x) val = min(x.valuation(), x.precision_absolute()) if val is infinity: return K.one() w = self.valuation(-val) prec = w + K.precision_cap() - trunc = self._truncation_bound(-val, prec) + trunc = self._truncation_bound(-val, prec - scalar.valuation()) self._compute_coeffs(trunc) ans = sum(self._coeffs[i] * x**i for i in range(trunc)) ans = ans.add_bigoh(prec) - return self._scalar * ans + return scalar * ans # Over prime finite fields From 355a4f685f9b81a1848b571d51ebaef446759165 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Sat, 22 Nov 2025 12:37:11 +0100 Subject: [PATCH 79/93] fix doctest --- src/sage/functions/hypergeometric_algebraic.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 64ccc28d69e..ad1c598c275 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -1187,10 +1187,10 @@ def valuation(self, log_radius=0, position=False): :: - sage: S. = Qp(5)[] - sage: g = 1/5 * hypergeometric([1/3, 2/3], [5^3/3], x) - sage: g.valuation(-1, position=True) - (-2, 1) + sage: S. = Qp(5)[] + sage: g = 1/5 * hypergeometric([1/3, 2/3], [5^3/3], x) + sage: g.valuation(-1, position=True) + (-3, 1) """ drift = -log_radius / self._e val, pos, _ = self._parameters.valuation_position(self._p, drift) From 804ef53b1c23c2be3b8e0f38ca63c62f1f17a2d4 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Sat, 22 Nov 2025 12:51:43 +0100 Subject: [PATCH 80/93] getitem and lazy power series --- .../functions/hypergeometric_algebraic.py | 95 +++++++------------ .../functions/hypergeometric_parameters.py | 2 +- 2 files changed, 37 insertions(+), 60 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index ad1c598c275..d9a181a4559 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -59,6 +59,7 @@ from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing from sage.rings.power_series_ring import PowerSeriesRing +from sage.rings.lazy_series_ring import LazyPowerSeriesRing from sage.rings.tate_algebra import TateAlgebra from sage.rings.polynomial.ore_polynomial_ring import OrePolynomialRing @@ -472,7 +473,7 @@ def _compute_coeffs(self, prec): c /= b + i coeffs.append(c) - def coefficient(self, n): + def __getitem__(self, n): r""" Return the ``n``-th coefficient of the series representation of this hypergeoimetric function. @@ -485,16 +486,37 @@ def coefficient(self, n): sage: S. = QQ[] sage: f = hypergeometric([1/3, 2/3], [1/2], x) - sage: f.coefficient(9) + sage: f[9] 409541017600/2541865828329 sage: g = f % 5 - sage: g.coefficient(9) + sage: g[9] 0 """ self._compute_coeffs(n+1) S = self.base_ring() return S(self._coeffs[n]) + def coefficient(self, n): + r""" + Return the ``n``-th coefficient of the series representation of this + hypergeoimetric function. + + INPUT: + + - ``n`` -- a non-negative integer + + EXAMPLES: + + sage: S. = QQ[] + sage: f = hypergeometric([1/3, 2/3], [1/2], x) + sage: f.coefficient(9) + 409541017600/2541865828329 + sage: g = f % 5 + sage: g.coefficient(9) + 0 + """ + return self[n] + def power_series(self, prec=20): r""" Return the power series representation of this hypergeometric @@ -511,9 +533,13 @@ def power_series(self, prec=20): sage: f.power_series(3) 1 + 4/9*x + 80/243*x^2 + O(x^3) """ - S = self.parent().power_series_ring() - self._compute_coeffs(prec) - return S(self._coeffs, prec=prec) + if prec is infinity: + S = self.parent().power_series_ring(infinity) + return S(lambda n: self[n]) + else: + S = self.parent().power_series_ring() + self._compute_coeffs(prec) + return S(self._coeffs, prec=prec) def shift(self, s): r""" @@ -1287,62 +1313,10 @@ def __call__(self, x): class HypergeometricAlgebraic_GFp(HypergeometricAlgebraic): def __init__(self, parent, arg1, arg2=None, scalar=None): - # TODO: do we want to simplify automatically if the - # hypergeometric series is a polynomial? HypergeometricAlgebraic.__init__(self, parent, arg1, arg2, scalar) self._p = p = self.base_ring().cardinality() self._coeffs = [Qp(p, 1)(self._scalar)] - def power_series(self, prec=20): - S = self.parent().power_series_ring() - self._compute_coeffs(prec) - try: - f = S(self._coeffs, prec=prec) - except ValueError: - raise ValueError("denominator appears in the series at the required precision") - return f - - def is_almost_defined(self): - p = self._char - d = self.denominator() - if d.gcd(p) > 1: - return False - u = 1 - if not self._parameters.parenthesis_criterion(u): - return False - u = p % d - while u != 1: - if not self._parameters.parenthesis_criterion(u): - return False - u = p*u % d - return True - - def is_defined(self): - p = self._char - if not self.is_almost_defined(): - return False - bound = self._parameters.bound - if bound < p: - return True - prec = 1 + p ** ceil(log(self._parameters.bound, p)) - try: - self.series(prec) - except ValueError: - return False - return True - - def is_defined_conjectural(self): - p = self._char - if not self.is_almost_defined(): - return False - bound = self._parameters.bound - q = p - while q <= bound: - if not self._parameters.q_parenthesis_criterion(q): - return False - q *= p - return True - def __call__(self, x): return self.polynomial()(x) @@ -1553,7 +1527,10 @@ def polynomial_ring(self): return PolynomialRing(self.base_ring(), self._name) def power_series_ring(self, default_prec=None): - return PowerSeriesRing(self.base_ring(), self._name, default_prec=default_prec) + if default_prec is infinity: + return LazyPowerSeriesRing(self.base_ring(), self._name) + else: + return PowerSeriesRing(self.base_ring(), self._name, default_prec=default_prec) # Helper functions diff --git a/src/sage/functions/hypergeometric_parameters.py b/src/sage/functions/hypergeometric_parameters.py index 0e2291eec26..d240c21c87a 100644 --- a/src/sage/functions/hypergeometric_parameters.py +++ b/src/sage/functions/hypergeometric_parameters.py @@ -711,7 +711,7 @@ def valuation_position(self, p, drift=0): signature_prev = signature indices_prev = indices - def valuation_function(self, p, start=0): + def valuation_function(self, p, start): valstart, _, r = self.valuation_position(p, start) if valstart is -infinity: raise ValueError From 9f7e81d6eeec8d31a35867e730dc5758fc5ab0c6 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Sun, 23 Nov 2025 16:23:30 +0100 Subject: [PATCH 81/93] the valuation is given by the first term in signature --- src/sage/functions/hypergeometric_parameters.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/sage/functions/hypergeometric_parameters.py b/src/sage/functions/hypergeometric_parameters.py index d240c21c87a..9396bf72397 100644 --- a/src/sage/functions/hypergeometric_parameters.py +++ b/src/sage/functions/hypergeometric_parameters.py @@ -604,7 +604,6 @@ def valuation_position(self, p, drift=0): bound = 1 + max(pa.abs() for pa, _ in params) thresold = d * sum(dw for _, dw in params if dw > 0) - valuation = position = ZZ(0) valfinal = None signature_prev = indices_prev = None indices = {} @@ -686,11 +685,10 @@ def valuation_position(self, p, drift=0): signature.append((val + w, pos, param)) # The halting criterion - minimum = min(signature) - valuation, position, _ = minimum if q > bound: + valuation, position, _ = signature[0] if drift > 2*thresold and all(signature[i][0] > valuation + thresold for i in range(1, n)): - return valuation, position, r + return ZZ(valuation), ZZ(position), r if growth == 0: if count < order: TM = TMr * TM @@ -700,10 +698,9 @@ def valuation_position(self, p, drift=0): TM = TM.weak_transitive_closure() except ValueError: return -infinity, None, r - valfinal = min(TM[i, j].lift() + signature[j][0] - for i in range(n) for j in range(n)) + valfinal = min(TM[0, j].lift() + signature[j][0] for i in range(n)) if valuation == valfinal: - return valuation, position, r + return ZZ(valuation), ZZ(position), r # We update the values for the next r q = pq From f7f9b589eba7e60fd494f75b230140f8b8d1d55c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20F=C3=BCrnsinn?= Date: Sun, 23 Nov 2025 21:18:34 +0100 Subject: [PATCH 82/93] documentation --- .../functions/hypergeometric_algebraic.py | 72 ++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index e141afba0cf..8e2c7dcb827 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -1256,6 +1256,7 @@ def power_series(self, prec=20): raise ValueError("denominator appears in the series at the required precision") return f + # Obsolete? def is_almost_defined(self): p = self._char d = self.denominator() @@ -1271,6 +1272,7 @@ def is_almost_defined(self): u = p*u % d return True + # Obsolete? def is_defined(self): p = self._char if not self.is_almost_defined(): @@ -1285,6 +1287,7 @@ def is_defined(self): return False return True + # Obsolete? def is_defined_conjectural(self): p = self._char if not self.is_almost_defined(): @@ -1313,6 +1316,31 @@ def is_algebraic(self): return True def p_curvature(self): + r""" + Return the matrix of the `p`-curvature of the associated differential + operator, in the standard basis. + + EXAMLES:: + + sage: S. = GF(5)[] + sage: f = hypergeometric ([1/9, 4/9, 5/9], [1/3, 1], x) + sage: f.p_curvature() + [ 0 2/(x^5 + 4*x^4) 1/(x^4 + 4*x^3)] + [ 0 0 0] + [ 0 0 0] + + The following example defines an algebraic function over ``QQ``, thus + its p-curvature vanishes for almost all of its reductions.:: + + sage: S. = QQ[] + sage: f = hypergeometric([1/3, 2/3], [1/2], x) + sage: f.is_algebraic() + True + sage: g = f % 5 + sage: g.p_curvature() + [0 0] + [0 0] + """ L = self.differential_operator() K = L.base_ring().fraction_field() S = OrePolynomialRing(K, L.parent().twisting_derivation().extend_to_fraction_field(), names='d') @@ -1365,6 +1393,30 @@ def annihilating_ore_polynomial(self, var='Frob'): # QUESTION: does this method actually return the # minimal Ore polynomial annihilating self? # Probably not :-( + r""" + Return an Ore polynomaial in the Frobenius morphism, that annihilates + this hypergeometric function. + + INPUT: + + - ``var`` -- a string (default: ``Frob``), name of the varaiable + representing the Frobenius morphism. + + EXAMPLES:: + + sage: S. = GF(5)[] + sage: f = hypergeometric([1/3, 2/3], [1/2], x) + sage: f.annihilating_ore_polynomial() + (4*x^10 + 2*x^5 + 4)*Frob^2 + (4*x^3 + 4*x^2 + 1)*Frob + x^2 + sage: ps = f.power_series(100) + sage: (4*x^10 + 2*x^5 + 4)*ps^(5^2) + (4*x^3 + 4*x^2 + 1)*ps^5 + x^2*ps + O(x^100) + + ALGORITHM:: + + We follow the method described in [CFV2025]_, Section 3.2.3, that + also explain why such a polynomial exists. + """ parameters = self._parameters if not parameters.is_balanced(): raise NotImplementedError("the hypergeometric function is not a pFq with q = p-1") @@ -1423,6 +1475,24 @@ def annihilating_ore_polynomial(self, var='Frob'): return insert_zeroes(Ore(ker), order) def is_lucas(self): + # Is it clear that this is correct? Why do we know that f itself + # satisifies the p-Lucas equation, and not just any other root + # of the annihilating polynomial? + r""" + Returns whether this hypergeometric function has the ``p``-Lucas + property. + + EXAMPLES:: + + sage: S. = QQ[] + sage: f = hypergeom([1/5, 4/5], [1], x) + sage: g = f % 19 + sage: g.is_lucas() + True + sage: h = f % 17 + sage: h.is_lucas() + False + """ p = self._char if self._parameters.frobenius_order(p) > 1: # TODO: check this @@ -1431,7 +1501,7 @@ def is_lucas(self): K = S.fraction_field() Ore = OrePolynomialRing(K, K.frobenius_endomorphism(), names='F') Z = Ore(self.annihilating_ore_polynomial()) - Ap = self.series(p).polynomial() + Ap = self.power_series(p).polynomial() F = Ap * Ore.gen() - 1 return (Z % F).is_zero() From c5deeb49de267d67783b9b23068a0fb15ad4f7e4 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Mon, 24 Nov 2025 07:23:05 +0100 Subject: [PATCH 83/93] small fixes --- src/sage/functions/hypergeometric_algebraic.py | 2 +- src/sage/functions/hypergeometric_parameters.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index d9a181a4559..68861b92995 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -120,7 +120,7 @@ def __init__(self, parent, arg1, arg2=None, scalar=None): parameters = HypergeometricParameters(arg1, arg2) char = self.parent()._char if scalar: - if any(b in ZZ and b < 0 for b in parameters.bottom): + if any(b in ZZ and b <= 0 for b in parameters.bottom): raise ValueError("the parameters %s do not define a hypergeometric function" % parameters) if char > 0: val, _, _ = parameters.valuation_position(char) diff --git a/src/sage/functions/hypergeometric_parameters.py b/src/sage/functions/hypergeometric_parameters.py index 9396bf72397..852520ab430 100644 --- a/src/sage/functions/hypergeometric_parameters.py +++ b/src/sage/functions/hypergeometric_parameters.py @@ -698,7 +698,7 @@ def valuation_position(self, p, drift=0): TM = TM.weak_transitive_closure() except ValueError: return -infinity, None, r - valfinal = min(TM[0, j].lift() + signature[j][0] for i in range(n)) + valfinal = min(TM[0, j].lift() + signature[j][0] for j in range(n)) if valuation == valfinal: return ZZ(valuation), ZZ(position), r From 3c0d91fe7108919fa39479f4ff2b06092cd79426 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Mon, 24 Nov 2025 07:25:17 +0100 Subject: [PATCH 84/93] cosmetic --- src/sage/functions/hypergeometric_algebraic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 4ed9b00755b..56839165fe5 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -1495,7 +1495,7 @@ def is_lucas(self): # satisifies the p-Lucas equation, and not just any other root # of the annihilating polynomial? r""" - Returns whether this hypergeometric function has the ``p``-Lucas + Return whether this hypergeometric function has the ``p``-Lucas property. EXAMPLES:: @@ -1507,7 +1507,7 @@ def is_lucas(self): True sage: h = f % 17 sage: h.is_lucas() - False + False """ p = self._char if self._parameters.frobenius_order(p) > 1: From fac8c541ea551610f2f96a45842fc390d118db9d Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Tue, 25 Nov 2025 06:12:36 +0100 Subject: [PATCH 85/93] optimize dwork_relation --- .../functions/hypergeometric_algebraic.py | 51 ++++++++++++------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 56839165fe5..efa278a3559 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -78,7 +78,7 @@ class HypergeometricAlgebraic(Element): r""" Class for hypergeometric functions over arbitrary base rings. """ - def __init__(self, parent, arg1, arg2=None, scalar=None): + def __init__(self, parent, arg1, arg2=None, scalar=None, check=True): r""" Initialize this hypergeometric function. @@ -119,7 +119,7 @@ def __init__(self, parent, arg1, arg2=None, scalar=None): else: parameters = HypergeometricParameters(arg1, arg2) char = self.parent()._char - if scalar: + if check and scalar: if any(b in ZZ and b <= 0 for b in parameters.bottom): raise ValueError("the parameters %s do not define a hypergeometric function" % parameters) if char > 0: @@ -1072,7 +1072,7 @@ def is_maximum_unipotent_monodromy(self): # Over the p-adics class HypergeometricAlgebraic_padic(HypergeometricAlgebraic): - def __init__(self, parent, arg1, arg2=None, scalar=None): + def __init__(self, parent, arg1, arg2=None, scalar=None, check=True): r""" Initialize this hypergeometric function. @@ -1103,7 +1103,7 @@ def __init__(self, parent, arg1, arg2=None, scalar=None): sage: TestSuite(h).run() """ - HypergeometricAlgebraic.__init__(self, parent, arg1, arg2, scalar) + HypergeometricAlgebraic.__init__(self, parent, arg1, arg2, scalar, check) K = self.base_ring() self._p = K.prime() self._e = K.e() @@ -1312,8 +1312,8 @@ def __call__(self, x): # Over prime finite fields class HypergeometricAlgebraic_GFp(HypergeometricAlgebraic): - def __init__(self, parent, arg1, arg2=None, scalar=None): - HypergeometricAlgebraic.__init__(self, parent, arg1, arg2, scalar) + def __init__(self, parent, arg1, arg2=None, scalar=None, check=True): + HypergeometricAlgebraic.__init__(self, parent, arg1, arg2, scalar, check) self._p = p = self.base_ring().cardinality() self._coeffs = [Qp(p, 1)(self._scalar)] @@ -1380,6 +1380,14 @@ def dwork_relation(self): Return (P1, h1), ..., (Ps, hs) such that self = P1*h1^p + ... + Ps*hs^p + + TESTS:: + + sage: S. = GF(3)[] + sage: f = hypergeometric([7/8, 9/8, 11/8], [3/2, 7/4], x) + sage: f.dwork_relation() + {hypergeometric((3/8, 5/8, 9/8), (1/2, 5/4), x): 1, + hypergeometric((1, 21/8, 25/8, 27/8), (3, 13/4, 7/2), x): 2*x^7} """ parameters = self._parameters if not parameters.is_balanced(): @@ -1387,22 +1395,27 @@ def dwork_relation(self): p = self._char H = self.parent() F = H.base_ring() - x = H.polynomial_ring().gen() + S = H.polynomial_ring() coeffs = self._coeffs + pas = parameters.top + parameters.bottom + criticals = [(1-pa) % p for pa in pas] + criticals.sort() + criticals.append(p) Ps = {} - for r in range(p): - params = parameters.shift(r).dwork_image(p) + for i in range(len(pas)) + ci = criticals[i] + cj = criticals[i+1] + if cj == ci: + continue + params = parameters.shift(ci).dwork_image(p) _, s, _ = params.valuation_position(p) - h = H(params.shift(s)) - e = s*p + r - if e >= len(coeffs): - self._compute_coeffs(e + 1) - c = F(coeffs[e]) - if c: - if h in Ps: - Ps[h] += c * x**e - else: - Ps[h] = c * x**e + ci += s*p + cj += s*p + h = H(params.shift(s), check=False) + self._compute_coeffs(cj + 1) + P = S(self._coeffs[ci:cj]) + if P: + Ps[h] = Ps.get(h, 0) + (P << ci) return Ps def annihilating_ore_polynomial(self, var='Frob'): From 32a0c08365acdd7a9745138077359baca66444aa Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Tue, 25 Nov 2025 09:45:04 +0100 Subject: [PATCH 86/93] oops --- src/sage/functions/hypergeometric_algebraic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index efa278a3559..7cd3e0d40dd 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -1402,7 +1402,7 @@ def dwork_relation(self): criticals.sort() criticals.append(p) Ps = {} - for i in range(len(pas)) + for i in range(len(pas)): ci = criticals[i] cj = criticals[i+1] if cj == ci: From cee9d0cd60bb599487e983824711af0b244f0507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20F=C3=BCrnsinn?= Date: Thu, 27 Nov 2025 23:09:18 +0100 Subject: [PATCH 87/93] doc, rerurn valuation in QQ instead of ZZ --- .../functions/hypergeometric_algebraic.py | 49 +++++++++++++++++-- .../functions/hypergeometric_parameters.py | 4 +- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 7cd3e0d40dd..42fedada4ad 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -158,6 +158,14 @@ def _repr_(self): return s def _latex_(self): + r""" + EXAMPLES:: + + sage: S. = QQ[]+ + sage: f = hypergeometric([1/3, 2/3], [1/2], x) + sage: f._latex_() + '\\,_{2} F_{1} \\left(\\begin{matrix} \\frac{1}{3},\\frac{2}{3}\\\\\\frac{1}{2}\\end{matrix}; x \\right)' + """ if self._parameters is None: return "0" scalar = self._scalar @@ -1330,6 +1338,23 @@ def polynomial(self): raise NotImplementedError def is_algebraic(self): + # I am convinced that this is true, but strictly speaking we only have + # a statement for almost all primes in the literature. + r""" + Return whether this hypergeometric function is algebraic. + + EXAMPLES:: + + sage: S. = GF(19) + sage: f = hypergeometric([1/5, 2/5, 3/5, 1/11], [1/2, 1/7], x) + sage: f.is_algebraic() + True + + ALGORITHM: + + Every hypergeometric function that can be reduced modulo ``p`` is + algebraic modulo ``p``. + """ return True def p_curvature(self): @@ -1337,7 +1362,7 @@ def p_curvature(self): Return the matrix of the `p`-curvature of the associated differential operator, in the standard basis. - EXAMLES:: + EXAMPLES:: sage: S. = GF(5)[] sage: f = hypergeometric ([1/9, 4/9, 5/9], [1/3, 1], x) @@ -1373,6 +1398,22 @@ def p_curvature(self): def p_curvature_corank(self): # maybe p_curvature_rank is preferable? # TODO: check if it is also correct when the parameters are not balanced + r""" + Return the corant of the ``p``-curvature matrix. + + EXAMPLES:: + + sage: S. = GF(5)[] + sage: f = hypergeometric([1/9, 4/9, 5/9], [1/3, 1], x) + sage: f.p_curvature_corank() + 2 + + ALGORITHM: + + We use [CFV2025]_, Thm. 3.1.17 and the fact that the corank of the + p-curvature agrees with the number of solutions of the hypergeometric + differential equation. + """ return self._parameters.q_interlacing_number(self._char) def dwork_relation(self): @@ -1386,8 +1427,8 @@ def dwork_relation(self): sage: S. = GF(3)[] sage: f = hypergeometric([7/8, 9/8, 11/8], [3/2, 7/4], x) sage: f.dwork_relation() - {hypergeometric((3/8, 5/8, 9/8), (1/2, 5/4), x): 1, - hypergeometric((1, 21/8, 25/8, 27/8), (3, 13/4, 7/2), x): 2*x^7} + {hypergeometric((1, 21/8, 25/8, 27/8), (3, 13/4, 7/2), x): 2*x^7, + hypergeometric((3/8, 5/8, 9/8), (1/2, 5/4), x): 1} """ parameters = self._parameters if not parameters.is_balanced(): @@ -1514,7 +1555,7 @@ def is_lucas(self): EXAMPLES:: sage: S. = QQ[] - sage: f = hypergeom([1/5, 4/5], [1], x) + sage: f = hypergeometric([1/5, 4/5], [1], x) sage: g = f % 19 sage: g.is_lucas() True diff --git a/src/sage/functions/hypergeometric_parameters.py b/src/sage/functions/hypergeometric_parameters.py index 852520ab430..fa51fdfe0d1 100644 --- a/src/sage/functions/hypergeometric_parameters.py +++ b/src/sage/functions/hypergeometric_parameters.py @@ -688,7 +688,7 @@ def valuation_position(self, p, drift=0): if q > bound: valuation, position, _ = signature[0] if drift > 2*thresold and all(signature[i][0] > valuation + thresold for i in range(1, n)): - return ZZ(valuation), ZZ(position), r + return QQ(valuation), ZZ(position), r if growth == 0: if count < order: TM = TMr * TM @@ -700,7 +700,7 @@ def valuation_position(self, p, drift=0): return -infinity, None, r valfinal = min(TM[0, j].lift() + signature[j][0] for j in range(n)) if valuation == valfinal: - return ZZ(valuation), ZZ(position), r + return QQ(valuation), ZZ(position), r # We update the values for the next r q = pq From 1903eb61bfef53b6e6bd27d980838f10a3340758 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Sat, 29 Nov 2025 07:34:56 +0100 Subject: [PATCH 88/93] implement is_equal_as_series (first attempt) --- .../functions/hypergeometric_algebraic.py | 72 +++++++++++++++---- 1 file changed, 58 insertions(+), 14 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 42fedada4ad..99714b996d5 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -1337,6 +1337,55 @@ def degree(self): def polynomial(self): raise NotImplementedError + @coerce_binop + def is_equal_as_series(self, other): + r""" + TESTS:: + + sage: S. = GF(13)[] + sage: h1 = hypergeometric([1/12, 1/6], [1/3], x) + sage: h2 = hypergeometric([1/12, 1/4], [1/2], x) + sage: h1.is_equal_as_series(h2) + True + """ + if self == other: + return True + H = self.parent() + p = self._p + queued = [(self, other)] + checked = {} + index = 0 + while index < len(queued): + left, right = queued[index] + index += 1 + if (left, right) in checked or (right, left) in checked: + continue + checked[(left, right)] = True + lpa = left._parameters + rpa = right._parameters + criticals = [(1-pa) % p for pa in lpa.top + lpa.bottom + rpa.top + rpa.bottom] + criticals.sort() + criticals.append(p) + for i in range(len(criticals) - 1): + ei = criticals[i] + ej = criticals[i+1] + if ei == ej: + continue + ldpa = lpa.shift(ei).dwork_image(p) + _, lpos, _ = ldpa.valuation_position(p) + rdpa = rpa.shift(ei).dwork_image(p) + _, rpos, _ = rdpa.valuation_position(p) + if lpos != rpos: + return False + if left[ei + lpos*p] == 0 and right[ei + rpos*p] == 0: + continue + if any(left[r + lpos*p] != right[r + rpos*p] for r in range(ei, ej)): + return False + lsec = H(ldpa.shift(lpos)) + rsec = H(rdpa.shift(rpos)) + queued.append((lsec, rsec)) + return True + def is_algebraic(self): # I am convinced that this is true, but strictly speaking we only have # a statement for almost all primes in the literature. @@ -1545,9 +1594,6 @@ def annihilating_ore_polynomial(self, var='Frob'): return insert_zeroes(Ore(ker), order) def is_lucas(self): - # Is it clear that this is correct? Why do we know that f itself - # satisifies the p-Lucas equation, and not just any other root - # of the annihilating polynomial? r""" Return whether this hypergeometric function has the ``p``-Lucas property. @@ -1562,18 +1608,16 @@ def is_lucas(self): sage: h = f % 17 sage: h.is_lucas() False + + :: + + sage: S. = GF(11)[] + sage: h = hypergeometric([1/10, 5/24], [5/12], x) + sage: h.is_lucas() + True """ - p = self._char - if self._parameters.frobenius_order(p) > 1: - # TODO: check this - return False - S = self.parent().polynomial_ring() - K = S.fraction_field() - Ore = OrePolynomialRing(K, K.frobenius_endomorphism(), names='F') - Z = Ore(self.annihilating_ore_polynomial()) - Ap = self.power_series(p).polynomial() - F = Ap * Ore.gen() - 1 - return (Z % F).is_zero() + return all(P.degree() < self._p and self.is_equal_as_series(h) + for h, P in self.dwork_relation().items()) # Parent From 8bc41add0b6a21d97001adcd98083720add899d1 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Sat, 29 Nov 2025 07:42:26 +0100 Subject: [PATCH 89/93] fix doctest --- src/sage/functions/hypergeometric_algebraic.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 99714b996d5..b28bc84400d 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -161,7 +161,7 @@ def _latex_(self): r""" EXAMPLES:: - sage: S. = QQ[]+ + sage: S. = QQ[] sage: f = hypergeometric([1/3, 2/3], [1/2], x) sage: f._latex_() '\\,_{2} F_{1} \\left(\\begin{matrix} \\frac{1}{3},\\frac{2}{3}\\\\\\frac{1}{2}\\end{matrix}; x \\right)' @@ -1392,17 +1392,15 @@ def is_algebraic(self): r""" Return whether this hypergeometric function is algebraic. + This method always returns ``True`` since every hypergeometric + function in characteristic `p` is algebraic. + EXAMPLES:: - sage: S. = GF(19) + sage: S. = GF(13)[] sage: f = hypergeometric([1/5, 2/5, 3/5, 1/11], [1/2, 1/7], x) sage: f.is_algebraic() True - - ALGORITHM: - - Every hypergeometric function that can be reduced modulo ``p`` is - algebraic modulo ``p``. """ return True From c1c134056cbd5897bc33ff86ca3e02d2ee354b2f Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Sat, 29 Nov 2025 12:10:27 +0100 Subject: [PATCH 90/93] fix ruff --- .../functions/hypergeometric_algebraic.py | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index b28bc84400d..0dfe3a077e1 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -25,8 +25,7 @@ from sage.misc.latex import latex_variable_name from sage.misc.misc_c import prod -from sage.misc.functional import log -from sage.functions.other import floor, ceil +from sage.functions.other import floor from sage.functions.hypergeometric import hypergeometric from sage.arith.misc import gcd from sage.matrix.constructor import matrix @@ -1482,9 +1481,7 @@ def dwork_relation(self): raise ValueError("the hypergeometric function must be a pFq with q = p-1") p = self._char H = self.parent() - F = H.base_ring() S = H.polynomial_ring() - coeffs = self._coeffs pas = parameters.top + parameters.bottom criticals = [(1-pa) % p for pa in pas] criticals.sort() @@ -1511,13 +1508,13 @@ def annihilating_ore_polynomial(self, var='Frob'): # minimal Ore polynomial annihilating self? # Probably not :-( r""" - Return an Ore polynomaial in the Frobenius morphism, that annihilates - this hypergeometric function. + Return an Ore polynomaial in the Frobenius morphism, that + annihilates this hypergeometric function. INPUT: - - ``var`` -- a string (default: ``Frob``), name of the varaiable - representing the Frobenius morphism. + - ``var`` -- a string (default: ``Frob``), name of the variable + representing the Frobenius morphism. EXAMPLES:: @@ -1525,11 +1522,24 @@ def annihilating_ore_polynomial(self, var='Frob'): sage: f = hypergeometric([1/3, 2/3], [1/2], x) sage: f.annihilating_ore_polynomial() (4*x^10 + 2*x^5 + 4)*Frob^2 + (4*x^3 + 4*x^2 + 1)*Frob + x^2 - sage: ps = f.power_series(100) - sage: (4*x^10 + 2*x^5 + 4)*ps^(5^2) + (4*x^3 + 4*x^2 + 1)*ps^5 + x^2*ps - O(x^100) + sage: s = f.power_series(1000) + sage: (4*x^10 + 2*x^5 + 4)*s^(5^2) + (4*x^3 + 4*x^2 + 1)*s^5 + x^2*s + O(x^1000) + + There is no guarantee that the returned Ore polynomial is minimal. + As an illustration, in the next example, the method outputs a Ore + polynomial of degree `2` while `f` is already solution of a Frobenius + equation of degree `1`:: - ALGORITHM:: + sage: S. = GF(11)[] + sage: f = hypergeometric([1/10, 5/24], [5/12], x) + sage: f.annihilating_ore_polynomial() + (8*x^12 + 6*x^11 + 6*x + 10)*Frob^2 + 1 + sage: s = f.power_series(1000) + sage: s == (1 + 5*x)*s^11 + True + + ALGORITHM: We follow the method described in [CFV2025]_, Section 3.2.3, that also explain why such a polynomial exists. From 8c7c2f8245ab7d575d7f2390e890c583408074c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20F=C3=BCrnsinn?= Date: Sun, 30 Nov 2025 00:15:55 +0100 Subject: [PATCH 91/93] Issue with good_reduction_primes for non-balanced hypergeometric functions --- src/sage/functions/hypergeometric_algebraic.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index b28bc84400d..3d87ddb519b 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -841,6 +841,16 @@ def good_reduction_primes(self): params = self._parameters d = params.d + if not len(self.top()) == len(self.bottom())+1: + # sage: S. = QQ[] + # sage: f = hypergeometric([1/4, 2/4, 3/4], [1/8], x) + # sage: f.valuation(31) + # 0 + # sage: f.good_reduction_primes() + # Finite set of prime numbers: 3, 5, 7, 11, 13 + raise NotImplementedError("Currently only implemented for nFn-1.") + + if not params.parenthesis_criterion(1): # Easy case: # the parenthesis criterion is not fulfilled for c=1 From 2940abeacf828e03cb3a749bc406e18a10f52c43 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Sun, 30 Nov 2025 06:06:51 +0100 Subject: [PATCH 92/93] cosmetic --- src/sage/functions/hypergeometric_algebraic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 0dfe3a077e1..b93fba94de4 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -924,7 +924,7 @@ def is_globally_bounded(self, include_infinity=True): INPUT: - - ``include_infinity`` -- Boolean (default: ``True``) + - ``include_infinity`` -- a boolean (default: ``True``) EXAMPLES: From 4e15d05867ea228451ec4ab7feb5dbdff39a4c47 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Sun, 30 Nov 2025 08:41:14 +0100 Subject: [PATCH 93/93] fix good_reduction_primes; certainly, there is a better solution though --- .../functions/hypergeometric_algebraic.py | 81 ++++++++----------- 1 file changed, 32 insertions(+), 49 deletions(-) diff --git a/src/sage/functions/hypergeometric_algebraic.py b/src/sage/functions/hypergeometric_algebraic.py index 5cb6d367fac..b2f7e51e1bf 100644 --- a/src/sage/functions/hypergeometric_algebraic.py +++ b/src/sage/functions/hypergeometric_algebraic.py @@ -833,65 +833,48 @@ def good_reduction_primes(self): TESTS:: - sage: h = hypergeometric([1/5, 2/5, 3/5], [1/2, 1/7, 1/11], x) - sage: h.good_reduction_primes() + sage: f = hypergeometric([1/5, 2/5, 3/5], [1/2, 1/7, 1/11], x) + sage: f.good_reduction_primes() Finite set of prime numbers: 2, 7, 11 + + :: + + sage: g = hypergeometric([1/4, 1/2, 3/4], [1/8], x) + sage: g.good_reduction_primes() + Set of prime numbers congruent to 3, 5, 7 modulo 8: 3, 5, 7, 11, ... + sage: (73*g).good_reduction_primes() + Set of prime numbers congruent to 3, 5, 7 modulo 8 with 73 included: 3, 5, 7, 11, ... """ + scalar = self._scalar + if scalar == 0: + return Primes() params = self._parameters d = params.d - - if not len(self.top()) == len(self.bottom())+1: - # sage: S. = QQ[] - # sage: f = hypergeometric([1/4, 2/4, 3/4], [1/8], x) - # sage: f.valuation(31) - # 0 - # sage: f.good_reduction_primes() - # Finite set of prime numbers: 3, 5, 7, 11, 13 - raise NotImplementedError("Currently only implemented for nFn-1.") - - - if not params.parenthesis_criterion(1): - # Easy case: - # the parenthesis criterion is not fulfilled for c=1 - goods = {} - - else: - - # We check the parenthesis criterion for other c - # and derive congruence classes with good reduction - cs = [c for c in range(d) if d.gcd(c) == 1] - goods = {c: None for c in cs} - goods[1] = True - for c in cs: - if goods[c] is not None: - continue - cc = c - goods[c] = True - while cc != 1: - if goods[cc] is False or not params.parenthesis_criterion(cc): - goods[c] = False - break - cc = (cc * c) % d - if goods[c]: - cc = c - while cc != 1: - goods[cc] = True - cc = (cc * c) % d - - # We treat exceptional primes bound = params.bound + exceptions = {} for p in Primes(): if p > bound: break - val = self.valuation(p) - if val >= 0: - exceptions[p] = True - elif val < 0 and goods.get(p % d, False): - exceptions[p] = False + val, _, _ = params.valuation_position(p) + exceptions[p] = (val + scalar.valuation(p) >= 0) - goods = [c for c, v in goods.items() if v] - return Primes(modulus=d, classes=goods, exceptions=exceptions) + classes = [] + F = None + for c in range(bound, bound + d): + if d.gcd(c) > 1: + continue + val, _, _ = params.valuation_position(c) + if val >= 0: + classes.append(c % d) + if val is not -infinity: + if F is None: + F = scalar.factor() + for p, mult in F: + if p > bound and (p-c) % d == 0: + exceptions[p] = (val + mult >= 0) + + return Primes(modulus=d, classes=classes, exceptions=exceptions) def is_algebraic(self): r"""