Exemple #1
0
 def _sollya_annotate(self, center, rad, polys):
     import sagesollya as sollya
     logger = logging.getLogger(__name__ + ".sollya")
     logger.info("calling annotatefunction() on %s derivatives", len(polys))
     center = QQ(center)
     sollya_fun = self._sollya_object
     # sollya keeps all annotations, let's not bother with selecting
     # the best one
     for ord, pol0 in enumerate(polys):
         pol = ZZ(ord).factorial() * pol0
         sollya_pol = sum(
             [c.center() * sollya.x**k for k, c in enumerate(pol)])
         dom = RIF(center - rad,
                   center + rad)  # XXX: dangerous when inexact
         err_pol = pol.map_coefficients(lambda c: c - c.squash())
         err = RIF(err_pol(RBF.zero().add_error(rad)))
         with sollya.settings(display=sollya.dyadic):
             logger = logging.getLogger(__name__ + ".sollya.annotate")
             logger.debug("annotatefunction(%s, %s, %s, %s, %s);",
                          sollya_fun, sollya_pol, sollya.SollyaObject(dom),
                          sollya.SollyaObject(err),
                          sollya.SollyaObject(center))
         sollya.annotatefunction(sollya_fun, sollya_pol, dom, err, center)
         sollya_fun = sollya.diff(sollya_fun)
     logger.info("...done")
Exemple #2
0
 def _update_approx(self, center, rad, prec, derivatives):
     ini, path = self._path_to(center, prec)
     eps = RBF.one() >> prec
     # keep="all" won't do anything until _path_to returns better paths
     ctx = ancont.Context(self.dop, path, eps, keep="all")
     pairs = ancont.analytic_continuation(ctx, ini=ini)
     for (vert, val) in pairs:
         known = self._inivecs.get(vert)
         if known is None or known[0].accuracy() < val[0][0].accuracy():
             self._inivecs[vert] = [c[0] for c in val]
     logger.info(
         "computing new polynomial approximations: "
         "ini=%s, path=%s, rad=%s, eps=%s, ord=%s", ini, path, rad, eps,
         derivatives)
     polys = polapprox.doit(self.dop,
                            ini=ini,
                            path=path,
                            rad=rad,
                            eps=eps,
                            derivatives=derivatives,
                            x_is_real=True,
                            economization=polapprox.chebyshev_economization)
     logger.info("...done")
     approx = self._polys.get(center, [])
     new_approx = []
     for ord, pol in enumerate(polys):
         if ord >= len(approx) or approx[ord].prec < prec:
             new_approx.append(RealPolApprox(pol, prec))
         else:
             new_approx.append(approx[ord])
     self._update_approx_hook(center, rad, polys)
     self._polys[center] = new_approx
     return polys
Exemple #3
0
 def accuracy(self):
     infinity = RBF.maximal_accuracy()
     if self.universe.is_exact():
         return infinity
     elif isinstance(self.universe, (RealBallField, ComplexBallField)):
         return min(
             infinity,
             *(x.accuracy() for val in self.shift.values() for x in val))
     else:
         raise ValueError
Exemple #4
0
 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
Exemple #5
0
 def _disk(self, pt):
     assert pt.is_real()
     # Since approximation disks satisfy 2·rad ≤ dist(center, sing), any
     # approximation disk containing pt must have rad ≤ dist(pt, sing)
     max_rad = pt.dist_to_sing().min(self.max_rad)
     # What we want is the largest such disk containing pt
     expo = ZZ(max_rad.log(2).upper().ceil()) - 1  # rad = 2^expo
     logger.log(logging.DEBUG - 2, "max_rad = %s, expo = %s", max_rad, expo)
     while True:
         approx_pt = pt.approx_abs_real(-expo)
         mantissa = (approx_pt.squash() >> expo).floor()
         if ZZ(mantissa) % 2 == 0:
             mantissa += 1
         center = mantissa << expo
         dist = Point(center, pt.dop).dist_to_sing()
         rad = RBF.one() << expo
         logger.log(
             logging.DEBUG - 2,
             "candidate disk: approx_pt = %s, mantissa = %s, "
             "center = %s, dist = %s, rad = %s", approx_pt, mantissa,
             center, dist, rad)
         if safe_ge(dist >> 1, rad):
             break
         expo -= 1
     logger.debug("disk for %s: center=%s, rad=%s", pt, center, rad)
     # pt may be a ball with nonzero radius: check that it is contained in
     # our candidate disk
     log = RBF.zero() if 0 in approx_pt else approx_pt.abs().log(2)
     F = RealBallField(ZZ((expo - log).max(0).upper().ceil()) + 10)
     dist_to_center = (F(approx_pt) - F(center)).abs()
     if not safe_le(dist_to_center, rad):
         assert not safe_gt((approx_pt.squash() - center).squash(), rad)
         logger.info("check that |%s - %s| < %s failed", approx_pt, center,
                     rad)
         return None, None
     # exactify center so that subsequent computations are not limited by the
     # precision of its parent
     center = QQ(center)
     return center, rad
