def algebraic_topological_model_delta_complex(K, base_ring=None):
    r"""
    Algebraic topological model for cell complex ``K``
    with coefficients in the field ``base_ring``.

    This has the same basic functionality as
    :func:`algebraic_topological_model`, but it also works for
    `\Delta`-complexes. For simplicial and cubical complexes it is
    somewhat slower, though.

    INPUT:

    - ``K`` -- a simplicial complex, a cubical complex, or a
      `\Delta`-complex
    - ``base_ring`` -- coefficient ring; must be a field

    OUTPUT: a pair ``(phi, M)`` consisting of

    - chain contraction ``phi``
    - chain complex `M`

    See :func:`algebraic_topological_model` for the main
    documentation. The difference in implementation between the two:
    this uses matrix and vector algebra. The other function does more
    of the computations "by hand" and uses cells (given as simplices
    or cubes) to index various dictionaries. Since the cells in
    `\Delta`-complexes are not as nice, the other function does not
    work for them, while this function relies almost entirely on the
    structure of the associated chain complex.

    EXAMPLES::

        sage: from sage.homology.algebraic_topological_model import algebraic_topological_model_delta_complex as AT_model
        sage: RP2 = simplicial_complexes.RealProjectivePlane()
        sage: phi, M = AT_model(RP2, GF(2))
        sage: M.homology()
        {0: Vector space of dimension 1 over Finite Field of size 2,
         1: Vector space of dimension 1 over Finite Field of size 2,
         2: Vector space of dimension 1 over Finite Field of size 2}
        sage: T = delta_complexes.Torus()
        sage: phi, M = AT_model(T, QQ)
        sage: M.homology()
        {0: Vector space of dimension 1 over Rational Field,
         1: Vector space of dimension 2 over Rational Field,
         2: Vector space of dimension 1 over Rational Field}

    If you want to work with cohomology rather than homology, just
    dualize the outputs of this function::

        sage: M.dual().homology()
        {0: Vector space of dimension 1 over Rational Field,
         1: Vector space of dimension 2 over Rational Field,
         2: Vector space of dimension 1 over Rational Field}
        sage: M.dual().degree_of_differential()
        1
        sage: phi.dual()
        Chain homotopy between:
          Chain complex endomorphism of Chain complex with at most 3 nonzero terms over Rational Field
          and Chain complex morphism:
            From: Chain complex with at most 3 nonzero terms over Rational Field
            To:   Chain complex with at most 3 nonzero terms over Rational Field

    In degree 0, the inclusion of the homology `M` into the chain
    complex `C` sends the homology generator to a single vertex::

        sage: K = delta_complexes.Simplex(2)
        sage: phi, M = AT_model(K, QQ)
        sage: phi.iota().in_degree(0)
        [0]
        [0]
        [1]

    In cohomology, though, one needs the dual of every degree 0 cell
    to detect the degree 0 cohomology generator::

        sage: phi.dual().iota().in_degree(0)
        [1]
        [1]
        [1]

    TESTS::

        sage: T = cubical_complexes.Torus()
        sage: C = T.chain_complex()
        sage: H, M = AT_model(T, QQ)
        sage: C.differential(1) * H.iota().in_degree(1).column(0) == 0
        True
        sage: C.differential(1) * H.iota().in_degree(1).column(1) == 0
        True
        sage: coC = T.chain_complex(cochain=True)
        sage: coC.differential(1) * H.dual().iota().in_degree(1).column(0) == 0
        True
        sage: coC.differential(1) * H.dual().iota().in_degree(1).column(1) == 0
        True
    """
    def conditionally_sparse(m):
        """
        Return a sparse matrix if the characteristic is zero.

        Multiplication of matrices with low density seems to be quicker
        if the matrices are sparse, when over the rationals. Over
        finite fields, dense matrices are faster regardless of
        density.
        """
        if base_ring == QQ:
            return m.sparse_matrix()
        else:
            return m

    if not base_ring.is_field():
        raise ValueError('the coefficient ring must be a field')

    # The following are all dictionaries indexed by dimension.
    # For each n, gens[n] is an ordered list of the n-cells generating the complex M.
    gens = {}
    pi_data = {}
    phi_data = {}
    iota_data = {}

    for n in range(-1, K.dimension()+1):
        gens[n] = []

    C = K.chain_complex(base_ring=base_ring)
    n_cells = []
    pi_cols = []
    iota_cols = {}

    for dim in range(K.dimension()+1):
        # old_cells: cells one dimension lower.
        old_cells = n_cells
        # n_cells: the standard basis for the vector space C.free_module(dim).
        n_cells = C.free_module(dim).gens()
        diff = C.differential(dim)
        # diff is sparse and low density. Dense matrices are faster
        # over finite fields, but for low density matrices, sparse
        # matrices are faster over the rationals.
        if base_ring != QQ:
            diff = diff.dense_matrix()

        rank = len(n_cells)
        old_rank = len(old_cells)

        # Create some matrix spaces to try to speed up matrix creation.
        MS_pi_t = MatrixSpace(base_ring, old_rank, len(gens[dim-1]))

        pi_old = MS_pi_t.matrix(pi_cols).transpose()
        iota_cols_old = iota_cols
        iota_cols = {}
        pi_cols_old = pi_cols
        pi_cols = []
        phi_old = MatrixSpace(base_ring, rank, old_rank, sparse=(base_ring==QQ)).zero()
        phi_old_cols = phi_old.columns()
        phi_old = conditionally_sparse(phi_old)
        to_be_deleted = []

        zero_vector = vector(base_ring, rank)
        pi_nrows = pi_old.nrows()

        for c_idx, c in enumerate(n_cells):
            # c_bar = c - phi(bdry(c)):
            # Avoid a bug in matrix-vector multiplication (trac 19378):
            if not diff:
                c_bar = c
                pi_bdry_c_bar = False
            else:
                if base_ring == QQ:
                    c_bar = c - phi_old * (diff * c)
                    pi_bdry_c_bar = conditionally_sparse(pi_old) * (diff * c_bar)
                else:
                    c_bar = c - phi_old * diff * c
                    pi_bdry_c_bar = conditionally_sparse(pi_old) * diff * c_bar

            # One small typo in the published algorithm: it says
            # "if bdry(c_bar) == 0", but should say
            # "if pi(bdry(c_bar)) == 0".
            if not pi_bdry_c_bar:
                # Append c to list of gens.
                gens[dim].append(c_idx)
                # iota(c) = c_bar
                iota_cols[c_idx] = c_bar
                # pi(c) = c
                pi_cols.append(c)
            else:
                # Take any u in gens so that lambda_i = <u, pi(bdry(c_bar))> != 0.
                # u_idx will be the index of the corresponding cell.
                (u_idx, lambda_i) = pi_bdry_c_bar.leading_item()
                for (u_idx, lambda_i) in iteritems(pi_bdry_c_bar):
                    if u_idx not in to_be_deleted:
                        break
                # This element/column needs to be deleted from gens and
                # iota_old. Do that later.
                to_be_deleted.append(u_idx)
                # pi(c) = 0.
                pi_cols.append(zero_vector)
                for c_j_idx, c_j in enumerate(old_cells):
                    # eta_ij = <u, pi(c_j)>.
                    # That is, eta_ij is the u_idx entry in the vector pi_old * c_j:
                    eta_ij = c_j.dot_product(pi_old.row(u_idx))
                    if eta_ij:
                        # Adjust phi(c_j).
                        phi_old_cols[c_j_idx] += eta_ij * lambda_i**(-1) * c_bar
                        # Adjust pi(c_j).
                        pi_cols_old[c_j_idx] -= eta_ij * lambda_i**(-1) * pi_bdry_c_bar

                # The matrices involved have many zero entries. For
                # such matrices, using sparse matrices is faster over
                # the rationals, slower over finite fields.
                phi_old = matrix(base_ring, phi_old_cols, sparse=(base_ring==QQ)).transpose()
                keep = vector(base_ring, pi_nrows, {i:1 for i in range(pi_nrows)
                                                    if i not in to_be_deleted})
                cols = [v.pairwise_product(keep) for v in pi_cols_old]
                pi_old = MS_pi_t.matrix(cols).transpose()

        # Here cols is a temporary storage for the columns of iota.
        cols = [iota_cols_old[i] for i in sorted(iota_cols_old.keys())]
        for r in sorted(to_be_deleted, reverse=True):
            del cols[r]
            del gens[dim-1][r]
        iota_data[dim-1] = matrix(base_ring, len(gens[dim-1]), old_rank, cols).transpose()
        # keep: rows to keep in pi_cols_old. Start with all
        # columns, then delete those in to_be_deleted.
        keep = sorted(set(range(pi_nrows)).difference(to_be_deleted))
        # Now cols is a temporary storage for columns of pi.
        cols = [v.list_from_positions(keep) for v in pi_cols_old]
        pi_data[dim-1] = matrix(base_ring, old_rank, len(gens[dim-1]), cols).transpose()
        phi_data[dim-1] = phi_old

        V_gens = VectorSpace(base_ring, len(gens[dim]))
        if pi_cols:
            cols = []
            for v in pi_cols:
                cols.append(V_gens(v.list_from_positions(gens[dim])))
            pi_cols = cols

    pi_data[dim] = matrix(base_ring, rank, len(gens[dim]), pi_cols).transpose()
    cols = [iota_cols[i] for i in sorted(iota_cols.keys())]
    iota_data[dim] = matrix(base_ring, len(gens[dim]), rank, cols).transpose()

    # M_data will contain (trivial) matrices defining the differential
    # on M. Keep track of the sizes using "M_rows" and "M_cols", which are
    # just the ranks of consecutive graded pieces of M.
    M_data = {}
    M_rows = 0
    for n in range(K.dimension()+1):
        M_cols = len(gens[n])
        M_data[n] = zero_matrix(base_ring, M_rows, M_cols)
        M_rows = M_cols

    M = ChainComplex(M_data, base_ring=base_ring, degree=-1)

    pi = ChainComplexMorphism(pi_data, C, M)
    iota = ChainComplexMorphism(iota_data, M, C)
    phi = ChainContraction(phi_data, pi, iota)
    return phi, M
