ore_algebra.analytic¶
Symbolic-numeric tools
This module implements symbolic-numeric features such as the computation of values of univariate D-finite functions, and connection matrices between regular points of univariate differential operators.
The basic features are accessible through methods of univariate differential
operators, such as
numerical_solution()
and
numerical_transition_matrix()
.
Only more advanced or experimental functionality requires calling this module
directly.
A short introduction to the features most likely to be of interest to casual
users can be found in the paper Rigorous Multiple-Precision Evaluation of
D-Finite Functions in SageMath by the author, available at
<https://arxiv.org/abs/1607.01967>.
See also ore_algebra.examples
for more examples.
Please note that this software is intended both for “end users” interested in performing symbolic-numeric computations with D-finite functions, and as a playground for experimenting with algorithms doing such computations. As a consequence, some features may be undocumented and/or very experimental.
Advanced API (unstable)
ore_algebra.analytic.bounds |
Error bounds |
ore_algebra.analytic.context |
Analytic continuation contexts |
ore_algebra.analytic.function |
D-Finite analytic functions |
ore_algebra.analytic.monodromy |
Monodromy matrices |
ore_algebra.analytic.path |
Analytic continuation paths |
ore_algebra.analytic.polynomial_approximation |
Rigorous approximation of D-finite functions by polynomials |
ore_algebra.analytic.ui |
Some convenience functions for features not yet easily accessible from methods of differential operators. |
Additional examples
ore_algebra.analytic.examples.misc |
Miscellaneous examples |
Basic Usage¶
EXAMPLES:
sage: from ore_algebra import OreAlgebra, DifferentialOperators
sage: Pol.<x> = QQ[]
sage: Dop.<Dx> = OreAlgebra(Pol)
sage: QQi.<i> = QuadraticField(-1)
An operator of order 2 annihilating arctan(x) and the constants:
sage: dop = (x^2 + 1)*Dx^2 + 2*x*Dx
sage: dop.numerical_solution([0, 1], [0, 1], 1e-30)
[0.7853981633974483096156608458198...]
sage: dop.numerical_transition_matrix([0, 1], 1e-20)
[ [1.00...] [0.7853981633974483096...]]
[ [+/- ...] [0.5000000000000000000...]]
Display some information on what is going on:
sage: import logging
sage: logging.basicConfig()
sage: logger = logging.getLogger('ore_algebra.analytic')
sage: logger.setLevel(logging.INFO)
sage: dop = (x^2 + 1)*Dx^2 + 2*x*Dx
sage: dop.numerical_transition_matrix([0, 1], 1e-20)
INFO:ore_algebra.analytic.analytic_continuation:path: 0 --> 1/2 --> 1
INFO:ore_algebra.analytic.analytic_continuation:0 --> 1/2: ordinary case
...
INFO:ore_algebra.analytic.naive_sum:summed ... terms, ...
...
[ [1.00...] [0.7853981633974483096...]]
[ [+/- ...] [0.5000000000000000000...]]
sage: logger.setLevel(logging.WARNING)
An operator annihilating exp + arctan:
sage: dop = (x+1)*(x^2+1)*Dx^3-(x-1)*(x^2-3)*Dx^2-2*(x^2+2*x-1)*Dx
sage: dop.numerical_transition_matrix( [0, 1+i], 1e-10)
[ [1.00...] + [+/- ...]*I [1.017221967...] + [0.402359478...]*I [-1.097056...] + [3.76999161...]*I]
[ [+/- ...] + [+/- ...]*I [0.200000000...] + [-0.40000000...]*I [2.5373878...] + [5.37471057...]*I]
[ [+/- ...] + [+/- ...]*I [-0.04000000...] + [0.280000000...]*I [1.5486939...] + [1.72735528...]*I]
Regular Singular Connection Problems¶
Connection to a singular point:
sage: NF.<sqrt2> = QuadraticField(2)
sage: dop = (x^2 - 2)*Dx^2 + x + 1
sage: dop.numerical_transition_matrix([0, 1, sqrt2], 1e-10)
[ [2.49388...] + [...]*I [2.40894...] + [...]*I]
[[-0.20354...] + [...]*I [0.20437...] + [6.45961...]*I]
This kind of connection matrices linking ordinary points to regular singular points can be used to compute classical special functions, like Bessel functions:
sage: alg = QQbar(-20)^(1/3)
sage: (x*Dx^2 + Dx + x).numerical_transition_matrix([0, alg], 1e-8)
[ [3.7849872...] + [1.7263190...]*I [1.3140884...] + [-2.3112610...]*I]
[ [1.0831414...] + [-3.3595150...]*I [-2.0854436...] + [-0.7923237...]*I]
sage: t = SR.var('t')
sage: f1 = (ln(2) - euler_gamma - I*pi/2)*bessel_J(0, t) - bessel_K(0, I*t)
sage: f2 = bessel_J(0, t)
sage: matrix([[f1, f2], [diff(f1, t), diff(f2, t)]]).subs(t=alg.n()).n()
[ 3.7849872... + 1.7263190...*I 1.3140884... - 2.3112610...*I]
[ 1.0831414... - 3.3595150...*I -2.0854436... - 0.7923237...*I]
or the cosine integral:
sage: dop = x*Dx^3 + 2*Dx^2 + x*Dx
sage: ini = [1, CBF(euler_gamma), 0]
sage: dop.numerical_solution(ini, path=[0, sqrt(2)])
[0.46365280236686...]
sage: CBF(sqrt(2)).ci()
[0.46365280236686...]
sage: dop.numerical_solution(ini, path=[0, 456/123*i+1])
[6.1267878728616...] + [-3.39197789100074...]*I
sage: CBF(456/123*I + 1).ci()
[6.126787872861...] + [-3.391977891000...]*I
The slightly less classical Whittaker functions are an interesting test case as they involve irrational exponents:
sage: dop = 4*x^2*Dx^2 + (-x^2+8*x-11)
sage: dop.numerical_transition_matrix([0, 10])
[[-3.829367993175840...] [7.857756823216673...]]
[[-1.135875563239369...] [1.426170676718429...]]
sage: kappa, mu = CBF(2), CBF(sqrt(3))
sage: z = CBF(10)
sage: dop.numerical_solution(ini=[0,1], path=[0, z]) # Whittaker M
[7.85775682321...] + [+/- ...]*I
sage: (-z/2).exp()*z^(mu+1/2)*z.hypergeometric([mu-kappa+1/2],[1+2*mu])
[7.85775682321...]
This one has both algebraic exponents and an algebraic evaluation point:
sage: alg = NumberField(x^6+86*x^5+71*x^4-80*x^3+2*x^2+7*x+24, 'alg',
....: embedding=CC(0.6515637 + 0.3731162*I)).gen()
sage: dop = 4*x^2*Dx^2 + (-x^2+8*x-11)
sage: dop.numerical_transition_matrix([0, alg])
[[2.503339393562986...] + [-0.714903133441901...]*I [0.2144377477885843...] + [0.3310657638490197...]*I]
[[-0.4755983564143503...] + [2.154602091528463...]*I [0.9461935691709922...] + [0.3918807160953653...]*I]
Another use of “singular” transition matrices is in combinatorics, in relation with singularity analysis. Here is the constant factor in the asymptotic expansion of Apéry numbers (compare M. D. Hirschhorn, Estimating the Apéry numbers, Fibonacci Quart. 50, 2012, 129–131), computed as a connection constant:
sage: _, z, Dz = DifferentialOperators(QQ, 'z')
sage: dop = (z^2*(z^2-34*z+1)*Dz^3 + 3*z*(2*z^2-51*z+1)*Dz^2
....: + (7*z^2-112*z+1)*Dz + (z-5))
sage: roots = dop.leading_coefficient().roots(AA)
sage: roots
[(0, 2), (0.02943725152285942?, 1), (33.97056274847714?, 1)]
sage: mat = dop.numerical_transition_matrix([0, roots[1][0]], 1e-10)
sage: mat.list()
[[4.846055616...],
[-3.77845406...],
[1.473024273...],
[-14.9569783...]*I,
[+/- ...]*I,
[4.546376247...]*I,
[-59.9006990...],
[28.70759161...],
[-18.2076291...]]
sage: cst = -((1/4)*I)*(1+2^(1/2))^2*2^(3/4)/(pi*(2*2^(1/2)-3))
sage: mat[1][2].overlaps(CBF(cst))
True
The EXPERIMENTAL assume_analytic
authorizes paths that go through a singular
point, and makes the assumption that the solution(s) of interest are analytic at
that point:
sage: dop = ((x-1)^2*Dx-1).lclm(Dx-1)
sage: dop.local_basis_monomials(0)
[1, x^2]
sage: dop.numerical_solution([1, 1/2],[0, 2])
Traceback (most recent call last):
...
ValueError: Step 0 --> 2 passes through or too close to singular point 1...
sage: dop.numerical_solution([1, 1/2],[0, 1, 2])
Traceback (most recent call last):
...
NotImplementedError: analytic continuation through irregular singular points
is not supported
sage: dop.numerical_solution([1, 1/2],[0, 2], assume_analytic=True)
[7.389056098930650...] + [+/- ...]*I
Note that it can lead to surprising results. In this example, the equation has order one, and analytic continuation around the singular point yields the same value from both sides, but the function is not analytic:
sage: _.<y> = Pol.fraction_field()[]
sage: y = Pol.fraction_field().extension(y^3 - x^2*(x+1)).gen()
sage: dop = (x*Dx-1).annihilator_of_composition(y)
sage: dop.numerical_solution([1], [0, -1+i, -2], assume_analytic=True)
[-1.587401051968199...] + [+/- ...]*I
sage: dop.numerical_solution([1], [0, -1-i, -2], assume_analytic=True)
[-1.587401051968199...] + [+/- ...]*I
sage: dop.numerical_solution([1], [0, -2], assume_analytic=True)
[0.7937005259840997...] + [1.374729636998602...]*I
Credits¶
The author would like to thank the following people for comments, examples, bug reports, or feedback:
Jakob Ablinger, Frédéric Chyzak, Manuel Kauers, Christoph Koutschan, Christoph Lauter, Pierre Lairez, Yvan Le Borgne, Steve Melczer, Clemens Raab, Bruno Salvy, Emre Sertoz, Armin Straub, Michael Wallner.
Tests¶
TESTS:
sage: import ore_algebra.analytic.polynomial_approximation as pa
sage: Dop(x).numerical_solution([], [0, 1])
0
sage: (Dx - 1).numerical_solution([42], [1])
42.000000000000000
sage: Dx.numerical_solution([1], [0, 1], 1e-10).parent()
Real ball field with 3... precision
sage: logger = logging.getLogger('ore_algebra.analytic.binary_splitting')
sage: logger.setLevel(logging.INFO)
sage: (Dx - 1).numerical_solution([1], [0, i + pi], algorithm="binsplit")
INFO:ore_algebra.analytic.binary_splitting:...
[12.5029695888765...] + [19.4722214188416...]*I
sage: logger.setLevel(logging.WARNING)
sage: (Dx^2 + 1).numerical_solution(vector([1, 0]), [0, 1])
[0.540302305868139...]
sage: (Dx^2 + 1).numerical_solution(column_matrix([0, 1]), [0, 1])
[0.841470984807896...]
sage: (Dx^2 + 1).numerical_solution(range(2), [0, 1])
[0.841470984807896...]
sage: _, y, Dy = DifferentialOperators(QQ, 'y')
sage: (Dx^2 - x).numerical_solution(ini, [0, 2], post_transform=Dy)
Traceback (most recent call last):
...
TypeError: ...
sage: dop = x*Dx^3 + 2*Dx^2 + x*Dx
sage: ini = [1, CBF(euler_gamma), 0] # Cosine integral
sage: dop.numerical_solution(ini, path=[0, sqrt(2)], post_transform=Dx^10)
[-11340.0278985950...]
sage: (Dx^2 - 1).numerical_transition_matrix([1, 1])
[1.0000000000000000 0]
[ 0 1.0000000000000000]
sage: Dop(x).numerical_transition_matrix([0, 1])
[]
sage: _, y, Dy = DifferentialOperators(QuadraticField(-2))
sage: Dy.numerical_transition_matrix([0]).parent()
Full MatrixSpace of 1 by 1 dense matrices over Complex ball field...
sage: (x^2*Dx + 1).numerical_solution([1], [-1, 0])
Traceback (most recent call last):
...
NotImplementedError: analytic continuation through irregular
singular points is not supported
sage: import logging; logging.basicConfig()
sage: logger = logging.getLogger('ore_algebra.analytic.binary_splitting')
sage: logger.setLevel(logging.INFO)
sage: dop = (x^2 + 1)*Dx^2 + 2*x*Dx
sage: dop.numerical_transition_matrix([0,1], algorithm="binsplit")
INFO:ore_algebra.analytic.binary_splitting:...
[ [1.0000000000000...] [0.785398163397448...]]
[ [+/- ...] [0.500000000000000...]]
sage: logger.setLevel(logging.WARNING)
Some corner cases:
sage: (x*Dx + 1).numerical_transition_matrix([0, 1], 1e-10)
[1.00...]
sage: (x*Dx + 1).numerical_transition_matrix([0, 0], 1e-10)
[1.00...]
sage: dop = x*Dx^3 + 2*Dx^2 + x*Dx
sage: mat = dop.numerical_transition_matrix([-1, 0, i, -1])
sage: id = identity_matrix(3)
sage: all(y.rad() < 1e-13 for row in (mat - id) for y in row)
True
A recurrence with constant coefficients:
sage: (Dx - (x - 1)).numerical_solution(ini=[1], path=[0, i/30])
[0.99888940314741...] + [-0.03330865088952795...]*I
Some simple tests involving large non-integer valuations:
sage: dop = (x*Dx-1001/2).symmetric_product(Dx-1)
sage: dop = dop._normalize_base_ring()[-1]
sage: ref = exp(CBF(1/2))/RBF(2)^(1001/2)
sage: ref.overlaps(dop.numerical_transition_matrix([0, 1/2], 1e-10)[0,0])
True
sage: ref.overlaps(dop.numerical_transition_matrix([0, 1/2], 1e-10,
....: algorithm="binsplit")[0,0])
True
sage: ref = exp(CBF(2))/RBF(1/2)^(1001/2)
sage: ref.overlaps(dop.numerical_transition_matrix([0, 2], 1e-10)[0,0])
True
sage: ref.overlaps(dop.numerical_transition_matrix([0, 2], 1e-10,
....: algorithm="binsplit")[0,0])
True
sage: dop = (x*Dx+1001/2).symmetric_product(Dx-1)
sage: dop = dop._normalize_base_ring()[-1]
sage: val = dop.numerical_transition_matrix([0, 1/2], 1e-10)[0,0]
sage: val2 = dop.numerical_transition_matrix([0, 1/2], 1e-10,
....: algorithm="binsplit")[0,0]
sage: val2
[7.632381510...e+150 +/- ...] + [+/- ...]*I
sage: type(val.parent()) is type(val2.parent())
True
sage: ref = CBF(1/2)^(-1001/2)*exp(CBF(1/2))
sage: ref.overlaps(val)
True
sage: ref.overlaps(val2)
True
sage: (CBF(2)^(-1001/2)*exp(CBF(2))).overlaps(dop.numerical_transition_matrix([0, 2], 1e-10)[0,0])
True
sage: h = CBF(1/2)
sage: #dop = (Dx-1).lclm(x^2*Dx^2 - x*(2*x+1999)*Dx + (x^2 + 1999*x + 1000^2))
sage: dop = x^2*Dx^3 + (-3*x^2 - 1997*x)*Dx^2 + (3*x^2 + 3994*x + 998001)*Dx - x^2 - 1997*x - 998001
sage: mat = dop.numerical_transition_matrix([0,1/2], 1e-5)
sage: mat[0,0].overlaps(exp(h))
True
sage: mat[0,1].overlaps(exp(h)*h^1000*log(h))
True
sage: mat[0,2].overlaps(exp(h)*h^1000)
True
sage: mat = dop.numerical_transition_matrix([0,1/2], 1e-5, algorithm="binsplit")
sage: mat[0,0].overlaps(exp(h))
True
sage: mat[0,1].overlaps(exp(h)*h^1000*log(h))
True
sage: mat[0,2].overlaps(exp(h)*h^1000)
True
sage: dop = (x^3 + x^2)*Dx^3 + (-1994*x^2 - 1997*x)*Dx^2 + (994007*x + 998001)*Dx + 998001
sage: mat = dop.numerical_transition_matrix([0, 1/2], 1e-5)
sage: mat[0,0].overlaps(1/(1+h))
True
sage: mat[0,1].overlaps(h^1000/(1+h)*log(h))
True
sage: mat[0,2].overlaps(h^1000/(1+h))
True
sage: mat = dop.numerical_transition_matrix([0, 1/2], 1e-5, algorithm="binsplit")
sage: mat[0,0].overlaps(1/(1+h))
True
sage: mat[0,1].overlaps(h^1000/(1+h)*log(h))
True
sage: mat[0,2].overlaps(h^1000/(1+h))
True
A few larger or harder examples:
sage: _, z, Dz = DifferentialOperators()
sage: dop = ((-1/8*z^2 + 5/21*z - 1/4)*Dz^10 + (5/4*z + 5)*Dz^9
....: + (-4*z^2 + 1/17*z)*Dz^8 + (-2/7*z^2 - 2*z)*Dz^7
....: + (z + 2)*Dz^6 + (z^2 - 5/2*z)*Dz^5 + (-2*z + 2)*Dz^4
....: + (1/2*z^2 + 1/2)*Dz^2 + (-3*z^2 - z + 17)*Dz - 1/9*z^2 + 1)
sage: mat = dop.numerical_transition_matrix([0,1/2], 1e-10)
sage: [mat[k,k] for k in range(mat.nrows())] # TODO double-check
[[1.000000007...],
[1.000003515...],
[1.000007137...],
[1.000008805...],
[1.008705163...],
[0.996364192...],
[9.254196906...],
[1.318793616...],
[-73.6519600...],
[700357.9445...]]
sage: dop = (z+1)*(3*z^2-z+2)*Dz^3 + (5*z^3+4*z^2+2*z+4)*Dz^2 \
....: + (z+1)*Dz + (4*z^3+2*z^2+5)
sage: path = [0,-2/5+3/5*i,-2/5+i,-1/5+7/5*i]
sage: dop.numerical_solution([0,i,0], path, 1e-150)
[-1.5598481440603221187326507993405933893413346644879595004537063375459901302359572361012065551669069...] +
[-0.7107764943512671843673286878693314397759047479618104045777076954591551406949345143368742955333566...]*I
sage: dop = (x^2 - 2)^3*Dx^4 + Dx - x # not checked
sage: dop.numerical_transition_matrix([0, sqrt(2)]).list()
[[0.98516054284204...] + [+/- ...]*I,
[1.43425774983774...] + [+/- ...]*I,
[2.01886239982754...] + [+/- ...]*I,
[2.85137242855000...] + [+/- ...]*I,
[-1.3944458226243...] + [-0.09391189035988...]*I,
[-0.9214294571840...] + [-0.06205560714763...]*I,
[0.06603682810527...] + [0.004447389249634...]*I,
[2.00286623389151...] + [0.134887244173283...]*I,
[-0.2899130252115...] + [0.040592826141673...]*I,
[-1.2687773995494...] + [0.177650729403477...]*I,
[-2.0315916312188...] + [0.284457884625146...]*I,
[-1.7968286340837...] + [0.251587014058884...]*I,
[11.7461242246428...] + [0.845618628311844...]*I,
[7.25750774904584...] + [0.522477340639341...]*I,
[-2.2555651419501...] + [-0.16238104288070...]*I,
[-20.165854280851...] + [-1.45176585140606...]*I]
Operators with rational function coefficients:
sage: dop = (x/x)*Dx - 1
sage: dop.parent()
Univariate Ore algebra in Dx over Fraction Field of Univariate Polynomial Ring in x over Rational Field
sage: dop.numerical_solution([1], [0, 1])
[2.71828182845904...]
sage: dop.numerical_transition_matrix([0, 1])
[[2.71828182845904...]]
sage: dop.local_basis_monomials(0)
[1]
sage: dop.numerical_solution([1], [0,1], 1e-30, algorithm='binsplit')
[2.7182818284590452353602874713...]
sage: _ = pa.on_disk(dop, [1], [0], 1, 1e-3)
sage: ((x/1)*Dx^2 - 1).local_basis_monomials(0)
[1, x]
sage: ((x/1)*Dx^2 - 1).numerical_transition_matrix([0, 1])
[[0.0340875989376363...] [1.59063685463732...]]
[[-0.579827135138349...] [2.27958530233606...]]
sage: ((x/1)*Dx^2 - 1).numerical_transition_matrix([0, 1], algorithm='binsplit')
[[0.0340875989376363...] [1.59063685463732...]]
[[-0.579827135138349...] [2.27958530233606...]]
Algorithm choice:
sage: dop = Dx + 1/4/(x - 1/2)
sage: dop.numerical_transition_matrix([0,1+I,1], 1e-300, algorithm='naive')
[[0.707...399...] + [0.707...399...]*I]
An interesting example borrower from M. Neher (“An Enclosure Method for the
Solution of Linear ODEs with Polynomial Coefficients”, Numerical Functional
Analysis and Optimization 20, 1999, 779–803), where it is currently necessary
to significantly decrease eps
to get precise results:
sage: dop = (Dx^4 - (x^2+10*x+26)*Dx^3 - (-20*x-99-1/2)*Dx^2
....: - (x^2+10*x+25)*Dx - (-2*x^2-4*x+29+1/2))
sage: ini = [5,4,3/2,1/3]
sage: dop.numerical_solution(ini, [0,1])
[10.87312731 +/- ...e-9]
sage: dop.numerical_solution(ini, [0,3/2], 1e-30)
[15.685911746183227 +/- ...e-16]
sage: dop.numerical_solution(ini, [0,5], 1e-150)
[+/- ...e-35]
Handling of algebraic points:
sage: (Dx - i).numerical_solution([1], [sqrt(2), 0])
[0.1559436947653744...] + [-0.9877659459927355...]*I
sage: (Dx - i).numerical_solution([1], [0, sqrt(2)])
[0.1559436947653744...] + [0.9877659459927355...]*I
sage: (Dx - i).numerical_solution([1], [sqrt(2), sqrt(3)])
[0.9499135278648561...] + [0.3125128630622157...]*I
sage: (((x-i)*Dx)^2+1-x).numerical_transition_matrix([i, 2*i])
[ [2.582900...] + [-2.3669708...]*I [0.13647705...] + [0.1829854...]*I]
[[-0.831495...] + [5.21969896...]*I [0.30502246...] + [0.1440307...]*I]
sage: ((x*Dx)^2 + 1 - x*i).numerical_transition_matrix([0,i])
[ [3.75401556...] + [-1.58459278...]*I [0.16222572...] + [0.06847646...]*I]
[ [-5.01978846...] + [2.71131400...]*I [0.21692472...] + [0.11716650...]*I]
sage: NF.<sqrt2> = QuadraticField(2)
sage: ((x*Dx)^2 + 1 - x*sqrt2).numerical_transition_matrix([0,1])
[ [1.21483503...] + [0.72868035...]*I [1.21483503...] + [-0.72868035...]*I]
[[0.8557200...] + [-0.30988046...]*I [0.85572000...] + [0.30988046...]*I]
This used to yield a very coarse enclosure with some earlier versions:
sage: (Dx^2 + x).numerical_solution([1, 0], [0,108])
[0.2731261535202004...]
Miscellaneous tests:
sage: dop = -452*Dx^10 + (-2*x^2 - x - 1/2)*Dx^9 + (1/2*x + 22)*Dx^8 + (1/4*x^2 + x)*Dx^7 + (1/3*x^2 - 1/2*x + 1/3)*Dx^6 + (-3*x^2 + x + 1)*Dx^5 + (x^2 - 4/3*x)*Dx^4 + (2*x^2 - 2*x)*Dx^3 + (2*x^2 + x)*Dx^2 + (-2/3*x^2 - 5/27*x - 1/3)*Dx - 18*x^2 + 6/5*x - 6
sage: ((dop.numerical_transition_matrix([0,1])*dop.numerical_transition_matrix([1, 1+i, 0]) - 1)).norm('frob') < 1e-13
True
sage: ((9*x^2 - 1)*Dx + 18*x).numerical_transition_matrix([0,I,1], squash_intervals=True)
[[-0.125000000000000...] + [+/- ...]*I]
Test suite¶
To run the test suite of the ore_algebra.analytic
subpackage, run:
src$ PYTHONPATH="$PWD" sage -t ore_algebra/analytic/