Exemple #6
0
    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
Exemple #8
0
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)
Exemple #9
0
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)
Exemple #10
0
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)
Exemple #11
0
class DFiniteFunction(object):
    r"""
    At the moment, this class just provides a simple caching mechanism for
    repeated evaluations of a D-Finite function on the real line. It may
    evolve to support evaluations on the complex plane, branch cuts, ring
    operations on D-Finite functions, and more. Do not expect any API stability.

    TESTS::

        sage: from ore_algebra import *
        sage: from ore_algebra.analytic.function import DFiniteFunction
        sage: from ore_algebra.analytic.path import Point
        sage: from sage.rings.infinity import AnInfinity

        sage: Dops, x, Dx = DifferentialOperators()

        sage: f = DFiniteFunction(Dx - 1, [1], max_rad=7/8)
        sage: f._disk(Point(pi, dop=Dx-1))
        (7/2, 0.5000000000000000)

        sage: f = DFiniteFunction(Dx^2 - x,
        ....:         [1/(gamma(2/3)*3^(2/3)), -1/(gamma(1/3)*3^(1/3))],
        ....:         name='my_Ai')
        sage: f.plot((-5,5))
        Graphics object consisting of 1 graphics primitive
        sage: f._known_bound(RBF(RIF(1/3, 2/3)), post_transform=Dops.one())
        [0.2...]
        sage: f._known_bound(RBF(RIF(-1, 5.99)), post_transform=Dops.one())
        [+/- ...]
        sage: f._known_bound(RBF(RIF(-1, 6.01)), Dops.one())
        [+/- inf]

        sage: f = DFiniteFunction((x^2 + 1)*Dx^2 + 2*x*Dx, [0, 1])
        sage: f.plot((0, 4))
        Graphics object consisting of 1 graphics primitive
        sage: f._known_bound(RBF(RIF(1,4)), Dops.one())
        [1e+0 +/- 0.3...]
        sage: f.approx(1, post_transform=Dx), f.approx(2, post_transform=Dx)
        ([0.5000000000000...], [0.2000000000000...])
        sage: f._known_bound(RBF(RIF(1.1, 2.9)), post_transform=Dx)
        [+/- 0.4...]
        sage: f._known_bound(RBF(AnInfinity()), Dops.one())
        [+/- inf]

        sage: f = DFiniteFunction((x + 1)*Dx^2 + 2*x*Dx, [0, 1])
        sage: f._known_bound(RBF(RIF(1,10)),Dops.one())
        nan
        sage: f._known_bound(RBF(AnInfinity()), Dops.one())
        nan

        sage: f = DFiniteFunction(Dx^2 + 2*x*Dx, [1, -2/sqrt(pi)], name='my_erfc')
        sage: f._known_bound(RBF(RIF(-1/2,1/2)), post_transform=Dx^2)
        [+/- inf]
        sage: _ = f.approx(1/2, post_transform=Dx^2)
        sage: _ = f.approx(-1/2, post_transform=Dx^2)
        sage: f._known_bound(RBF(RIF(-1/2,1/2)), post_transform=Dx^2)
        [+/- 1.5...]
    """

    # Stupid, but simple and deterministic caching strategy:
    #
    # - To any center c = m·2^k with k ∈ ℤ and m *odd*, we associate the disk of
    #   radius 2^k. Any two disks with the same k have at most one point in
    #   common.
    #
    # - Thus, in the case of an equation with at least one finite singular
    #   point, there is a unique largest disk of the previous collection that
    #   contains any given ordinary x ∈ ℝ \ ℤ·2^(-∞) while staying “far enough”
    #   from the singularities.
    #
    # - When asked to evaluate f at x, we actually compute and store a
    #   polynomial approximation on (the real trace of) the corresponding disk
    #   and/or a vector of initial conditions at its center, which can then be
    #   reused for subsequent evaluations.
    #
    # - We may additionally want to allow disks (of any radius?) centered at
    #   real regular singular points, and perhaps, as a special case, at 0.
    #   These would be used when possible, and one would revert to the other
    #   family otherwise.

    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 __repr__(self):
        return self.name

    @cached_method
    def _is_everywhere_defined(self):
        return not any(
            rt.imag().contains_zero()
            for rt, mult in self.dop.leading_coefficient().roots(CIF))

    def _disk(self, pt):
        assert pt.is_real()
        # Since approximation disks satisfy 2·rad ≤ dist(center, sing), any
        # approximation disk containing pt must have rad ≤ dist(pt, sing)
        max_rad = pt.dist_to_sing().min(self.max_rad)
        # What we want is the largest such disk containing pt
        expo = ZZ(max_rad.log(2).upper().ceil()) - 1  # rad = 2^expo
        logger.log(logging.DEBUG - 2, "max_rad = %s, expo = %s", max_rad, expo)
        while True:
            approx_pt = pt.approx_abs_real(-expo)
            mantissa = (approx_pt.squash() >> expo).floor()
            if ZZ(mantissa) % 2 == 0:
                mantissa += 1
            center = mantissa << expo
            dist = Point(center, pt.dop).dist_to_sing()
            rad = RBF.one() << expo
            logger.log(
                logging.DEBUG - 2,
                "candidate disk: approx_pt = %s, mantissa = %s, "
                "center = %s, dist = %s, rad = %s", approx_pt, mantissa,
                center, dist, rad)
            if safe_ge(dist >> 1, rad):
                break
            expo -= 1
        logger.debug("disk for %s: center=%s, rad=%s", pt, center, rad)
        # pt may be a ball with nonzero radius: check that it is contained in
        # our candidate disk
        log = RBF.zero() if 0 in approx_pt else approx_pt.abs().log(2)
        F = RealBallField(ZZ((expo - log).max(0).upper().ceil()) + 10)
        dist_to_center = (F(approx_pt) - F(center)).abs()
        if not safe_le(dist_to_center, rad):
            assert not safe_gt((approx_pt.squash() - center).squash(), rad)
            logger.info("check that |%s - %s| < %s failed", approx_pt, center,
                        rad)
            return None, None
        # exactify center so that subsequent computations are not limited by the
        # precision of its parent
        center = QQ(center)
        return center, rad

    def _rad(self, center):
        return RBF.one() << QQ(center).valuation(2)

    def _path_to(self, dest, prec=None):
        r"""
        Find a path from a point with known "initial" values to pt
        """
        # TODO:
        # - attempt to start as close as possible to the destination
        #   [and perhaps add logic to change for a starting point with exact
        #   initial values if loosing too much precision]
        # - return a path passing through "interesting" points (and cache the
        #   associated initial vectors)
        start, ini = self.ini.items()[0]
        return ini, [start, dest]

    # Having the update (rather than the full test-and-update) logic in a
    # separate method is convenient to override it in subclasses.
    def _update_approx(self, center, rad, prec, derivatives):
        ini, path = self._path_to(center, prec)
        eps = RBF.one() >> prec
        # keep="all" won't do anything until _path_to returns better paths
        ctx = ancont.Context(self.dop, path, eps, keep="all")
        pairs = ancont.analytic_continuation(ctx, ini=ini)
        for (vert, val) in pairs:
            known = self._inivecs.get(vert)
            if known is None or known[0].accuracy() < val[0][0].accuracy():
                self._inivecs[vert] = [c[0] for c in val]
        logger.info(
            "computing new polynomial approximations: "
            "ini=%s, path=%s, rad=%s, eps=%s, ord=%s", ini, path, rad, eps,
            derivatives)
        polys = polapprox.doit(self.dop,
                               ini=ini,
                               path=path,
                               rad=rad,
                               eps=eps,
                               derivatives=derivatives,
                               x_is_real=True,
                               economization=polapprox.chebyshev_economization)
        logger.info("...done")
        approx = self._polys.get(center, [])
        new_approx = []
        for ord, pol in enumerate(polys):
            if ord >= len(approx) or approx[ord].prec < prec:
                new_approx.append(RealPolApprox(pol, prec))
            else:
                new_approx.append(approx[ord])
        self._update_approx_hook(center, rad, polys)
        self._polys[center] = new_approx
        return polys

    def _sollya_annotate(self, center, rad, polys):
        import sagesollya as sollya
        logger = logging.getLogger(__name__ + ".sollya")
        logger.info("calling annotatefunction() on %s derivatives", len(polys))
        center = QQ(center)
        sollya_fun = self._sollya_object
        # sollya keeps all annotations, let's not bother with selecting
        # the best one
        for ord, pol0 in enumerate(polys):
            pol = ZZ(ord).factorial() * pol0
            sollya_pol = sum(
                [c.center() * sollya.x**k for k, c in enumerate(pol)])
            dom = RIF(center - rad,
                      center + rad)  # XXX: dangerous when inexact
            err_pol = pol.map_coefficients(lambda c: c - c.squash())
            err = RIF(err_pol(RBF.zero().add_error(rad)))
            with sollya.settings(display=sollya.dyadic):
                logger = logging.getLogger(__name__ + ".sollya.annotate")
                logger.debug("annotatefunction(%s, %s, %s, %s, %s);",
                             sollya_fun, sollya_pol, sollya.SollyaObject(dom),
                             sollya.SollyaObject(err),
                             sollya.SollyaObject(center))
            sollya.annotatefunction(sollya_fun, sollya_pol, dom, err, center)
            sollya_fun = sollya.diff(sollya_fun)
        logger.info("...done")

    def _known_bound(self, iv, post_transform):
        post_transform = normalize_post_transform(self.dop, post_transform)
        Balls = iv.parent()
        Ivs = RealIntervalField(Balls.precision())
        mid = [
            c for c in self._polys.keys()
            if Balls(c).add_error(self._rad(c)).overlaps(iv)
        ]
        mid.sort()
        rad = [self._rad(c) for c in mid]
        crude_bound = (Balls(AnInfinity())
                       if self._is_everywhere_defined() else Balls('nan'))
        if len(mid) < 1 or not mid[0] - rad[0] <= iv <= mid[-1] + rad[-1]:
            return crude_bound
        if not all(
                safe_eq(mid[i] + rad[i], mid[i + 1] - rad[i + 1])
                for i in range(len(mid) - 1)):
            return crude_bound
        bound = None
        for c, r in zip(mid, rad):
            if len(self._polys[c]) <= post_transform.order():
                return crude_bound
            polys = [a.pol for a in self._polys[c]]
            dom = Balls(Ivs(Balls(c).add_error(r)).intersection(Ivs(iv)))
            reduced_dom = dom - c
            img = sum(
                ZZ(j).factorial() * coeff(dom) * polys[j](reduced_dom)
                for j, coeff in enumerate(post_transform))
            bound = img if bound is None else bound.union(img)
        return bound

    def approx(self, pt, prec=None, post_transform=None):
        r"""
        TESTS::

            sage: from ore_algebra import *
            sage: from ore_algebra.analytic.function import DFiniteFunction
            sage: DiffOps, x, Dx = DifferentialOperators()

            sage: h = DFiniteFunction(Dx^3-1, [0, 0, 1])
            sage: h.approx(0, post_transform=Dx^2)
            [2.0000000000000...]

            sage: f = DFiniteFunction((x^2 + 1)*Dx^2 + 2*x*Dx, [0, 1], max_prec=20)
            sage: f.approx(1/3, prec=10)
            [0.32...]
            sage: f.approx(1/3, prec=40)
            [0.321750554396...]
            sage: f.approx(1/3, prec=10, post_transform=Dx)
            [0.9...]
            sage: f.approx(1/3, prec=40, post_transform=Dx)
            [0.900000000000...]
            sage: f.approx(1/3, prec=10, post_transform=Dx^2)
            [-0.54...]
            sage: f.approx(1/3, prec=40, post_transform=Dx^2)
            [-0.540000000000...]

        """
        pt = Point(pt, self.dop)
        if prec is None:
            prec = _guess_prec(pt)
        if post_transform is None:
            post_transform = self.dop.parent().one()
        derivatives = min(post_transform.order() + 1, self._max_derivatives)
        post_transform = normalize_post_transform(self.dop, post_transform)
        if prec >= self.max_prec or not pt.is_real():
            logger.info(
                "performing high-prec evaluation "
                "(pt=%s, prec=%s, post_transform=%s)", pt, prec,
                post_transform)
            ini, path = self._path_to(pt)
            eps = RBF.one() >> prec
            return self.dop.numerical_solution(ini,
                                               path,
                                               eps,
                                               post_transform=post_transform)
        center, rad = self._disk(pt)
        if center is None:
            # raise NotImplementedError
            logger.info("falling back on generic evaluator")
            ini, path = self._path_to(pt)
            eps = RBF.one() >> prec
            return self.dop.numerical_solution(ini,
                                               path,
                                               eps,
                                               post_transform=post_transform)
        approx = self._polys.get(center, [])
        Balls = RealBallField(prec)
        # due to the way the polynomials are recomputed, the precisions attached
        # to the successive derivatives are nonincreasing
        if (len(approx) < derivatives or approx[derivatives - 1].prec < prec):
            polys = self._update_approx(center, rad, prec, derivatives)
        else:
            polys = [a.pol for a in approx]
        bpt = Balls(pt.value)
        reduced_pt = bpt - Balls(center)
        val = sum(
            ZZ(j).factorial() * coeff(bpt) * polys[j](reduced_pt)
            for j, coeff in enumerate(post_transform))
        return val

    def __call__(self, x, prec=None):
        return self.approx(x, prec=prec)

    def plot(self, xrange, **options):
        r"""
        Plot this function.

        The plot is intended to give an idea of the accuracy of the evaluations
        that led to it. However, it may not be a rigorous enclosure of the graph
        of the function.

        EXAMPLES::

            sage: from ore_algebra import *
            sage: from ore_algebra.analytic.function import DFiniteFunction
            sage: DiffOps, x, Dx = DifferentialOperators()

            sage: f = DFiniteFunction(Dx^2 - x,
            ....:         [1/(gamma(2/3)*3^(2/3)), -1/(gamma(1/3)*3^(1/3))])
            sage: plot(f, (-10, 5), color='black')
            Graphics object consisting of 1 graphics primitive
        """
        mids = generate_plot_points(lambda x: self.approx(x, 20).mid(),
                                    xrange,
                                    plot_points=200)
        ivs = [(x, self.approx(x, 20)) for x, _ in mids]
        bounds = [(x, y.upper()) for x, y in ivs]
        bounds += [(x, y.lower()) for x, y in reversed(ivs)]
        options.setdefault('aspect_ratio', 'automatic')
        g = plot.polygon(bounds, thickness=1, **options)
        return g

    def plot_known(self):
        r"""
        TESTS::

            sage: from ore_algebra import *
            sage: from ore_algebra.analytic.function import DFiniteFunction
            sage: DiffOps, x, Dx = DifferentialOperators()

            sage: f = DFiniteFunction((x^2 + 1)*Dx^2 + 2*x*Dx, [0, 1])
            sage: f(-10, 100) # long time
            [-1.4711276743037345918528755717...]
            sage: f.approx(5, post_transform=Dx) # long time
            [0.038461538461538...]
            sage: f.plot_known() # long time
            Graphics object consisting of ... graphics primitives
        """
        g = plot.Graphics()
        for center, polys in self._polys.iteritems():
            center, rad = self._disk(Point(center, self.dop))
            xrange = (center - rad).mid(), (center + rad).mid()
            for i, a in enumerate(polys):
                # color palette copied from sage.plot.plot.plot
                color = plot.Color((0.66666 + i * 0.61803) % 1,
                                   1,
                                   0.4,
                                   space='hsl')
                Balls = a.pol.base_ring()
                g += plot.plot(lambda x: a.pol(Balls(x)).mid(),
                               xrange,
                               color=color)
                g += plot.text(str(a.prec), (center, a.pol(center).mid()),
                               color=color)
        for point, ini in self._inivecs.iteritems():
            g += plot.point2d((point, 0), size=50)
        return g

    def _sollya_(self):
        r"""
        EXAMPLES::

            sage: import logging; logging.basicConfig(level=logging.INFO)
            sage: logger = logging.getLogger("ore_algebra.analytic.function.sollya")
            sage: logger.setLevel(logging.DEBUG)

            sage: import ore_algebra
            sage: DiffOps, x, Dx = ore_algebra.DifferentialOperators()
            sage: from ore_algebra.analytic.function import DFiniteFunction
            sage: f = DFiniteFunction(Dx - 1, [1])

            sage: import sagesollya as sollya # optional - sollya
            sage: sollya.plot(f, sollya.Interval(0, 1)) # not tested
            ...

        """
        if self._sollya_object is not None:
            return self._sollya_object
        import sagesollya as sollya
        logger = logging.getLogger(__name__ + ".sollya.eval")
        Dx = self.dop.parent().gen()

        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

        wrapper.__name__ = self.name
        self._sollya_object = sollya.sagefunction(wrapper)
        for pt, ini in self.ini.iteritems():
            pt = RIF(pt)
            if pt.is_exact():
                fun = self._sollya_object
                for ord, val in enumerate(ini):
                    try:
                        sollya.annotatefunction(fun, val, pt, RIF.zero())
                    except TypeError:
                        logger.info("annotation failed: D^%s(%s)(%s) = %s",
                                    ord, self.name, pt, val)
                    fun = sollya.diff(fun)
        self._update_approx_hook = self._sollya_annotate
        return self._sollya_object