Esempio n. 2
0
def algebraic_topological_model_delta_complex(K, base_ring=None):
    r"""
    Algebraic topological model for cell complex ``K``
    with coefficients in the field ``base_ring``.

    This has the same basic functionality as
    :func:`algebraic_topological_model`, but it also works for
    `\Delta`-complexes. For simplicial and cubical complexes it is
    somewhat slower, though.

    INPUT:

    - ``K`` -- a simplicial complex, a cubical complex, or a
      `\Delta`-complex
    - ``base_ring`` -- coefficient ring; must be a field

    OUTPUT: a pair ``(phi, M)`` consisting of

    - chain contraction ``phi``
    - chain complex `M`

    See :func:`algebraic_topological_model` for the main
    documentation. The difference in implementation between the two:
    this uses matrix and vector algebra. The other function does more
    of the computations "by hand" and uses cells (given as simplices
    or cubes) to index various dictionaries. Since the cells in
    `\Delta`-complexes are not as nice, the other function does not
    work for them, while this function relies almost entirely on the
    structure of the associated chain complex.

    EXAMPLES::

        sage: from sage.homology.algebraic_topological_model import algebraic_topological_model_delta_complex as AT_model
        sage: RP2 = simplicial_complexes.RealProjectivePlane()
        sage: phi, M = AT_model(RP2, GF(2))
        sage: M.homology()
        {0: Vector space of dimension 1 over Finite Field of size 2,
         1: Vector space of dimension 1 over Finite Field of size 2,
         2: Vector space of dimension 1 over Finite Field of size 2}
        sage: T = delta_complexes.Torus()
        sage: phi, M = AT_model(T, QQ)
        sage: M.homology()
        {0: Vector space of dimension 1 over Rational Field,
         1: Vector space of dimension 2 over Rational Field,
         2: Vector space of dimension 1 over Rational Field}

    If you want to work with cohomology rather than homology, just
    dualize the outputs of this function::

        sage: M.dual().homology()
        {0: Vector space of dimension 1 over Rational Field,
         1: Vector space of dimension 2 over Rational Field,
         2: Vector space of dimension 1 over Rational Field}
        sage: M.dual().degree_of_differential()
        1
        sage: phi.dual()
        Chain homotopy between:
          Chain complex endomorphism of Chain complex with at most 3 nonzero terms over Rational Field
          and Chain complex morphism:
            From: Chain complex with at most 3 nonzero terms over Rational Field
            To:   Chain complex with at most 3 nonzero terms over Rational Field

    In degree 0, the inclusion of the homology `M` into the chain
    complex `C` sends the homology generator to a single vertex::

        sage: K = delta_complexes.Simplex(2)
        sage: phi, M = AT_model(K, QQ)
        sage: phi.iota().in_degree(0)
        [0]
        [0]
        [1]

    In cohomology, though, one needs the dual of every degree 0 cell
    to detect the degree 0 cohomology generator::

        sage: phi.dual().iota().in_degree(0)
        [1]
        [1]
        [1]

    TESTS::

        sage: T = cubical_complexes.Torus()
        sage: C = T.chain_complex()
        sage: H, M = AT_model(T, QQ)
        sage: C.differential(1) * H.iota().in_degree(1).column(0) == 0
        True
        sage: C.differential(1) * H.iota().in_degree(1).column(1) == 0
        True
        sage: coC = T.chain_complex(cochain=True)
        sage: coC.differential(1) * H.dual().iota().in_degree(1).column(0) == 0
        True
        sage: coC.differential(1) * H.dual().iota().in_degree(1).column(1) == 0
        True
    """
    def conditionally_sparse(m):
        """
        Return a sparse matrix if the characteristic is zero.

        Multiplication of matrices with low density seems to be quicker
        if the matrices are sparse, when over the rationals. Over
        finite fields, dense matrices are faster regardless of
        density.
        """
        if base_ring == QQ:
            return m.sparse_matrix()
        else:
            return m

    if not base_ring.is_field():
        raise ValueError('the coefficient ring must be a field')

    # The following are all dictionaries indexed by dimension.
    # For each n, gens[n] is an ordered list of the n-cells generating the complex M.
    gens = {}
    pi_data = {}
    phi_data = {}
    iota_data = {}

    for n in range(-1, K.dimension()+1):
        gens[n] = []

    C = K.chain_complex(base_ring=base_ring)
    n_cells = []
    pi_cols = []
    iota_cols = {}

    for dim in range(K.dimension()+1):
        # old_cells: cells one dimension lower.
        old_cells = n_cells
        # n_cells: the standard basis for the vector space C.free_module(dim).
        n_cells = C.free_module(dim).gens()
        diff = C.differential(dim)
        # diff is sparse and low density. Dense matrices are faster
        # over finite fields, but for low density matrices, sparse
        # matrices are faster over the rationals.
        if base_ring != QQ:
            diff = diff.dense_matrix()

        rank = len(n_cells)
        old_rank = len(old_cells)

        # Create some matrix spaces to try to speed up matrix creation.
        MS_pi_t = MatrixSpace(base_ring, old_rank, len(gens[dim-1]))

        pi_old = MS_pi_t.matrix(pi_cols).transpose()
        iota_cols_old = iota_cols
        iota_cols = {}
        pi_cols_old = pi_cols
        pi_cols = []
        phi_old = MatrixSpace(base_ring, rank, old_rank, sparse=(base_ring==QQ)).zero()
        phi_old_cols = phi_old.columns()
        phi_old = conditionally_sparse(phi_old)
        to_be_deleted = []

        zero_vector = vector(base_ring, rank)
        pi_nrows = pi_old.nrows()

        for c_idx, c in enumerate(n_cells):
            # c_bar = c - phi(bdry(c)):
            # Avoid a bug in matrix-vector multiplication (trac 19378):
            if not diff:
                c_bar = c
                pi_bdry_c_bar = False
            else:
                if base_ring == QQ:
                    c_bar = c - phi_old * (diff * c)
                    pi_bdry_c_bar = conditionally_sparse(pi_old) * (diff * c_bar)
                else:
                    c_bar = c - phi_old * diff * c
                    pi_bdry_c_bar = conditionally_sparse(pi_old) * diff * c_bar

            # One small typo in the published algorithm: it says
            # "if bdry(c_bar) == 0", but should say
            # "if pi(bdry(c_bar)) == 0".
            if not pi_bdry_c_bar:
                # Append c to list of gens.
                gens[dim].append(c_idx)
                # iota(c) = c_bar
                iota_cols[c_idx] = c_bar
                # pi(c) = c
                pi_cols.append(c)
            else:
                # Take any u in gens so that lambda_i = <u, pi(bdry(c_bar))> != 0.
                # u_idx will be the index of the corresponding cell.
                (u_idx, lambda_i) = pi_bdry_c_bar.leading_item()
                for (u_idx, lambda_i) in pi_bdry_c_bar.iteritems():
                    if u_idx not in to_be_deleted:
                        break
                # This element/column needs to be deleted from gens and
                # iota_old. Do that later.
                to_be_deleted.append(u_idx)
                # pi(c) = 0.
                pi_cols.append(zero_vector)
                for c_j_idx, c_j in enumerate(old_cells):
                    # eta_ij = <u, pi(c_j)>.
                    # That is, eta_ij is the u_idx entry in the vector pi_old * c_j:
                    eta_ij = c_j.dot_product(pi_old.row(u_idx))
                    if eta_ij:
                        # Adjust phi(c_j).
                        phi_old_cols[c_j_idx] += eta_ij * lambda_i**(-1) * c_bar
                        # Adjust pi(c_j).
                        pi_cols_old[c_j_idx] -= eta_ij * lambda_i**(-1) * pi_bdry_c_bar

                # The matrices involved have many zero entries. For
                # such matrices, using sparse matrices is faster over
                # the rationals, slower over finite fields.
                phi_old = matrix(base_ring, phi_old_cols, sparse=(base_ring==QQ)).transpose()
                keep = vector(base_ring, pi_nrows, {i:1 for i in range(pi_nrows)
                                                    if i not in to_be_deleted})
                cols = [v.pairwise_product(keep) for v in pi_cols_old]
                pi_old = MS_pi_t.matrix(cols).transpose()

        # Here cols is a temporary storage for the columns of iota.
        cols = [iota_cols_old[i] for i in sorted(iota_cols_old.keys())]
        for r in sorted(to_be_deleted, reverse=True):
            del cols[r]
            del gens[dim-1][r]
        iota_data[dim-1] = matrix(base_ring, len(gens[dim-1]), old_rank, cols).transpose()
        # keep: rows to keep in pi_cols_old. Start with all
        # columns, then delete those in to_be_deleted.
        keep = sorted(set(range(pi_nrows)).difference(to_be_deleted))
        # Now cols is a temporary storage for columns of pi.
        cols = [v.list_from_positions(keep) for v in pi_cols_old]
        pi_data[dim-1] = matrix(base_ring, old_rank, len(gens[dim-1]), cols).transpose()
        phi_data[dim-1] = phi_old

        V_gens = VectorSpace(base_ring, len(gens[dim]))
        if pi_cols:
            cols = []
            for v in pi_cols:
                cols.append(V_gens(v.list_from_positions(gens[dim])))
            pi_cols = cols

    pi_data[dim] = matrix(base_ring, rank, len(gens[dim]), pi_cols).transpose()
    cols = [iota_cols[i] for i in sorted(iota_cols.keys())]
    iota_data[dim] = matrix(base_ring, len(gens[dim]), rank, cols).transpose()

    # M_data will contain (trivial) matrices defining the differential
    # on M. Keep track of the sizes using "M_rows" and "M_cols", which are
    # just the ranks of consecutive graded pieces of M.
    M_data = {}
    M_rows = 0
    for n in range(K.dimension()+1):
        M_cols = len(gens[n])
        M_data[n] = zero_matrix(base_ring, M_rows, M_cols)
        M_rows = M_cols

    M = ChainComplex(M_data, base_ring=base_ring, degree=-1)

    pi = ChainComplexMorphism(pi_data, C, M)
    iota = ChainComplexMorphism(iota_data, M, C)
    phi = ChainContraction(phi_data, pi, iota)
    return phi, M
