class Point(SageObject): r""" A point on the complex plane with an associated differential operator. A point can be exact (a number field element) or inexact (a real or complex interval or ball). It can be classified as ordinary, regular singular, etc. The main reason for making the operator part of the definition of Points is that this gives a convenient place to cache information that depend on both, with an appropriate lifetime. Note however that the point is considered to lie on the complex plane, not on the Riemann surface of the operator. """ def __init__(self, point, dop=None): """ TESTS:: sage: from ore_algebra import * sage: from ore_algebra.analytic.path import Point sage: Dops, x, Dx = DifferentialOperators() sage: [Point(z, Dx) ....: for z in [1, 1/2, 1+I, QQbar(I), RIF(1/3), CIF(1/3), pi, ....: RDF(1), CDF(I), 0.5r, 0.5jr, 10r, QQbar(1), AA(1/3)]] [1, 1/2, I + 1, I, [0.333333333333333...], [0.333333333333333...], 3.141592653589794?, 1.000000000000000, 1.000000000000000*I, 0.5000000000000000, 0.5000000000000000*I, 10, 1, 1/3] sage: Point(sqrt(2), Dx).iv() [1.414...] """ SageObject.__init__(self) from sage.rings.complex_double import ComplexDoubleField_class from sage.rings.complex_field import ComplexField_class from sage.rings.complex_interval_field import ComplexIntervalField_class from sage.rings.real_double import RealDoubleField_class from sage.rings.real_mpfi import RealIntervalField_class from sage.rings.real_mpfr import RealField_class point = sage.structure.coerce.py_scalar_to_element(point) try: parent = point.parent() except AttributeError: raise TypeError("unexpected value for point: " + repr(point)) if isinstance(point, Point): self.value = point.value elif isinstance( parent, (number_field_base.NumberField, RealBallField, ComplexBallField)): self.value = point elif QQ.has_coerce_map_from(parent): self.value = QQ.coerce(point) # must come before QQbar, due to a bogus coerce map (#14485) elif parent is sage.symbolic.ring.SR: try: return self.__init__(point.pyobject(), dop) except TypeError: pass try: return self.__init__(QQbar(point), dop) except (TypeError, ValueError, NotImplementedError): pass try: self.value = RLF(point) except (TypeError, ValueError): self.value = CLF(point) elif QQbar.has_coerce_map_from(parent): alg = QQbar.coerce(point) NF, val, hom = alg.as_number_field_element() if NF is QQ: self.value = QQ.coerce(val) # parent may be ZZ else: embNF = number_field.NumberField(NF.polynomial(), NF.variable_name(), embedding=hom(NF.gen())) self.value = val.polynomial()(embNF.gen()) elif isinstance( parent, (RealField_class, RealDoubleField_class, RealIntervalField_class)): self.value = RealBallField(point.prec())(point) elif isinstance(parent, (ComplexField_class, ComplexDoubleField_class, ComplexIntervalField_class)): self.value = ComplexBallField(point.prec())(point) else: try: self.value = RLF.coerce(point) except TypeError: self.value = CLF.coerce(point) parent = self.value.parent() assert (isinstance( parent, (number_field_base.NumberField, RealBallField, ComplexBallField)) or parent is RLF or parent is CLF) self.dop = dop or point.dop self.keep_value = False def _repr_(self): """ TESTS:: sage: from ore_algebra import * sage: from ore_algebra.analytic.path import Point sage: Dops, x, Dx = DifferentialOperators() sage: Point(10**20, Dx) ~1.0000e20 """ try: len = (self.value.numerator().real().numerator().nbits() + self.value.numerator().imag().numerator().nbits() + self.value.denominator().nbits()) if len > 50: return '~' + repr(self.value.n(digits=5)) except AttributeError: pass return repr(self.value) # Numeric representations @cached_method def iv(self): """ sage: from ore_algebra import * sage: from ore_algebra.analytic.path import Point sage: Dops, x, Dx = DifferentialOperators() sage: [Point(z, Dx).iv() ....: for z in [1, 1/2, 1+I, QQbar(I), RIF(1/3), CIF(1/3), pi]] [1.000000000000000, 0.5000000000000000, 1.000000000000000 + 1.000000000000000*I, 1.000000000000000*I, [0.333333333333333 +/- 3.99e-16], [0.333333333333333 +/- 3.99e-16], [3.141592653589793 +/- 7.83e-16]] """ return IC(self.value) def exact(self): r""" sage: from ore_algebra import * sage: from ore_algebra.analytic.path import Point sage: Dops, x, Dx = DifferentialOperators() sage: QQi.<i> = QuadraticField(-1) sage: [Point(z, Dx).exact() for z in [1, 1/2, 1+i, QQbar(I)]] [1, 1/2, i + 1, I] sage: [Point(z, Dx).exact() for z in [RBF(3/4), RBF(1) + I]] [3/4, i + 1] sage: Point(RIF(1/3), Dx).exact() Traceback (most recent call last): ... ValueError """ if self.is_exact(): return self elif isinstance(self.value, RealBall) and self.value.is_exact(): return Point(QQ(self.value), self.dop) elif isinstance(self.value, ComplexBall) and self.value.is_exact(): value = QQi((QQ(self.value.real()), QQ(self.value.imag()))) return Point(value, self.dop) raise ValueError def approx_abs_real(self, prec): r""" Compute an approximation with absolute error about 2^(-prec). """ if isinstance(self.value.parent(), RealBallField): return self.value elif self.value.is_zero(): return RealBallField(max(2, prec)).zero() elif self.is_real(): expo = ZZ(IR(self.value).abs().log(2).upper().ceil()) rel_prec = max(2, prec + expo + 10) val = RealBallField(rel_prec)(self.value) return val else: raise ValueError("point may not be real") def is_real(self): return is_real_parent(self.value.parent()) def is_exact(self): # XXX: also include exact balls? return isinstance( self.value, (rings.Integer, rings.Rational, rings.NumberFieldElement)) ### Methods that depend on dop @cached_method def is_ordinary(self): lc = self.dop.leading_coefficient() if self.is_exact(): return bool(lc(self.value)) elif not lc(self.iv()).contains_zero(): return True else: raise ValueError("can't tell if inexact point is singular") def is_singular(self): return not is_ordinary(self) @cached_method def is_regular(self): try: if self.is_ordinary(): return True except ValueError: # we could handle balls containing no irregular singular point... raise NotImplementedError("can't tell if inexact point is regular") assert self.is_exact() # Fuchs criterion Pols = self.dop.base_ring().change_ring(self.value.parent()) def val(pol): return Pols(pol).valuation(Pols([self.value, -1])) ref = val(self.dop.leading_coefficient()) - self.dop.order() return all(val(coef) - k >= ref for k, coef in enumerate(self.dop)) def is_regular_singular(self): return not self.is_ordinary() and self.is_regular() def is_irregular(self): return not is_regular(self) def singularity_type(self, short=False): r""" EXAMPLES:: sage: from ore_algebra import * sage: from ore_algebra.analytic.path import Point sage: Dops, x, Dx = DifferentialOperators() sage: dop = (x^2 + 1)*Dx^2 + 2*x*Dx sage: Point(1, dop).singularity_type() 'ordinary point' sage: Point(i, dop).singularity_type() 'regular singular point' sage: Point(0, x^2*Dx + 1).singularity_type() 'irregular singular point' sage: Point(CIF(1/3), x^2*Dx + 1).singularity_type() 'ordinary point' sage: Point(CIF(1/3)-1/3, x^2*Dx + 1).singularity_type() 'point of unknown singularity type' """ try: if self.is_ordinary(): return "" if short else "ordinary point" elif self.is_regular(): return "regular singular point" else: return "irregular singular point" except (ValueError, NotImplementedError): return "point of unknown singularity type" def descr(self): t = self.singularity_type(short=True) if t == "": return repr(self) else: return t + " " + repr(self) def dist_to_sing(self): """ Distance of self to the singularities of self.dop *other than self*. TESTS:: sage: from ore_algebra import * sage: from ore_algebra.analytic.path import Point sage: Dops, x, Dx = DifferentialOperators() sage: dop = (x^2 + 1)*Dx^2 + 2*x*Dx sage: Point(1, dop).dist_to_sing() [1.41421356237309...] sage: Point(i, dop).dist_to_sing() 2.00... sage: Point(1+i, dop).dist_to_sing() 1.00... """ # TODO - solve over CBF directly; perhaps with arb's own poly solver sing = dop_singularities(self.dop, CIF) sing = [IC(s) for s in sing] close, distant = split(lambda s: s.overlaps(self.iv()), sing) if (len(close) >= 2 or len(close) == 1 and not self.dop.leading_coefficient()(self.value).is_zero()): raise NotImplementedError # refine? dist = [(self.iv() - s).abs() for s in distant] min_dist = IR(rings.infinity).min(*dist) if min_dist.contains_zero(): raise NotImplementedError # refine??? return IR(min_dist.lower()) def local_diffop(self): # ? r""" TESTS:: sage: from ore_algebra import DifferentialOperators sage: from ore_algebra.analytic.path import Point sage: Dops, x, Dx = DifferentialOperators() sage: Point(1, x*Dx - 1).local_diffop() (x + 1)*Dx - 1 sage: Point(RBF(1/2), x*Dx - 1).local_diffop() (x + 1/2)*Dx - 1 """ Pols_dop = self.dop.base_ring() # NOTE: pushout(QQ[x], K) doesn't handle embeddings well, and creates # an L equal but not identical to K. But then other constructors like # PolynomialRing(L, x) sometimes return objects over K found in cache, # leading to endless headaches with slow coercions. But the version here # may be closer to what I really want in any case. # XXX: This seems to work in the usual trivial case where we are looking # for a scalar domain containing QQ and QQ[i], but probably won't be # enough if we really have two different number fields with embeddings ex = self.exact() Scalars = pushout.pushout(Pols_dop.base_ring(), ex.value.parent()) Pols = Pols_dop.change_ring(Scalars) A, B = self.dop.base_ring().base_ring(), ex.value.parent() C = Pols.base_ring() assert C is A or C != A assert C is B or C != B dop_P = self.dop.change_ring(Pols) return dop_P.annihilator_of_composition(Pols([ex.value, 1])) def local_basis_structure(self): r""" EXAMPLES:: sage: from ore_algebra import * sage: from ore_algebra.analytic.path import Point sage: Dops, x, Dx = DifferentialOperators() sage: Point(0, x*Dx^2 + Dx + x).local_basis_structure() [FundamentalSolution(leftmost=0, shift=0, log_power=1, value=None), FundamentalSolution(leftmost=0, shift=0, log_power=0, value=None)] sage: Point(0, Dx^3 + x*Dx + x).local_basis_structure() [FundamentalSolution(leftmost=0, shift=0, log_power=0, value=None), FundamentalSolution(leftmost=0, shift=1, log_power=0, value=None), FundamentalSolution(leftmost=0, shift=2, log_power=0, value=None)] """ # TODO: provide a way to compute the first terms of the series. First # need a good way to share code with fundamental_matrix_regular. Or # perhaps modify generalized_series_solutions() to agree with our # definition of the basis? if self.is_ordinary(): # support inexact points in this case return [ FundamentalSolution(QQbar.zero(), ZZ(expo), ZZ.zero(), None) for expo in range(self.dop.order()) ] elif not self.is_regular(): raise NotImplementedError("irregular singular point") sols = map_local_basis(self.local_diffop(), lambda ini, bwrec: None, lambda leftmost, shift: {}) sols.sort(key=sort_key_by_asympt) return sols
class Point(SageObject): r""" A point on the complex plane with an associated differential operator. A point can be exact (a number field element) or inexact (a real or complex interval or ball). It can be classified as ordinary, regular singular, etc. The main reason for making the operator part of the definition of Points is that this gives a convenient place to cache information that depend on both, with an appropriate lifetime. Note however that the point is considered to lie on the complex plane, not on the Riemann surface of the operator. """ def __init__(self, point, dop=None, singular=None, **kwds): """ INPUT: - ``singular``: can be set to True to force this point to be considered a singular point, even if this cannot be checked (e.g. because we only have an enclosure) TESTS:: sage: from ore_algebra import * sage: from ore_algebra.analytic.path import Point sage: Dops, x, Dx = DifferentialOperators() sage: [Point(z, Dx) ....: for z in [1, 1/2, 1+I, QQbar(I), RIF(1/3), CIF(1/3), pi, ....: RDF(1), CDF(I), 0.5r, 0.5jr, 10r, QQbar(1), AA(1/3)]] [1, 1/2, I + 1, I, [0.333333333333333...], [0.333333333333333...], 3.141592653589794?, 1.000000000000000, 1.000000000000000*I, 0.5000000000000000, 0.5000000000000000*I, 10, 1, 1/3] sage: Point(sqrt(2), Dx).iv() [1.414...] sage: Point(RBF(0), (x-1)*x*Dx, singular=True).dist_to_sing() 1.000000000000000 """ SageObject.__init__(self) from sage.rings.complex_double import ComplexDoubleField_class from sage.rings.complex_field import ComplexField_class from sage.rings.complex_interval_field import ComplexIntervalField_class from sage.rings.real_double import RealDoubleField_class from sage.rings.real_mpfi import RealIntervalField_class from sage.rings.real_mpfr import RealField_class point = sage.structure.coerce.py_scalar_to_element(point) try: parent = point.parent() except AttributeError: raise TypeError("unexpected value for point: " + repr(point)) if isinstance(point, Point): self.value = point.value elif isinstance( parent, (number_field_base.NumberField, RealBallField, ComplexBallField)): self.value = point elif QQ.has_coerce_map_from(parent): self.value = QQ.coerce(point) # must come before QQbar, due to a bogus coerce map (#14485) elif parent is sage.symbolic.ring.SR: try: return self.__init__(point.pyobject(), dop) except TypeError: pass try: return self.__init__(QQbar(point), dop) except (TypeError, ValueError, NotImplementedError): pass try: self.value = RLF(point) except (TypeError, ValueError): self.value = CLF(point) elif QQbar.has_coerce_map_from(parent): alg = QQbar.coerce(point) NF, val, hom = alg.as_number_field_element() if NF is QQ: self.value = QQ.coerce(val) # parent may be ZZ else: embNF = number_field.NumberField(NF.polynomial(), NF.variable_name(), embedding=hom(NF.gen())) self.value = val.polynomial()(embNF.gen()) elif isinstance( parent, (RealField_class, RealDoubleField_class, RealIntervalField_class)): self.value = RealBallField(point.prec())(point) elif isinstance(parent, (ComplexField_class, ComplexDoubleField_class, ComplexIntervalField_class)): self.value = ComplexBallField(point.prec())(point) else: try: self.value = RLF.coerce(point) except TypeError: self.value = CLF.coerce(point) parent = self.value.parent() assert (isinstance( parent, (number_field_base.NumberField, RealBallField, ComplexBallField)) or parent is RLF or parent is CLF) if dop is None: # TBI if isinstance(point, Point): self.dop = point.dop else: self.dop = DifferentialOperator(dop.numerator()) self._force_singular = bool(singular) self.options = kwds def _repr_(self): """ TESTS:: sage: from ore_algebra import * sage: from ore_algebra.analytic.path import Point sage: Dops, x, Dx = DifferentialOperators() sage: Point(10**20, Dx) ~1.0000e20 """ try: len = (self.value.numerator().real().numerator().nbits() + self.value.numerator().imag().numerator().nbits() + self.value.denominator().nbits()) if len > 50: return '~' + repr(self.value.n(digits=5)) except AttributeError: pass return repr(self.value) # Numeric representations @cached_method def iv(self): """ sage: from ore_algebra import * sage: from ore_algebra.analytic.path import Point sage: Dops, x, Dx = DifferentialOperators() sage: [Point(z, Dx).iv() ....: for z in [1, 1/2, 1+I, QQbar(I), RIF(1/3), CIF(1/3), pi]] [1.000000000000000, 0.5000000000000000, 1.000000000000000 + 1.000000000000000*I, 1.000000000000000*I, [0.333333333333333 +/- 3.99e-16], [0.333333333333333 +/- 3.99e-16], [3.141592653589793 +/- 7.83e-16]] """ return IC(self.value) def exact(self): r""" sage: from ore_algebra import * sage: from ore_algebra.analytic.path import Point sage: Dops, x, Dx = DifferentialOperators() sage: QQi.<i> = QuadraticField(-1) sage: [Point(z, Dx).exact() for z in [1, 1/2, 1+i, QQbar(I)]] [1, 1/2, i + 1, I] sage: [Point(z, Dx).exact() for z in [RBF(3/4), RBF(1) + I]] [3/4, i + 1] sage: Point(RIF(1/3), Dx).exact() Traceback (most recent call last): ... ValueError """ if self.value.parent().is_exact(): return self elif isinstance(self.value, RealBall) and self.value.is_exact(): return Point(QQ(self.value), self.dop) elif isinstance(self.value, ComplexBall) and self.value.is_exact(): value = QQi((QQ(self.value.real()), QQ(self.value.imag()))) return Point(value, self.dop) raise ValueError def approx_abs_real(self, prec): r""" Compute an approximation with absolute error about 2^(-prec). """ if isinstance(self.value.parent(), RealBallField): return self.value elif self.value.is_zero(): return RealBallField(max(2, prec)).zero() elif self.is_real(): expo = ZZ(IR(self.value).abs().log(2).upper().ceil()) rel_prec = max(2, prec + expo + 10) val = RealBallField(rel_prec)(self.value) return val else: raise ValueError("point may not be real") def is_real(self): return is_real_parent(self.value.parent()) def is_exact(self): return (isinstance( self.value, (rings.Integer, rings.Rational, rings.NumberFieldElement)) or isinstance(self.value, (RealBall, ComplexBall)) and self.value.is_exact()) def rationalize(self): a = self.iv() lc = self.dop.leading_coefficient() if lc(a).contains_zero(): raise PathPrecisionError else: return Point(_rationalize(a), self.dop) # Point equality is identity def __eq__(self, other): return self is other def __hash__(self): return id(self) ### Methods that depend on dop @cached_method def is_ordinary(self): if self._force_singular: return False lc = self.dop.leading_coefficient() if not lc(self.iv()).contains_zero(): return True if self.is_exact(): try: val = lc(self.value) except TypeError: # work around coercion weaknesses val = lc.change_ring(QQbar)(QQbar.coerce(self.value)) return not val.is_zero() else: raise ValueError("can't tell if inexact point is singular") def is_singular(self): return not self.is_ordinary() @cached_method def is_regular(self): try: if self.is_ordinary(): return True except ValueError: # we could handle balls containing no irregular singular point... raise NotImplementedError("can't tell if inexact point is regular") assert self.is_exact() # Fuchs criterion Pols = self.dop.base_ring().change_ring(self.value.parent()) def val(pol): return Pols(pol).valuation(Pols([self.value, -1])) ref = val(self.dop.leading_coefficient()) - self.dop.order() return all(val(coef) - k >= ref for k, coef in enumerate(self.dop)) def is_regular_singular(self): return not self.is_ordinary() and self.is_regular() def is_irregular(self): return not is_regular(self) def singularity_type(self, short=False): r""" EXAMPLES:: sage: from ore_algebra import * sage: from ore_algebra.analytic.path import Point sage: Dops, x, Dx = DifferentialOperators() sage: dop = (x^2 + 1)*Dx^2 + 2*x*Dx sage: Point(1, dop).singularity_type() 'ordinary point' sage: Point(i, dop).singularity_type() 'regular singular point' sage: Point(0, x^2*Dx + 1).singularity_type() 'irregular singular point' sage: Point(CIF(1/3), x^2*Dx + 1).singularity_type() 'ordinary point' sage: Point(CIF(1/3)-1/3, x^2*Dx + 1).singularity_type() 'point of unknown singularity type' """ try: if self.is_ordinary(): return "" if short else "ordinary point" elif self.is_regular(): return "regular singular point" else: return "irregular singular point" except (ValueError, NotImplementedError): return "point of unknown singularity type" def descr(self): t = self.singularity_type(short=True) if t == "": return repr(self) else: return t + " " + repr(self) def dist_to_sing(self): """ Distance of self to the singularities of self.dop *other than self*. TESTS:: sage: from ore_algebra import * sage: from ore_algebra.analytic.path import Point sage: Dops, x, Dx = DifferentialOperators() sage: dop = (x^2 + 1)*Dx^2 + 2*x*Dx sage: Point(1, dop).dist_to_sing() [1.41421356237309...] sage: Point(i, dop).dist_to_sing() 2.00... sage: Point(1+i, dop).dist_to_sing() 1.00... """ sing = self.dop._singularities(IC) close, distant = split(lambda s: s.overlaps(self.iv()), sing) if (len(close) >= 2 or len(close) == 1 and not self.is_singular()): raise NotImplementedError # refine? dist = [(self.iv() - s).abs() for s in distant] min_dist = IR(rings.infinity).min(*dist) if min_dist.contains_zero(): raise NotImplementedError # refine??? return IR(min_dist.lower()) def local_basis_structure(self): r""" EXAMPLES:: sage: from ore_algebra import * sage: from ore_algebra.analytic.path import Point sage: Dops, x, Dx = DifferentialOperators() sage: Point(0, x*Dx^2 + Dx + x).local_basis_structure() [FundamentalSolution(leftmost=0, shift=0, log_power=1, value=None), FundamentalSolution(leftmost=0, shift=0, log_power=0, value=None)] sage: Point(0, Dx^3 + x*Dx + x).local_basis_structure() [FundamentalSolution(leftmost=0, shift=0, log_power=0, value=None), FundamentalSolution(leftmost=0, shift=1, log_power=0, value=None), FundamentalSolution(leftmost=0, shift=2, log_power=0, value=None)] """ # TODO: provide a way to compute the first terms of the series. First # need a good way to share code with fundamental_matrix_regular. Or # perhaps modify generalized_series_solutions() to agree with our # definition of the basis? if self.is_ordinary(): # support inexact points in this case return [ FundamentalSolution(QQbar.zero(), ZZ(expo), ZZ.zero(), None) for expo in range(self.dop.order()) ] elif not self.is_regular(): raise NotImplementedError("irregular singular point") sols = LocalBasisMapper().run(self.dop.shift(self)) sols.sort(key=sort_key_by_asympt) return sols