Exemple #12
0
    def approx(self, pt, prec=None, post_transform=None):
        r"""
        TESTS::

            sage: from ore_algebra import *
            sage: from ore_algebra.analytic.function import DFiniteFunction
            sage: DiffOps, x, Dx = DifferentialOperators()

            sage: h = DFiniteFunction(Dx^3-1, [0, 0, 1])
            sage: h.approx(0, post_transform=Dx^2)
            [2.0000000000000...]

            sage: f = DFiniteFunction((x^2 + 1)*Dx^2 + 2*x*Dx, [0, 1], max_prec=20)
            sage: f.approx(1/3, prec=10)
            [0.32...]
            sage: f.approx(1/3, prec=40)
            [0.321750554396...]
            sage: f.approx(1/3, prec=10, post_transform=Dx)
            [0.9...]
            sage: f.approx(1/3, prec=40, post_transform=Dx)
            [0.900000000000...]
            sage: f.approx(1/3, prec=10, post_transform=Dx^2)
            [-0.54...]
            sage: f.approx(1/3, prec=40, post_transform=Dx^2)
            [-0.540000000000...]

        """
        pt = Point(pt, self.dop)
        if prec is None:
            prec = _guess_prec(pt)
        if post_transform is None:
            post_transform = self.dop.parent().one()
        derivatives = min(post_transform.order() + 1, self._max_derivatives)
        post_transform = normalize_post_transform(self.dop, post_transform)
        if prec >= self.max_prec or not pt.is_real():
            logger.info(
                "performing high-prec evaluation "
                "(pt=%s, prec=%s, post_transform=%s)", pt, prec,
                post_transform)
            ini, path = self._path_to(pt)
            eps = RBF.one() >> prec
            return self.dop.numerical_solution(ini,
                                               path,
                                               eps,
                                               post_transform=post_transform)
        center, rad = self._disk(pt)
        if center is None:
            # raise NotImplementedError
            logger.info("falling back on generic evaluator")
            ini, path = self._path_to(pt)
            eps = RBF.one() >> prec
            return self.dop.numerical_solution(ini,
                                               path,
                                               eps,
                                               post_transform=post_transform)
        approx = self._polys.get(center, [])
        Balls = RealBallField(prec)
        # due to the way the polynomials are recomputed, the precisions attached
        # to the successive derivatives are nonincreasing
        if (len(approx) < derivatives or approx[derivatives - 1].prec < prec):
            polys = self._update_approx(center, rad, prec, derivatives)
        else:
            polys = [a.pol for a in approx]
        bpt = Balls(pt.value)
        reduced_pt = bpt - Balls(center)
        val = sum(
            ZZ(j).factorial() * coeff(bpt) * polys[j](reduced_pt)
            for j, coeff in enumerate(post_transform))
        return val
Exemple #13
0
 def _rad(self, center):
     return RBF.one() << QQ(center).valuation(2)
Exemple #14
0
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)