Esempio n. 3
0
class MatrixRec(object):
    r"""
    A matrix recurrence simultaneously generating the coefficients and partial
    sums of solutions of an ODE, and possibly derivatives of this solution.

    Note: Mathematically, the recurrence matrix has the structure of a
    StepMatrix (depending on parameters). However, this class does not
    derive from StepMatrix as the data structure is different.
    """
    def __init__(self, diffop, dz, derivatives, nterms_est):

        deq_Scalars = diffop.base_ring().base_ring()
        E = dz.parent()
        if deq_Scalars.characteristic() != 0:
            raise ValueError("only makes sense for scalar rings of "
                             "characteristic 0")
        assert deq_Scalars is dz.parent() or deq_Scalars != dz.parent()

        #### Recurrence operator

        # Reduce to the case of a number field generated by an algebraic
        # integer. This is mainly intended to avoid computing gcds (due to
        # denominators in the representation of number field elements) in the
        # product tree, but could also be useful to extend the field using Pari
        # in the future.
        NF_rec, AlgInts_rec = _number_field_with_integer_gen(deq_Scalars)
        # ore_algebra currently does not support orders as scalar rings
        Pols = PolynomialRing(NF_rec, 'n')
        Rops, Sn = ore_algebra.OreAlgebra(Pols, 'Sn').objgen()
        # Using the primitive part here would break the computation of
        # residuals! (Cf. local_solutions.)
        # recop = diffop.to_S(Rops).primitive_part().numerator()
        recop = diffop.to_S(Rops)
        recop = lcm([p.denominator() for p in recop.coefficients()]) * recop
        # Ensure that ordrec >= orddeq. When the homomorphic image of diffop in
        # Rops is divisible by Sn, it can happen that the recop (e.g., after
        # normalization to Sn-valuation 0) has order < orddeq, and our strategy
        # of using vectors of coefficients of the form [u(n-s'), ..., u(n+r-1)]
        # with s'=s-r does not work in this case.
        orddelta = recop.order() - diffop.order()
        if orddelta < 0:
            recop = Sn**(-orddelta) * recop

        #### Choose computation domains

        if ((isinstance(E, (RealBallField, ComplexBallField)) or E is QQ
             or utilities.is_QQi(E) or E is RLF or E is CLF)
                and (deq_Scalars is QQ or utilities.is_QQi(deq_Scalars))):
            # Special-case QQ and QQ[i] to use arb matrices
            # (overwrites AlgInts_rec)
            self.StepMatrix_class = StepMatrix_arb
            self.binsplit_threshold = 8
            # Working precision. We typically want operations on exact balls to
            # be exact, so that overshooting shouldn't be a problem.
            # XXX Less clear in the case dz ∈ XBF!
            # XXX The rough estimate below ignores the height of rec and dz.
            # prec = nterms_est*(recop.degree()*nterms_est.nbits()
            #                    + recop.order().nbits() + 1)
            prec = 8 + nterms_est * (1 + ZZ(ZZ(recop.order()).nbits()).nbits())
            if (E is QQ or isinstance(E, RealBallField)) and deq_Scalars is QQ:
                AlgInts_rec = AlgInts_pow = RealBallField(prec)
            else:
                AlgInts_rec = AlgInts_pow = ComplexBallField(prec)
            if is_NumberField(E):
                pow_den = AlgInts_pow(dz.denominator())
            else:
                pow_den = AlgInts_pow.one()
        else:
            self.StepMatrix_class = StepMatrix_generic
            self.binsplit_threshold = 64
            if is_NumberField(E):
                # In fact we should probably do something similar for dz in any
                # finite-dimensional Q-algebra. (But how?)
                NF_pow, AlgInts_pow = _number_field_with_integer_gen(E)
                pow_den = NF_pow(dz).denominator()
            else:
                # This includes the case E = ZZ, but dz could live in pretty
                # much any algebra over deq_Scalars (including matrices,
                # intervals...). Then the computation of sums_row may take time,
                # but we still hope to gain something on the computation of the
                # coefficients and/or limit interval blow-up thanks to the use
                # of binary splitting.
                AlgInts_pow = E
                pow_den = ZZ.one()
            assert pow_den.parent() is ZZ
        assert AlgInts_pow is AlgInts_rec or AlgInts_pow != AlgInts_rec

        #### Recurrence matrix

        self.recop = recop

        self.orddeq = diffop.order()
        self.ordrec = recop.order()
        self.orddelta = self.ordrec - self.orddeq

        Pols_rec, n = PolynomialRing(AlgInts_rec, 'n').objgen()
        self.rec_coeffs = [
            -Pols_rec(recop[i])(n - self.orddelta) for i in xrange(self.ordrec)
        ]
        self.rec_den = Pols_rec(recop.leading_coefficient())(n - self.orddelta)
        # Guard against various problems related to number field embeddings and
        # uniqueness
        assert Pols_rec.base_ring() is AlgInts_rec
        assert self.rec_den.base_ring() is AlgInts_rec
        assert self.rec_den(
            self.rec_den.base_ring().zero()).parent() is AlgInts_rec

        # Also store a version of the recurrence operator of the form
        # b[0](n) + b[1](n) S^(-1) + ··· + b[s](n) S^(-s).
        # This is convenient to share code with other implementations, or at
        # least make the implementations easier to compare.
        # XXX: understand what to do about variable names!
        self.bwrec = [
            recop[self.ordrec - k](Rops.base_ring().gen() - self.ordrec)
            for k in xrange(self.ordrec + 1)
        ]

        #### Power of dz. Note that this part does not depend on n.

        # If we extend the removal of denominators above to algebras other than
        # number fields, it would probably make more sense to move this into
        # the caller. --> support dz in non-com ring (mat)? power series work
        # only over com rings
        Series_pow = PolynomialRing(AlgInts_pow, 'delta')
        self.pow_num = Series_pow([pow_den * dz, pow_den])
        self.pow_den = pow_den
        self.derivatives = derivatives

        #### Partial sums

        # We need a parent containing both the coefficients of the operator and
        # the evaluation point.
        # XXX: Is this the correct way to get one? Should we use
        # canonical_coercion()? Something else?
        # XXX: This is not powerful enough to find a number field containing
        # two given number fields (both given with embeddings into CC)

        # Work around #14982 (fixed) + weaknesses of the coercion framework for orders
        #Series_sums = sage.categories.pushout.pushout(AlgInts_rec, Series_pow)
        try:
            AlgInts_sums = sage.categories.pushout.pushout(
                AlgInts_rec, AlgInts_pow)
        except sage.structure.coerce_exceptions.CoercionException:
            AlgInts_sums = sage.categories.pushout.pushout(NF_rec, AlgInts_pow)
        assert AlgInts_sums is AlgInts_rec or AlgInts_sums != AlgInts_rec
        assert AlgInts_sums is AlgInts_pow or AlgInts_sums != AlgInts_pow

        Series_sums = PolynomialRing(AlgInts_sums, 'delta')
        assert Series_sums.base_ring() is AlgInts_sums
        # for speed
        self.Series_sums = Series_sums
        self.series_class_sums = type(Series_sums.gen())

        self.Mat_rec = MatrixSpace(AlgInts_rec, self.ordrec, self.ordrec)
        self.Mat_sums_row = MatrixSpace(Series_sums, 1, self.ordrec)
        self.Mat_series_sums = self.Mat_rec.change_ring(Series_sums)

    def __call__(self, n):
        stepmat = self.StepMatrix_class()
        stepmat.idx_start = n
        stepmat.idx_end = n + 1
        stepmat.rec_den = self.rec_den(n)
        stepmat.rec_mat = self.Mat_rec.matrix()
        for i in xrange(self.ordrec - 1):
            stepmat.rec_mat[i, i + 1] = stepmat.rec_den
        for i in xrange(self.ordrec):
            stepmat.rec_mat[self.ordrec - 1, i] = self.rec_coeffs[i](n)
        stepmat.pow_num = self.pow_num
        stepmat.pow_den = self.pow_den
        # TODO: fix redundancy--the rec_den*pow_den probabably doesn't belong
        # here
        # XXX: should we give a truncation order?
        den = stepmat.rec_den * stepmat.pow_den
        den = self.series_class_sums(self.Series_sums, [den])
        stepmat.sums_row = self.Mat_sums_row.matrix()
        stepmat.sums_row[0, self.orddelta] = den
        stepmat.ord = self.derivatives

        stepmat.BigScalars = self.Series_sums  # XXX unused in arb case
        stepmat.Mat_big_scalars = self.Mat_series_sums

        return stepmat

    def one(self, n):
        stepmat = self.StepMatrix_class()
        stepmat.idx_start = stepmat.idx_end = n
        stepmat.rec_mat = self.Mat_rec.identity_matrix()
        stepmat.rec_den = self.rec_den.base_ring().one()
        stepmat.pow_num = self.pow_num.parent().one()
        stepmat.pow_den = self.pow_den.parent().one()
        stepmat.sums_row = self.Mat_sums_row.matrix()
        stepmat.ord = self.derivatives

        stepmat.BigScalars = self.Series_sums  # XXX unused in arb case
        stepmat.Mat_big_scalars = self.Mat_series_sums

        return stepmat

    def binsplit(self, low, high):
        if high - low <= self.binsplit_threshold:
            mat = self.one(low)
            for n in xrange(low, high):
                mat.imulleft(self(n))
        else:
            mid = (low + high) // 2
            mat = self.binsplit(low, mid)
            mat.imulleft(self.binsplit(mid, high))
        return mat

    def __repr__(self):
        return pprint.pformat(self.__dict__)

    # XXX: needs testing, especially when rop.valuation() > 0
    def normalized_residual(self, maj, prod, n, j):
        r"""
        Compute the normalized residual associated with the fundamental
        solution of index j.

        TESTS::

            sage: from ore_algebra import *
            sage: DOP, t, D = DifferentialOperators()
            sage: ode = D + 1/4/(t - 1/2)
            sage: ode.numerical_transition_matrix([0,1+I,1], 1e-100, algorithm='binsplit')
            [[0.707...2078...] + [0.707...]*I]
        """
        r, s = self.orddeq, self.ordrec
        IC = bounds.IC
        # Compute the "missing" coefficients u(n-s), ..., u(n-s'-1) s'=s-r):
        # indeed, it is convenient to compute the residuals starting from
        # u(n-s), ..., u(n-1), while our recurrence matrices produce the partial
        # sum of index n along with the vector [u(n-s'), ..., u(n+r-1)].
        last = [IC.zero()] * r  # u(n-s), ..., u(n-s'-1)
        last.extend([
            IC(c) / IC(prod.rec_den)  # u(n-s'), ..., u(n+r-1)
            for c in prod.rec_mat.column(s - r + j)
        ])  # XXX: check column index
        rop = self.recop
        v = rop.valuation()
        for i in xrange(r - 1, -1, -1):  # compute u(n-s+i)
            last[i] = ~(rop[v](n - s + i)) * sum(
                rop[k](n - s + i) * last[i + k]  # u(n-s+i)
                for k in xrange(v + 1, s + 1))
        # Now compute the residual. WARNING: this residual must correspond to
        # the operator stored in maj.dop, which typically isn't self.diffop (but
        # an operator in θx equal to x^k·self.diffop for some k).
        # XXX: do not recompute this every time!
        bwrnp = [[[pol(n + i)] for pol in self.bwrec] for i in range(s)]
        altlast = [[c] for c in reversed(last[:s])]
        return maj.normalized_residual(n, altlast, bwrnp)

    def normalized_residuals(self, maj, prod, n):
        return [
            self.normalized_residual(maj, prod, n, j)
            for j in xrange(self.orddeq)
        ]

    def term(self, prod, parent, j):
        r"""
        Given a prodrix representing a product B(n-1)···B(0) where B is the
        recurrence matrix associated to some differential operator P, return the
        term of index n of the fundamental solution of P of the form
        y[j](z) = z^j + O(z^r), 0 <= j < r = order(P).
        """
        orddelta = self.orddelta
        num = parent(prod.rec_mat[orddelta + j, orddelta]) * parent(
            prod.pow_num[0])
        den = parent(prod.rec_den) * parent(prod.pow_den)
        return num / den

    def partial_sums(self, prod, ring, rows):
        r"""
        Return a matrix of partial sums of the series and its derivatives.
        """
        numer = matrix(ring, rows, self.orddeq,
                       lambda i, j: prod.sums_row[0, self.orddelta + j][i])
        denom = ring(prod.rec_den) * ring(prod.pow_den)
        return numer / denom

    def error_estimate(self, prod):
        orddelta = self.orddelta
        num1 = sum(
            abs(prod.rec_mat[orddelta + j, orddelta])
            for j in range(self.orddeq))
        num2 = sum(abs(a) for a in prod.pow_num)
        den = abs(prod.rec_den) * abs(prod.pow_den)
        return num1 * num2 / den