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
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
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