def wrapper(pt, ord, prec): if RBF(pt.diameter()) >= self.max_rad / 4: return self._known_bound(RBF(pt), post_transform=Dx**ord) try: val = self.approx(pt, prec, post_transform=Dx**ord) except Exception: logger.info("pt=%s, ord=%s, prec=%s, error", pt, ord, prec, exc_info=(pt.absolute_diameter() < .5)) return RIF('nan') logger.debug("pt=%s, ord=%s, prec=%s, val=%s", pt, ord, prec, val) if not pt.overlaps(self._sollya_domain): backtrace = sollya.getbacktrace() logger.debug("%s not in %s", pt.str(style='brackets'), self._sollya_domain.str(style='brackets')) logger.debug("sollya backtrace: %s", [ sollya.objectname(t.struct.called_proc) for t in backtrace ]) return val
def __init__(self, dop, ini, name="dfinitefun", max_prec=256, max_rad=RBF('inf')): self.dop = dop = DifferentialOperator(dop) if not isinstance(ini, dict): ini = {0: ini} if len(ini) != 1: # In the future, we should support specifying several vectors of # initial values. raise NotImplementedError self.ini = ini self.name = name # Global maximum width for the approximation intervals. In the case of # equations with no finite singular point, we try to avoid cancellation # and interval blowup issues by taking polynomial approximations on # intervals on which the general term of the series doesn't grow too # large. The expected order of magnitude of the “top of the hump” is # about exp(κ·|αx|^(1/κ)) and doesn't depend on the base point. We also # let the user impose a maximum width, even in other cases. self.max_rad = RBF(max_rad) if dop.leading_coefficient().is_constant(): kappa, alpha = _growth_parameters(dop) self.max_rad = self.max_rad.min(1 / (alpha * RBF(kappa)**kappa)) self.max_prec = max_prec self._inivecs = {} self._polys = {} self._sollya_object = None self._sollya_domain = RIF('-inf', 'inf') self._max_derivatives = dop.order() self._update_approx_hook = (lambda *args: None)
def monodromy_matrices(dop, base, eps=1e-16, algorithm="connect"): r""" Compute generators of the monodromy group of ``dop`` with base point ``base``. OUTPUT: A list of matrices, each encoding the analytic continuation of solutions along a simple positive loop based in ``base`` around a singular point of ``dop`` (with no other singular point inside the loop). Identity matrices may be omitted. The precise choice of elements of the fundamental group corresponding to each matrix (position with respect to the other singular points, order) are unspecified. EXAMPLES:: sage: from ore_algebra import * sage: from ore_algebra.analytic.monodromy import monodromy_matrices sage: Dops, x, Dx = DifferentialOperators() sage: monodromy_matrices(Dx*x*Dx, 1) [ [ 1.0000... [6.2831853071795...]*I] [ 0 1.0000...] ] sage: monodromy_matrices(Dx*x*Dx, 1, algorithm="loop") [ [ 1.0000... [+/- ...] + [6.283185307179...]*I] [ 0 [1.000000000000...] + [+/- ...]*I] ] sage: monodromy_matrices(Dx*x*Dx, 1/2) [ [ 1.0000... [+/- ...] + [3.1415926535897...]*I] [ 0 [1.0000000000000...]] ] """ dop = DifferentialOperator(dop) base = path.Point(base, dop) if not base.is_regular(): raise ValueError("base point must be regular") eps = RBF(eps) if not (algorithm == "connect" or algorithm == "loop"): raise ValueError("unknown algorithm") id_mat = _identity_matrix(base, eps) def matprod(elts): return prod(reversed(elts), id_mat) # TODO: filter out the factors of the leading coefficient that correspond to # apparent singularities (may require improvements to the analytic # continuation code) tree = _sing_tree(dop, base) polygon_base, local_monodromy_base = _local_monodromy(dop, base, eps, algorithm) result = [] if base.is_ordinary() else local_monodromy_base def dfs(x, path, path_mat, polygon_x, local_monodromy_x): x.seen = True for y in [z for z in tree.neighbors(x) if not z.seen]: logger.info("Computing local monodromy around %s via %s", y, path) polygon_y, local_monodromy_y = _local_monodromy(dop, y, eps, algorithm) anchor_index_x, anchor_x = _closest_unsafe(polygon_x, y) anchor_index_y, anchor_y = _closest_unsafe(polygon_y, x) bypass_mat_x = matprod(local_monodromy_x[:anchor_index_x]) if anchor_index_y > 0: bypass_mat_y = matprod(local_monodromy_y[anchor_index_y:]) else: bypass_mat_y = id_mat edge_mat = dop.numerical_transition_matrix([anchor_x, anchor_y], eps, assume_analytic=True) new_path_mat = bypass_mat_y*edge_mat*bypass_mat_x*path_mat assert isinstance(new_path_mat, Matrix_complex_ball_dense) local_mat = matprod(local_monodromy_y) based_mat = (~new_path_mat)*local_mat*new_path_mat result.append(based_mat) dfs(y, path + [y], new_path_mat, polygon_y, local_monodromy_y) for x in tree: x.seen = False dfs(base, [base], id_mat, polygon_base, local_monodromy_base) return result
def _test_monodromy_matrices(): r""" TESTS:: sage: from ore_algebra.analytic.monodromy import _test_monodromy_matrices sage: _test_monodromy_matrices() """ from sage.all import matrix from ore_algebra import DifferentialOperators Dops, x, Dx = DifferentialOperators() h = QQ(1) / 2 i = QQi.gen() def norm(m): return sum(c.abs()**2 for c in m.list()).sqrtpos() mon = monodromy_matrices((x**2 + 1) * Dx - 1, QQ(1000000)) assert norm(mon[0] - CBF(pi).exp()) < RBF(1e-10) assert norm(mon[1] - CBF(-pi).exp()) < RBF(1e-10) mon = monodromy_matrices((x**2 - 1) * Dx - 1, QQ(0)) assert all(m == -1 for m in mon) dop = (x**2 + 1) * Dx**2 + 2 * x * Dx mon = monodromy_matrices(dop, QQbar(i + 1)) # mon[0] <--> i assert norm(mon[0] - matrix(CBF, [[1, pi * (1 + 2 * i)], [0, 1]])) < RBF(1e-10) assert norm(mon[1] - matrix(CBF, [[1, -pi * (1 + 2 * i)], [0, 1]])) < RBF(1e-10) mon = monodromy_matrices(dop, QQbar(-i + 1)) # mon[0] <--> -i assert norm(mon[0] - matrix(CBF, [[1, pi * (-1 + 2 * i)], [0, 1]])) < RBF(1e-10) assert norm(mon[1] - matrix(CBF, [[1, pi * (1 - 2 * i)], [0, 1]])) < RBF(1e-10) mon = monodromy_matrices(dop, QQbar(i)) # mon[0] <--> i assert norm(mon[0] - matrix(CBF, [[1, 0], [2 * pi * i, 1]])) < RBF(1e-10) assert norm(mon[1] - matrix(CBF, [[1, 0], [-2 * pi * i, 1]])) < RBF(1e-10) mon = monodromy_matrices(dop, QQbar(i), sing=[QQbar(i)]) assert len(mon) == 1 assert norm(mon[0] - matrix(CBF, [[1, 0], [2 * pi * i, 1]])) < RBF(1e-10) mon = monodromy_matrices(dop, QQbar(i), sing=[QQbar(-i)]) assert len(mon) == 1 assert norm(mon[0] - matrix(CBF, [[1, 0], [-2 * pi * i, 1]])) < RBF(1e-10) mon = monodromy_matrices(dop, QQbar(-i), sing=[QQbar(i)]) assert len(mon) == 1 assert norm(mon[0] - matrix(CBF, [[1, 0], [-2 * pi * i, 1]])) < RBF(1e-10) mon = monodromy_matrices(dop, QQbar(i), sing=[]) assert mon == [] dop = (x**2 + 1) * (x**2 - 1) * Dx**2 + 1 mon = monodromy_matrices(dop, QQ(0), sing=[QQ(1), QQbar(i)]) m0 = dop.numerical_transition_matrix([0, i + 1, 2 * i, i - 1, 0]) assert norm(m0 - mon[0]) < RBF(1e-10) m1 = dop.numerical_transition_matrix([0, 1 - i, 2, 1 + i, 0]) assert norm(m1 - mon[1]) < RBF(1e-10) dop = x * (x - 3) * (x - 4) * (x**2 - 6 * x + 10) * Dx**2 - 1 mon = monodromy_matrices(dop, QQ(-1)) m0 = dop.numerical_transition_matrix([-1, -i, 1, i, -1]) assert norm(m0 - mon[0]) < RBF(1e-10) m1 = dop.numerical_transition_matrix( [-1, i / 2, 3 - i / 2, 3 + h, 3 + i / 2, i / 2, -1]) assert norm(m1 - mon[1]) < RBF(1e-10) m2 = dop.numerical_transition_matrix( [-1, i / 2, 3 + i / 2, 4 - i / 2, 4 + h, 4 + i / 2, i / 2, -1]) assert norm(m2 - mon[2]) < RBF(1e-10) m3 = dop.numerical_transition_matrix([-1, 3 + i + h, 3 + 2 * i, -1]) assert norm(m3 - mon[3]) < RBF(1e-10) m4 = dop.numerical_transition_matrix( [-1, 3 - 2 * i, 3 - i + h, 3 - i / 2, -1]) assert norm(m4 - mon[4]) < RBF(1e-10) dop = (x - i)**2 * (x + i) * Dx - 1 mon = monodromy_matrices(dop, 0) assert norm(mon[0] + i) < RBF(1e-10) assert norm(mon[1] - i) < RBF(1e-10) dop = (x - i)**2 * (x + i) * Dx**2 - 1 mon = monodromy_matrices(dop, 0) m0 = dop.numerical_transition_matrix([0, i + 1, 2 * i, i - 1, 0]) assert norm(m0 - mon[0]) < RBF(1e-10) m1 = dop.numerical_transition_matrix([0, -i - 1, -2 * i, -i + 1, 0]) assert norm(m1 - mon[1]) < RBF(1e-10)
def _monodromy_matrices(dop, base, eps=1e-16, sing=None): r""" EXAMPLES:: sage: from ore_algebra import * sage: from ore_algebra.analytic.monodromy import _monodromy_matrices sage: Dops, x, Dx = DifferentialOperators() sage: rat = 1/(x^2-1) sage: dop = (rat*Dx - rat.derivative()).lclm(Dx*x*Dx) sage: [rec.point for rec in _monodromy_matrices(dop, 0) if not rec.is_scalar] [0] TESTS:: sage: from ore_algebra.examples import fcc sage: mon = list(_monodromy_matrices(fcc.dop5, -1, 2**(-2**7))) # long time (2.3 s) sage: [rec.monodromy[0][0] for rec in mon if rec.point == -5/3] # long time [[1.01088578589319884254557667137848...]] Thanks to Alexandre Goyer for this example:: sage: L1 = ((x^5 - x^4 + x^3)*Dx^3 + (27/8*x^4 - 25/9*x^3 + 8*x^2)*Dx^2 ....: + (37/24*x^3 - 25/9*x^2 + 14*x)*Dx - 2*x^2 - 3/4*x + 4) sage: L2 = ((x^5 - 9/4*x^4 + x^3)*Dx^3 + (11/6*x^4 - 31/4*x^3 + 7*x^2)*Dx^2 ....: + (7/30*x^3 - 101/20*x^2 + 10*x)*Dx + 4/5*x^2 + 5/6*x + 2) sage: L = L1*L2 sage: L = L.parent()(L.annihilator_of_composition(x+1)) sage: mon = list(_monodromy_matrices(L, 0, eps=1e-30)) # long time (1.3-1.7 s) sage: mon[-1][0], mon[-1][1][0][0] # long time (0.6403882032022075?, [1.15462187280628880820271...] + [-0.018967673022432256251718...]*I) """ dop = DifferentialOperator(dop) base = QQbar.coerce(base) eps = RBF(eps) if sing is None: sing = dop._singularities(QQbar) else: sing = [QQbar.coerce(s) for s in sing] todo = {x: TodoItem(x, dop, want_self=True, want_conj=False) for x in sing} base = todo.setdefault(base, TodoItem(base, dop)) if not base.point().is_regular(): raise ValueError("irregular singular base point") # If the coefficients are rational, reduce to handling singularities in the # same half-plane as the base point, and share some computations between # Galois conjugates. need_conjugates = False crit_cache = None if all(c in QQ for pol in dop for c in pol): need_conjugates = _merge_conjugate_singularities(dop, sing, base, todo) # TODO: do something like that even over number fields? # XXX this is actually a bit costly: do it only after checking that the # monodromy is not scalar? # XXX keep the cache from one run to the next when increasing prec? crit_cache = {} Scalars = ComplexBallField(utilities.prec_from_eps(eps)) id_mat = matrix.identity_matrix(Scalars, dop.order()) def matprod(elts): return prod(reversed(elts), id_mat) for key, todoitem in list(todo.items()): point = todoitem.point() # We could call _local_monodromy_loop() if point is irregular, but # delaying it may allow us to start returning results earlier. if point.is_regular(): if crit_cache is None or point.algdeg() == 1: crit = _critical_monomials(dop.shift(point)) emb = point.value.parent().hom(Scalars) else: mpol = point.value.minpoly() try: NF, crit = crit_cache[mpol] except KeyError: NF = point.value.parent() crit = _critical_monomials(dop.shift(point)) # Only store the critical monomials for reusing when all # local exponents are rational. We need to restrict to this # case because we do not have the technology in place to # follow algebraic exponents along the embedding of NF in ℂ. # (They are represented as elements of "new" number fields # given by as_embedded_number_field_element(), even when # they actually lie in NF itself as opposed to a further # algebraic extension. XXX: Ideally, LocalBasisMapper should # give us access to the tower of extensions in which the # exponents "naturally" live.) if all(sol.leftmost.parent() is QQ for sol in crit): crit_cache[mpol] = NF, crit emb = NF.hom([Scalars(point.value.parent().gen())], check=False) mon, scalar = _formal_monodromy_from_critical_monomials(crit, emb) if scalar: # No need to compute the connection matrices then! # XXX When we do need them, though, it would be better to get # the formal monodromy as a byproduct of their computation. if todoitem.want_self: yield LocalMonodromyData(key, mon, True) if todoitem.want_conj: conj = key.conjugate() logger.info( "Computing local monodromy around %s by " "complex conjugation", conj) conj_mat = ~mon.conjugate() yield LocalMonodromyData(conj, conj_mat, True) if todoitem is not base: del todo[key] continue todoitem.local_monodromy = [mon] todoitem.polygon = [point] if need_conjugates: base_conj_mat = dop.numerical_transition_matrix( [base.point(), base.point().conjugate()], eps, assume_analytic=True) def conjugate_monodromy(mat): return ~base_conj_mat * ~mat.conjugate() * base_conj_mat tree = _spanning_tree(base, todo.values()) def dfs(x, path, path_mat): logger.info("Computing local monodromy around %s via %s", x, path) local_mat = matprod(x.local_monodromy) based_mat = (~path_mat) * local_mat * path_mat if x.want_self: yield LocalMonodromyData(x.alg, based_mat, False) if x.want_conj: conj = x.alg.conjugate() logger.info( "Computing local monodromy around %s by complex " "conjugation", conj) conj_mat = conjugate_monodromy(based_mat) yield LocalMonodromyData(conj, conj_mat, False) x.done = True for y in tree.neighbors(x): if y.done: continue if y.local_monodromy is None: y.polygon, y.local_monodromy = _local_monodromy_loop( dop, y.point(), eps) new_path_mat = _extend_path_mat(dop, path_mat, x, y, eps, matprod) yield from dfs(y, path + [y], new_path_mat) yield from dfs(base, [base], id_mat)
def _monodromy_matrices(dop, base, eps=1e-16, sing=None): r""" EXAMPLES:: sage: from ore_algebra import * sage: from ore_algebra.analytic.monodromy import _monodromy_matrices sage: Dops, x, Dx = DifferentialOperators() sage: rat = 1/(x^2-1) sage: dop = (rat*Dx - rat.derivative()).lclm(Dx*x*Dx) sage: [rec.point for rec in _monodromy_matrices(dop, 0) if not rec.is_scalar] [0] TESTS:: sage: from ore_algebra.examples import fcc sage: mon = list(_monodromy_matrices(fcc.dop5, -1, 2**(-2**7))) # long time (2.3 s) sage: [rec.monodromy[0][0] for rec in mon if rec.point == -5/3] # long time [[1.01088578589319884254557667137848...]] """ dop = DifferentialOperator(dop) base = QQbar.coerce(base) eps = RBF(eps) if sing is None: sing = dop._singularities(QQbar) else: sing = [QQbar.coerce(s) for s in sing] todo = {x: TodoItem(x, dop, want_self=True, want_conj=False) for x in sing} base = todo.setdefault(base, TodoItem(base, dop)) if not base.point().is_regular(): raise ValueError("irregular singular base point") # If the coefficients are rational, reduce to handling singularities in the # same half-plane as the base point, and share some computations between # Galois conjugates. need_conjugates = False crit_cache = None if all(c in QQ for pol in dop for c in pol): need_conjugates = _merge_conjugate_singularities(dop, sing, base, todo) # TODO: do something like that even over number fields? # XXX this is actually a bit costly: do it only after checking that the # monodromy is not scalar? # XXX keep the cache from one run to the next when increasing prec? crit_cache = {} Scalars = ComplexBallField(utilities.prec_from_eps(eps)) id_mat = matrix.identity_matrix(Scalars, dop.order()) def matprod(elts): return prod(reversed(elts), id_mat) for key, todoitem in list(todo.items()): point = todoitem.point() # We could call _local_monodromy_loop() if point is irregular, but # delaying it may allow us to start returning results earlier. if point.is_regular(): if crit_cache is None or point.algdeg() == 1: crit = _critical_monomials(dop.shift(point)) emb = point.value.parent().hom(Scalars) else: mpol = point.value.minpoly() try: NF, crit = crit_cache[mpol] except KeyError: NF = point.value.parent() crit = _critical_monomials(dop.shift(point)) crit_cache[mpol] = NF, crit emb = NF.hom([Scalars(point.value.parent().gen())], check=False) mon, scalar = _formal_monodromy_from_critical_monomials(crit, emb) if scalar: # No need to compute the connection matrices then! # XXX When we do need them, though, it would be better to get # the formal monodromy as a byproduct of their computation. if todoitem.want_self: yield LocalMonodromyData(key, mon, True) if todoitem.want_conj: conj = key.conjugate() logger.info( "Computing local monodromy around %s by " "complex conjugation", conj) conj_mat = ~mon.conjugate() yield LocalMonodromyData(conj, conj_mat, True) if todoitem is not base: del todo[key] continue todoitem.local_monodromy = [mon] todoitem.polygon = [point] if need_conjugates: base_conj_mat = dop.numerical_transition_matrix( [base.point(), base.point().conjugate()], eps, assume_analytic=True) def conjugate_monodromy(mat): return ~base_conj_mat * ~mat.conjugate() * base_conj_mat tree = _spanning_tree(base, todo.values()) def dfs(x, path, path_mat): logger.info("Computing local monodromy around %s via %s", x, path) local_mat = matprod(x.local_monodromy) based_mat = (~path_mat) * local_mat * path_mat if x.want_self: yield LocalMonodromyData(x.alg, based_mat, False) if x.want_conj: conj = x.alg.conjugate() logger.info( "Computing local monodromy around %s by complex " "conjugation", conj) conj_mat = conjugate_monodromy(based_mat) yield LocalMonodromyData(conj, conj_mat, False) x.done = True for y in tree.neighbors(x): if y.done: continue if y.local_monodromy is None: y.polygon, y.local_monodromy = _local_monodromy_loop( dop, y.point(), eps) new_path_mat = _extend_path_mat(dop, path_mat, x, y, eps, matprod) yield from dfs(y, path + [y], new_path_mat) yield from dfs(base, [base], id_mat)
def _monodromy_matrices(dop, base, eps=1e-16, sing=None): r""" EXAMPLES:: sage: from ore_algebra import * sage: from ore_algebra.analytic.monodromy import _monodromy_matrices sage: Dops, x, Dx = DifferentialOperators() sage: rat = 1/(x^2-1) sage: dop = (rat*Dx - rat.derivative()).lclm(Dx*x*Dx) sage: [rec.point for rec in _monodromy_matrices(dop, 0) if not rec.is_scalar] [0] """ dop = DifferentialOperator(dop) base = QQbar.coerce(base) eps = RBF(eps) if sing is None: sing = dop._singularities(QQbar) else: sing = [QQbar.coerce(s) for s in sing] todo = { x: PointWithMonodromyData(x, dop, want_self=True, want_conj=False) for x in sing } base = todo.setdefault(base, PointWithMonodromyData(base, dop)) if not base.is_regular(): raise ValueError("irregular singular base point") # If the coefficients are rational, reduce to handling singularities in the # same half-plane as the base point. need_conjugates = _try_merge_conjugate_singularities(dop, sing, base, todo) Scalars = ComplexBallField(utilities.prec_from_eps(eps)) id_mat = matrix.identity_matrix(Scalars, dop.order()) def matprod(elts): return prod(reversed(elts), id_mat) for key, point in list(todo.items()): # We could call _local_monodromy_loop() if point is irregular, but # delaying it may allow us to start returning results earlier. if point.is_regular(): mon, scalar = _formal_monodromy_naive(dop.shift(point), Scalars) if scalar: # No need to compute the connection matrices then! # XXX When we do need them, though, it would be better to get # the formal monodromy as a byproduct of their computation. if point.want_self: yield LocalMonodromyData(QQbar(point), mon, True) if point.want_conj: conj = point.conjugate() logger.info( "Computing local monodromy around %s by " "complex conjugation", conj) conj_mat = ~mon.conjugate() yield LocalMonodromyData(QQbar(conj), conj_mat, True) if point is not base: del todo[key] continue point.local_monodromy = [mon] point.polygon = [point] if need_conjugates: base_conj_mat = dop.numerical_transition_matrix( [base, base.conjugate()], eps, assume_analytic=True) def conjugate_monodromy(mat): return ~base_conj_mat * ~mat.conjugate() * base_conj_mat tree = _spanning_tree(base, todo.values()) def dfs(x, path, path_mat): logger.info("Computing local monodromy around %s via %s", x, path) local_mat = matprod(x.local_monodromy) based_mat = (~path_mat) * local_mat * path_mat if x.want_self: yield LocalMonodromyData(QQbar(x), based_mat, False) if x.want_conj: conj = x.conjugate() logger.info( "Computing local monodromy around %s by complex " "conjugation", conj) conj_mat = conjugate_monodromy(based_mat) yield LocalMonodromyData(QQbar(x), conj_mat, False) x.done = True for y in tree.neighbors(x): if y.done: continue if y.local_monodromy is None: y.polygon, y.local_monodromy = _local_monodromy_loop( dop, y, eps) new_path_mat = _extend_path_mat(dop, path_mat, x, y, eps, matprod) yield from dfs(y, path + [y], new_path_mat) yield from dfs(base, [base], id_mat)