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