def compute_series_truncations(f,x,y,alpha,T): """ Computes the Puiseux series expansions at the `x`-point `x=a` with the necessary number of terms in order to compute the integral basis of the algebraic functions field corresponding to `f`. The Puiseux series is returned in parametric form for computational efficiency. (Sympy doesn't do as well with fractional exponents.) """ # compute the first terms of the Puiseux series expansions p = puiseux(f,x,y,alpha,nterms=0,parametric=False) # compute the expansion bounds N = compute_expansion_bounds(p,x,alpha) Nmax = max(N) # compute Puiseux series and truncate using the expansion bounds. r = puiseux(f,x,y,alpha,degree_bound=Nmax) n = len(r) for i in xrange(n): terms = r[i].collect(x-alpha,evaluate=False) ri_trunc = sum( coeff*term for term,coeff in terms.iteritems() if term.as_coeff_exponent(x)[1] <= N[i] ) r[i] = ri_trunc return r
def _multiplicity(g,u,v,u0,v0): """ Returns the multiplicity of the place (alpha : beta : 1) from the Puiseux series P at the place. For each (parametric) Puiseux series Pj = { x = x(t) { y = y(t) at (alpha : beta : 1) the contribution from Pj to the multiplicity is min( deg x(t), deg y(t) ). """ # compute Puiseux expansions at u=u0 and filter out # only those with v(t=0) == v0 P = puiseux(g,u,v,u0,nterms=0,parametric=_t) m = 0 for X,Y in P: X = X - u0 # Shift so no constant Y = Y - v0 # term remains. ri = abs( X.leadterm(_t)[1] ) # Get order of lead term si = abs( Y.leadterm(_t)[1] ) m += min(ri,si) return m
def test_extend_to_t(self): p = puiseux(self.f2, 0) pi = p[0] ti = 0.1 # 1e-8 pi.extend_to_t(ti, 1e-8) xt = pi.eval_x(ti) yt = pi.eval_y(ti) error = abs(self.f2(xt, yt)) self.assertLess(error, 1e-8) # 1e-14 pi.extend_to_t(ti, curve_tol=1e-14) xt = pi.eval_x(ti) yt = pi.eval_y(ti) error = abs(self.f2(xt, yt)) self.assertLess(error, 1e-14) # 1e-20 (multi-precise) pi.extend_to_t(ti, curve_tol=1e-20) xt = pi.eval_x(ti) yt = pi.eval_y(ti) error = abs(self.f2(xt, yt)) self.assertLess(error, 1e-20)
def get_PQ(self, f, a=0): p = puiseux(f, a) if p: series = [(P._xpart, P._ypart) for P in p] else: series = [] return series
def centered_at_place(self, P, order=None): r"""Rewrite the differential in terms of the local coordinates at `P`. If `P` is a regular place, then returns `self` as a sympy expression. Otherwise, if `P` is a discriminant place :math:`P(t) = \{x(t), y(t)\}` then returns .. math:: \omega |_P = q(x(t),y(t)) x'(t) / \partial_y f(x(t),y(t)). Parameters ---------- P : Place order : int, optional Passed to :meth:`PuiseuxTSeries.eval_y`. Returns ------- sympy.Expr """ # by default, non-discriminant places do not store Pusieux series # expansions. this might change in the future if P.is_discriminant(): p = P.puiseux_series else: p = puiseux(self.RS.f)[0] # substitute Puiseux series expansion into the differrential and expand # as a Laurent series xt = p.xpart yt = p.ypart.add_bigoh(p.order) dxdt = xt.derivative() omega = self.numer(xt,yt) * dxdt / self.denom(xt,yt) return omega
def get_PQ(self,f, a=0): p = puiseux(f,a) if p: series = [(P._xpart,P._ypart) for P in p] else: series = [] return series
def centered_at_place(self, P, order=None): r"""Rewrite the differential in terms of the local coordinates at `P`. If `P` is a regular place, then returns `self` as a sympy expression. Otherwise, if `P` is a discriminant place :math:`P(t) = \{x(t), y(t)\}` then returns .. math:: \omega |_P = q(x(t),y(t)) x'(t) / \partial_y f(x(t),y(t)). Parameters ---------- P : Place order : int, optional Passed to :meth:`PuiseuxTSeries.eval_y`. Returns ------- sympy.Expr """ # by default, non-discriminant places do not store Pusieux series # expansions. this might change in the future if P.is_discriminant(): p = P.puiseux_series else: p = puiseux(self.RS.f)[0] # substitute Puiseux series expansion into the differrential and expand # as a Laurent series xt = p.xpart yt = p.ypart.add_bigoh(p.order) dxdt = xt.derivative() omega = self.numer(xt, yt) * dxdt / self.denom(xt, yt) return omega
def test_extend_to_t(self): p = puiseux(self.f2,0) pi = p[0] ti = 0.1 # 1e-8 pi.extend_to_t(ti, 1e-8) xt = pi.eval_x(ti) yt = pi.eval_y(ti) error = abs(self.f2(xt,yt)) self.assertLess(error, 1e-8) # 1e-14 pi.extend_to_t(ti, curve_tol=1e-14) xt = pi.eval_x(ti) yt = pi.eval_y(ti) error = abs(self.f2(xt,yt)) self.assertLess(error, 1e-14) # 1e-20 (multi-precise) pi.extend_to_t(ti, curve_tol=1e-20) xt = pi.eval_x(ti) yt = pi.eval_y(ti) error = abs(self.f2(xt,yt)) self.assertLess(error, 1e-20)
def test_puiseux(self): self.assertEqual(puiseux(f1,x,y,0,4), [(T**2, T**7/2 + T**6 + T**5 + T**4)]) self.assertEqual(puiseux(f2,x,y,0,4), [(T, -3*T**19/256 + 3*T**14/128 - T**9/16 + T**4/2), (-T**2/2, -T**18/16384 + 3*T**13/4096 - T**8/64 - T**3/2)]) self.assertEqual(puiseux(f3,x,y,0,2), [(-57**(1/2)*T/19, 136*57**(1/2)*T**2/1083 + T), (57**(1/2)*T/19, -136*57**(1/2)*T**2/1083 + T), (T, -11*3**(1/2)*T/12 - 3**(1/2)/2), (T, 11*3**(1/2)*T/12 + 3**(1/2)/2)]) self.assertEqual(puiseux(f4,x,y,0,4), [(-T, T**4/16 - T**3/8 + T**2/2 + T), (T, -T**4/16 - T**3/8 - T**2/2 + T)])
def singularities(f): r"""Returns the singularities of the curve `f` in projective space. Returns all of the projective singular points :math:`(x_k,y_k,z_k)` on the curve :math:`f(x,y) = 0`. For each singular point a three-tuple :math:`(m, \delta, r)` is given representing the multiplicity, delta invariant, and branching number of the singularity. This information is used to resolve singularities for the purposes of computing a Riemann surface. The singularities are resolved using Puiseux series. Parameters ---------- f : polynomial A plane algebraic curve. Returns ------- list A list of the singularities, both finite and infinite, along with their multiplicity, delta invariant, and branching number information. """ S = singular_points_finite(f) S_oo = singular_points_infinite(f) S.extend(S_oo) info = [] for singular_pt in S: # perform a projective transformation of the curve so it's almost # centered at the singular point. g, u0, v0 = _transform(f, singular_pt) P = puiseux(g, u0, v0) # filter out any places with infinite v-part: they are being handled by # other centerings / transformations def has_finite_v(Pi): # make sure the order of the y-part is positive while Pi.order <= 0: Pi.add_term() n, alpha = zip(*Pi.terms) # if there are still no terms then they are positive exponent if n == []: return True elif min(n) >= 0: return True return False P = filter(has_finite_v, P) # P now consists of the places that project to (u0,v0) on the curve m = _multiplicity(P) delta = _delta_invariant(P) r = _branching_number(P) info.append((m, delta, r)) return zip(S, info)
def test_puiseux(self): self.assertEqual(puiseux(f1,x,y,0,4,parametric=T), [(T**2, T**7/2 + T**6 + T**5 + T**4)]) self.assertEqual(puiseux(f2,x,y,0,4,parametric=T), [(T,-3*T**19/256 + 3*T**14/128 - T**9/16 + T**4/2), (-T**2/2,-T**18/16384 + 3*T**13/4096 - T**8/64 - T**3/2)]) half = Rational(1,2) self.assertEqual(puiseux(f3,x,y,0,0,parametric=T), [(-57**half*T/19, 136*57**half*T**2/1083 + T), (57**half*T/19, -136*57**half*T**2/1083 + T), (T, -11*3**half*T/12 - 3**half/2), (T, 11*3**half*T/12 + 3**half/2)]) self.assertEqual(puiseux(f4,x,y,0,4,parametric=T), [(-T, T**4/16 - T**3/8 + T**2/2 + T), (T, -T**4/16 - T**3/8 - T**2/2 + T)])
def test_example_puiseux(self): p = puiseux(self.f1, 0)[0] px = p.xseries() R = px[0].parent() x = R.gen() half = QQ(1)/2 self.assertEqual(px[0].truncate(1), -x**half) self.assertEqual(px[1].truncate(1), x**half) p = puiseux(self.f2, 0)[0] px = p.xseries() R = px[0].parent() x = R.gen() third = QQ(1)/3 S = QQ['t']; t = S.gen() alpha,beta,gamma = (t**3 - 1).roots(ring=QQbar, multiplicities=False) self.assertEqual(px[0].truncate(1), alpha*x**third) self.assertEqual(px[1].truncate(1), gamma*x**third) self.assertEqual(px[2].truncate(1), beta*x**third)
def test_example_puiseux(self): p = puiseux(self.f1, 0)[0] px = p.xseries() R = px[0].parent() x = R.gen() half = QQ(1) / 2 self.assertEqual(px[0].truncate(1), -x**half) self.assertEqual(px[1].truncate(1), x**half) p = puiseux(self.f2, 0)[0] px = p.xseries() R = px[0].parent() x = R.gen() third = QQ(1) / 3 S = QQ['t'] t = S.gen() alpha, beta, gamma = (t**3 - 1).roots(ring=QQbar, multiplicities=False) self.assertEqual(px[0].truncate(1), alpha * x**third) self.assertEqual(px[1].truncate(1), gamma * x**third) self.assertEqual(px[2].truncate(1), beta * x**third)
def _delta_invariant(g,u,v,u0,v0): """ Returns the delta invariant corresponding to the singular point `singular_pt` = [alpha, beta, gamma] on the plane algebraic curve f(x,y) = 0. """ # compute Puiseux expansions at u=u0 and filter out only those # with v(t=0) == v0. We only chose one y=y(x) Puiseux series for # each place as a representative to prevent over-counting by using # the "grouped=True" flag in Puiseux P = puiseux(g,u,v,u0,nterms=0,parametric=_t,grouped=True) P_x = puiseux(g,u,v,u0,nterms=0,parametric=False,grouped=True) P_x_v0 = [] for i in range(len(P)): X,Y = P[i] p = P_x[i][0] if Y.subs(_t,0).simplify() == v0: # store the index as well so we know which parametric form # corresponds to this puiseux series. P_x_v0.append((p,i)) # now obtain ungrouped series P_x = puiseux(g,u,v,u0,nterms=0,parametric=False) # for each place compute its contribution to the delta invariant delta = sympy.Rational(0,1) for i in range(len(P_x_v0)): yhat, place_index = P_x_v0[i] j = P_x.index(yhat) IntPj = Int(j,P_x,u,u0) # obtain the ramification index by retreiving the # corresponding parametric form. By definition, this # parametric series satisfies Y(t=0) = v0 X,Y = P[place_index] rj = (X-u0).as_coeff_exponent(_t)[1] delta += sympy.Rational(rj * IntPj - rj + 1, 2) return sympy.numer(delta)
def _branching_number(g,u,v,u0,v0): """ Returns the branching number of the place [alpha : beta : 1] from the Puiseux series P at the place. The braching number is simply the number of distinct branches (i.e. non-interacting branches) at the place. In parametric form, this is simply the number of Puiseux series at the place. """ # compute Puiseux expansions at u=u0 and filter out # only those with v(t=0) == v0 P = puiseux(g,u,v,u0,nterms=1,parametric=_t) P_v0 = [(X,Y) for X,Y in P if Y.subs(_t,0) == v0] return sympy.S(len(P_v0))
def test_extend_to_t_oo(self): p = puiseux(self.f2,'oo') pi = p[0] ti = 100 # 1e-8 pi.extend_to_t(ti, 1e-8) xt = pi.eval_x(ti) yt = pi.eval_y(ti) error = abs(self.f2(xt,yt)) self.assertLess(error, 1e-8) # 1e-12 pi.extend_to_t(ti, curve_tol=1e-12) xt = pi.eval_x(ti) yt = pi.eval_y(ti) error = abs(self.f2(xt,yt)) self.assertLess(error, 1e-12)
def test_extend_to_t_oo(self): p = puiseux(self.f2, 'oo') pi = p[0] ti = 100 # 1e-8 pi.extend_to_t(ti, 1e-8) xt = pi.eval_x(ti) yt = pi.eval_y(ti) error = abs(self.f2(xt, yt)) self.assertLess(error, 1e-8) # 1e-12 pi.extend_to_t(ti, curve_tol=1e-12) xt = pi.eval_x(ti) yt = pi.eval_y(ti) error = abs(self.f2(xt, yt)) self.assertLess(error, 1e-12)
def compute_series_truncations(f, alpha): r"""Computes Puiseux series at :math:`x=\alpha` with necessary terms. The Puiseux series expansions of :math:`f = f(x,y)` centered at :math:`\alpha` are computed up to the number of terms needed for the integral basis algorithm to be successful. The expansion degree bounds are determined by :func:`compute_expansion_bounds`. Parameters ---------- f : polynomial alpha : complex Returns ------- list : PuiseuxXSeries A list of Puiseux series expansions centered at :math:`x = \alpha` with enough terms to compute integral bases as SymPy expressions. """ # compute the parametric Puiseix series with the minimal number of terms # needed to distinguish them. pt = puiseux(f, alpha) px = [p for P in pt for p in P.xseries()] # compute the orders necessary for the integral basis algorithm. the orders # are on the Puiseux x-series (non-parametric) so scale by the ramification # index of each series N = compute_expansion_bounds(px) for i in range(len(N)): e = px[i].ramification_index N[i] = ceil(N[i] * e) order = max(N) + 1 for pti in pt: pti.extend(order=order) # recompute the corresponding x-series with the extened terms px = [p for P in pt for p in P.xseries()] return px
def compute_series_truncations(f, alpha): r"""Computes Puiseux series at :math:`x=\alpha` with necessary terms. The Puiseux series expansions of :math:`f = f(x,y)` centered at :math:`\alpha` are computed up to the number of terms needed for the integral basis algorithm to be successful. The expansion degree bounds are determined by :func:`compute_expansion_bounds`. Parameters ---------- f : polynomial alpha : complex Returns ------- list : PuiseuxXSeries A list of Puiseux series expansions centered at :math:`x = \alpha` with enough terms to compute integral bases as SymPy expressions. """ # compute the parametric Puiseix series with the minimal number of terms # needed to distinguish them. pt = puiseux(f,alpha) px = [p for P in pt for p in P.xseries()] # compute the orders necessary for the integral basis algorithm. the orders # are on the Puiseux x-series (non-parametric) so scale by the ramification # index of each series N = compute_expansion_bounds(px) for i in range(len(N)): e = px[i].ramification_index N[i] = ceil(N[i]*e) order = max(N) + 1 for pti in pt: pti.extend(order=order) # recompute the corresponding x-series with the extened terms px = [p for P in pt for p in P.xseries()] return px
def ordered_puiseux_series(riemann_surface, complex_path, y0, target_point): r"""Returns an ordered list of Puiseux series such that each Puiseux series matches with the corresponding y-fibre element above the starting point of `complex_path`. In order to analytically continue from the regular places at the beginning of the path :math:`x=a` to the discriminant places at the end of the path :math:`x=b`we need to compute all of the `PuiseuxXSeries` at :math:`x=b`. There are two steps to this calculation: * compute enough terms of the Puiseux series centered at :math:`x=b` in order to accurately capture the y-roots at :math:`x=a`. * permute the series accordingly to match up with the y-roots at :math:`x=a`. Parameters ---------- riemann_surface : RiemannSurface The riemann surface on which all of this lives. complex_path : ComplexPath The path or path segment starting at a regular point and ending at a discriminant point. y0 : list of complex The starting fibre lying above the starting point of `complex_path`. The first component of the list indicates the starting sheet. target_point : complex The point to analytically continue to. Usually a discriminant point. Methods ------- .. autosummary:: analytically_continue Returns ------- list, Place : a list of Puiseux series and a Place A list of ordered Puiseux series corresponding to each branch above :math:`x=a` as well as the place that the first y-fibre element analytically continues to. """ # obtain all puiseux series above the target place f = riemann_surface.f x0 = CC(complex_path(0)) # XXX - need to coerce input to CC y0 = numpy.array(y0, dtype=complex) P = puiseux(f, target_point) # extend the Puiseux series to enough terms to accurately captue the # y-fibre above x=a (the starting point of the complex path) for Pi in P: Pi.extend_to_x(x0) # compute the corresponding x-series representations of the Puiseux series alpha = 0 if target_point == infinity else target_point px = [Pi.xseries() for Pi in P] p = [pxi for sublist in px for pxi in sublist] ramification_indices = [Pi.ramification_index for Pi in P] # reorder them according to the ordering of the y-fibre above x=x0 p_evals_above_x0 = [pj(x0-alpha) for pj in p] p_evals_above_x0 = numpy.array(p_evals_above_x0, dtype=complex) sigma = matching_permutation(p_evals_above_x0, y0) p = sigma.action(p) # also return the place that the first y-fibre element ends up analytically # continuing to px_idx = sigma[0] # index of target x-series in unsorted list place_idx = -1 # index of place corresponding to this x-series while px_idx >= 0: place_idx += 1 px_idx -= abs(ramification_indices[place_idx]) target_place = DiscriminantPlace(riemann_surface,P[place_idx]) return p, target_place
def places(self, alpha, beta=None): r"""Returns a place or places on the Riemann surface with the given x-projection and, optionally, given y-projection. Parameters ---------- alpha : complex The x-projection of the place. beta : complex If provided, will only return places with the given y-projection. There may be multiple places on the surface with the same x- and y-projections. Returns ------- places : Place or list of Places Returns all places on the Riemann surface with x-projection `alpha` or x,y-projection `(alpha, beta)`, if `beta` is provided. """ # alpha = infinity case infinities = [infinity, 'oo', numpy.Inf] if alpha in infinities: alpha = infinity p = puiseux(self.f, alpha) places = [DiscriminantPlace(self, pi) for pi in p] return places # if alpha is epsilon close to a discriminant point then set it exactly # equal to that discriminant point. there is usually no reason to # compute a puiseux series so close to a discriminant point try: alpha = QQbar(alpha) exact = True except TypeError: alpha = numpy.complex(alpha) exact = False b = self.path_factory.closest_discriminant_point(alpha,exact=exact) # if alpha is equal to or close to a discriminant point then return a # discriminant place if abs(alpha - b) < 1e-12: p = puiseux(self.f, b, beta) places = [DiscriminantPlace(self, pi) for pi in p] return places # otherwise, return a regular place if far enough away if not beta is None: curve_eval = self.f(alpha, beta) if abs(curve_eval) > 1e-8: raise ValueError('The place (%s, %s) does not lie on the curve ' '/ surface.' % (alpha, beta)) place = RegularPlace(self, alpha, beta) return place # if a beta (y-coordinate) is not specified then return all places # lying above x=alpha R = self.f.parent() x,y = R.gens() falpha = self.f(alpha,y).univariate_polynomial() yroots = falpha.roots(ring=falpha.base_ring(), multiplicities=False) places = [RegularPlace(self, alpha, beta) for beta in yroots] return places
def places(self, alpha, beta=None): r"""Returns a place or places on the Riemann surface with the given x-projection and, optionally, given y-projection. Parameters ---------- alpha : complex The x-projection of the place. beta : complex If provided, will only return places with the given y-projection. There may be multiple places on the surface with the same x- and y-projections. Returns ------- places : Place or list of Places Returns all places on the Riemann surface with x-projection `alpha` or x,y-projection `(alpha, beta)`, if `beta` is provided. """ # alpha = infinity case infinities = [infinity, 'oo', numpy.Inf] if alpha in infinities: alpha = infinity p = puiseux(self.f, alpha) places = [DiscriminantPlace(self, pi) for pi in p] return places # if alpha is epsilon close to a discriminant point then set it exactly # equal to that discriminant point. there is usually no reason to # compute a puiseux series so close to a discriminant point try: alpha = QQbar(alpha) exact = True except TypeError: alpha = numpy.complex(alpha) exact = False b = self.path_factory.closest_discriminant_point(alpha, exact=exact) # if alpha is equal to or close to a discriminant point then return a # discriminant place if abs(alpha - b) < 1e-12: p = puiseux(self.f, b, beta) places = [DiscriminantPlace(self, pi) for pi in p] return places # otherwise, return a regular place if far enough away if not beta is None: curve_eval = self.f(alpha, beta) if abs(curve_eval) > 1e-8: raise ValueError( 'The place (%s, %s) does not lie on the curve ' '/ surface.' % (alpha, beta)) place = RegularPlace(self, alpha, beta) return place # if a beta (y-coordinate) is not specified then return all places # lying above x=alpha R = self.f.parent() x, y = R.gens() falpha = self.f(alpha, y).univariate_polynomial() yroots = falpha.roots(ring=falpha.base_ring(), multiplicities=False) places = [RegularPlace(self, alpha, beta) for beta in yroots] return places
def ordered_puiseux_series(riemann_surface, complex_path, y0, target_point): r"""Returns an ordered list of Puiseux series such that each Puiseux series matches with the corresponding y-fibre element above the starting point of `complex_path`. In order to analytically continue from the regular places at the beginning of the path :math:`x=a` to the discriminant places at the end of the path :math:`x=b`we need to compute all of the `PuiseuxXSeries` at :math:`x=b`. There are two steps to this calculation: * compute enough terms of the Puiseux series centered at :math:`x=b` in order to accurately capture the y-roots at :math:`x=a`. * permute the series accordingly to match up with the y-roots at :math:`x=a`. Parameters ---------- riemann_surface : RiemannSurface The riemann surface on which all of this lives. complex_path : ComplexPath The path or path segment starting at a regular point and ending at a discriminant point. y0 : list of complex The starting fibre lying above the starting point of `complex_path`. The first component of the list indicates the starting sheet. target_point : complex The point to analytically continue to. Usually a discriminant point. Methods ------- .. autosummary:: analytically_continue Returns ------- list, Place : a list of Puiseux series and a Place A list of ordered Puiseux series corresponding to each branch above :math:`x=a` as well as the place that the first y-fibre element analytically continues to. """ # obtain all puiseux series above the target place f = riemann_surface.f x0 = CC(complex_path(0)) # XXX - need to coerce input to CC y0 = numpy.array(y0, dtype=complex) P = puiseux(f, target_point) # extend the Puiseux series to enough terms to accurately captue the # y-fibre above x=a (the starting point of the complex path) for Pi in P: Pi.extend_to_x(x0) # compute the corresponding x-series representations of the Puiseux series alpha = 0 if target_point == infinity else target_point px = [Pi.xseries() for Pi in P] p = [pxi for sublist in px for pxi in sublist] ramification_indices = [Pi.ramification_index for Pi in P] # reorder them according to the ordering of the y-fibre above x=x0 p_evals_above_x0 = [pj(x0 - alpha) for pj in p] p_evals_above_x0 = numpy.array(p_evals_above_x0, dtype=complex) sigma = matching_permutation(p_evals_above_x0, y0) p = sigma.action(p) # also return the place that the first y-fibre element ends up analytically # continuing to px_idx = sigma[0] # index of target x-series in unsorted list place_idx = -1 # index of place corresponding to this x-series while px_idx >= 0: place_idx += 1 px_idx -= abs(ramification_indices[place_idx]) target_place = DiscriminantPlace(riemann_surface, P[place_idx]) return p, target_place