""" Puiseux rings. These are used by the ring_series module to represented truncated Puiseux series. Elements of a Puiseux ring are like polynomials except that the exponents can be negative or rational rather than just non-negative integers. """ # Previously the ring_series module used PolyElement to represent Puiseux # series. This is problematic because it means that PolyElement has to support # negative and non-integer exponents which most polynomial representations do # not support. This module provides an implementation of a ring for Puiseux # series that can be used by ring_series without breaking the basic invariants # of polynomial rings. # # Ideally there would be more of a proper series type that can keep track of # not just the leading terms of a truncated series but also the precision # of the series. For now the rings here are just introduced to keep the # interface that ring_series was using before. from __future__ import annotations from sympy.polys.domains import QQ from sympy.polys.rings import PolyRing, PolyElement from sympy.core.add import Add from sympy.core.mul import Mul from sympy.external.gmpy import gcd, lcm from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Any, Unpack from sympy.core.expr import Expr from sympy.polys.domains import Domain from collections.abc import Iterable, Iterator def puiseux_ring( symbols: str | list[Expr], domain: Domain ) -> tuple[PuiseuxRing, Unpack[tuple[PuiseuxPoly, ...]]]: """Construct a Puiseux ring. This function constructs a Puiseux ring with the given symbols and domain. >>> from sympy.polys.domains import QQ >>> from sympy.polys.puiseux import puiseux_ring >>> R, x, y = puiseux_ring('x y', QQ) >>> R PuiseuxRing((x, y), QQ) >>> p = 5*x**QQ(1,2) + 7/y >>> p 7*y**(-1) + 5*x**(1/2) """ ring = PuiseuxRing(symbols, domain) return (ring,) + ring.gens # type: ignore class PuiseuxRing: """Ring of Puiseux polynomials. A Puiseux polynomial is a truncated Puiseux series. The exponents of the monomials can be negative or rational numbers. This ring is used by the ring_series module: >>> from sympy.polys.domains import QQ >>> from sympy.polys.puiseux import puiseux_ring >>> from sympy.polys.ring_series import rs_exp, rs_nth_root >>> ring, x, y = puiseux_ring('x y', QQ) >>> f = x**2 + y**3 >>> f y**3 + x**2 >>> f.diff(x) 2*x >>> rs_exp(x, x, 5) 1 + x + 1/2*x**2 + 1/6*x**3 + 1/24*x**4 Importantly the Puiseux ring can represent truncated series with negative and fractional exponents: >>> f = 1/x + 1/y**2 >>> f x**(-1) + y**(-2) >>> f.diff(x) -1*x**(-2) >>> rs_nth_root(8*x + x**2 + x**3, 3, x, 5) 2*x**(1/3) + 1/12*x**(4/3) + 23/288*x**(7/3) + -139/20736*x**(10/3) See Also ======== sympy.polys.ring_series.rs_series PuiseuxPoly """ def __init__(self, symbols: str | list[Expr], domain: Domain): poly_ring = PolyRing(symbols, domain) domain = poly_ring.domain ngens = poly_ring.ngens self.poly_ring = poly_ring self.domain = domain self.symbols = poly_ring.symbols self.gens = tuple([self.from_poly(g) for g in poly_ring.gens]) self.ngens = ngens self.zero = self.from_poly(poly_ring.zero) self.one = self.from_poly(poly_ring.one) self.zero_monom = poly_ring.zero_monom # type: ignore self.monomial_mul = poly_ring.monomial_mul # type: ignore def __repr__(self) -> str: return f"PuiseuxRing({self.symbols}, {self.domain})" def __eq__(self, other: Any) -> bool: if not isinstance(other, PuiseuxRing): return NotImplemented return self.symbols == other.symbols and self.domain == other.domain def from_poly(self, poly: PolyElement) -> PuiseuxPoly: """Create a Puiseux polynomial from a polynomial. >>> from sympy.polys.domains import QQ >>> from sympy.polys.rings import ring >>> from sympy.polys.puiseux import puiseux_ring >>> R1, x1 = ring('x', QQ) >>> R2, x2 = puiseux_ring('x', QQ) >>> R2.from_poly(x1**2) x**2 """ return PuiseuxPoly(poly, self) def from_dict(self, terms: dict[tuple[int, ...], Any]) -> PuiseuxPoly: """Create a Puiseux polynomial from a dictionary of terms. >>> from sympy.polys.domains import QQ >>> from sympy.polys.puiseux import puiseux_ring >>> R, x = puiseux_ring('x', QQ) >>> R.from_dict({(QQ(1,2),): QQ(3)}) 3*x**(1/2) """ return PuiseuxPoly.from_dict(terms, self) def from_int(self, n: int) -> PuiseuxPoly: """Create a Puiseux polynomial from an integer. >>> from sympy.polys.domains import QQ >>> from sympy.polys.puiseux import puiseux_ring >>> R, x = puiseux_ring('x', QQ) >>> R.from_int(3) 3 """ return self.from_poly(self.poly_ring(n)) def domain_new(self, arg: Any) -> Any: """Create a new element of the domain. >>> from sympy.polys.domains import QQ >>> from sympy.polys.puiseux import puiseux_ring >>> R, x = puiseux_ring('x', QQ) >>> R.domain_new(3) 3 >>> QQ.of_type(_) True """ return self.poly_ring.domain_new(arg) def ground_new(self, arg: Any) -> PuiseuxPoly: """Create a new element from a ground element. >>> from sympy.polys.domains import QQ >>> from sympy.polys.puiseux import puiseux_ring, PuiseuxPoly >>> R, x = puiseux_ring('x', QQ) >>> R.ground_new(3) 3 >>> isinstance(_, PuiseuxPoly) True """ return self.from_poly(self.poly_ring.ground_new(arg)) def __call__(self, arg: Any) -> PuiseuxPoly: """Coerce an element into the ring. >>> from sympy.polys.domains import QQ >>> from sympy.polys.puiseux import puiseux_ring >>> R, x = puiseux_ring('x', QQ) >>> R(3) 3 >>> R({(QQ(1,2),): QQ(3)}) 3*x**(1/2) """ if isinstance(arg, dict): return self.from_dict(arg) else: return self.from_poly(self.poly_ring(arg)) def index(self, x: PuiseuxPoly) -> int: """Return the index of a generator. >>> from sympy.polys.domains import QQ >>> from sympy.polys.puiseux import puiseux_ring >>> R, x, y = puiseux_ring('x y', QQ) >>> R.index(x) 0 >>> R.index(y) 1 """ return self.gens.index(x) def _div_poly_monom(poly: PolyElement, monom: Iterable[int]) -> PolyElement: ring = poly.ring div = ring.monomial_div return ring.from_dict({div(m, monom): c for m, c in poly.terms()}) def _mul_poly_monom(poly: PolyElement, monom: Iterable[int]) -> PolyElement: ring = poly.ring mul = ring.monomial_mul return ring.from_dict({mul(m, monom): c for m, c in poly.terms()}) def _div_monom(monom: Iterable[int], div: Iterable[int]) -> tuple[int, ...]: return tuple(mi - di for mi, di in zip(monom, div)) class PuiseuxPoly: """Puiseux polynomial. Represents a truncated Puiseux series. See the :class:`PuiseuxRing` class for more information. >>> from sympy import QQ >>> from sympy.polys.puiseux import puiseux_ring >>> R, x, y = puiseux_ring('x, y', QQ) >>> p = 5*x**2 + 7*y**3 >>> p 7*y**3 + 5*x**2 The internal representation of a Puiseux polynomial wraps a normal polynomial. To support negative powers the polynomial is considered to be divided by a monomial. >>> p2 = 1/x + 1/y**2 >>> p2.monom # x*y**2 (1, 2) >>> p2.poly x + y**2 >>> (y**2 + x) / (x*y**2) == p2 True To support fractional powers the polynomial is considered to be a function of ``x**(1/nx), y**(1/ny), ...``. The representation keeps track of a monomial and a list of exponent denominators so that the polynomial can be used to represent both negative and fractional powers. >>> p3 = x**QQ(1,2) + y**QQ(2,3) >>> p3.ns (2, 3) >>> p3.poly x + y**2 See Also ======== sympy.polys.puiseux.PuiseuxRing sympy.polys.rings.PolyElement """ ring: PuiseuxRing poly: PolyElement monom: tuple[int, ...] | None ns: tuple[int, ...] | None def __new__(cls, poly: PolyElement, ring: PuiseuxRing) -> PuiseuxPoly: return cls._new(ring, poly, None, None) @classmethod def _new( cls, ring: PuiseuxRing, poly: PolyElement, monom: tuple[int, ...] | None, ns: tuple[int, ...] | None, ) -> PuiseuxPoly: poly, monom, ns = cls._normalize(poly, monom, ns) return cls._new_raw(ring, poly, monom, ns) @classmethod def _new_raw( cls, ring: PuiseuxRing, poly: PolyElement, monom: tuple[int, ...] | None, ns: tuple[int, ...] | None, ) -> PuiseuxPoly: obj = object.__new__(cls) obj.ring = ring obj.poly = poly obj.monom = monom obj.ns = ns return obj def __eq__(self, other: Any) -> bool: if isinstance(other, PuiseuxPoly): return ( self.poly == other.poly and self.monom == other.monom and self.ns == other.ns ) elif self.monom is None and self.ns is None: return self.poly.__eq__(other) else: return NotImplemented @classmethod def _normalize( cls, poly: PolyElement, monom: tuple[int, ...] | None, ns: tuple[int, ...] | None, ) -> tuple[PolyElement, tuple[int, ...] | None, tuple[int, ...] | None]: if monom is None and ns is None: return poly, None, None if monom is not None: degs = [max(d, 0) for d in poly.tail_degrees()] if all(di >= mi for di, mi in zip(degs, monom)): poly = _div_poly_monom(poly, monom) monom = None elif any(degs): poly = _div_poly_monom(poly, degs) monom = _div_monom(monom, degs) if ns is not None: factors_d, [poly_d] = poly.deflate() degrees = poly.degrees() monom_d = monom if monom is not None else [0] * len(degrees) ns_new = [] monom_new = [] inflations = [] for fi, ni, di, mi in zip(factors_d, ns, degrees, monom_d): if di == 0: g = gcd(ni, mi) else: g = gcd(fi, ni, mi) ns_new.append(ni // g) monom_new.append(mi // g) inflations.append(fi // g) if any(infl > 1 for infl in inflations): poly_d = poly_d.inflate(inflations) poly = poly_d if monom is not None: monom = tuple(monom_new) if all(n == 1 for n in ns_new): ns = None else: ns = tuple(ns_new) return poly, monom, ns @classmethod def _monom_fromint( cls, monom: tuple[int, ...], dmonom: tuple[int, ...] | None, ns: tuple[int, ...] | None, ) -> tuple[Any, ...]: if dmonom is not None and ns is not None: return tuple(QQ(mi - di, ni) for mi, di, ni in zip(monom, dmonom, ns)) elif dmonom is not None: return tuple(QQ(mi - di) for mi, di in zip(monom, dmonom)) elif ns is not None: return tuple(QQ(mi, ni) for mi, ni in zip(monom, ns)) else: return tuple(QQ(mi) for mi in monom) @classmethod def _monom_toint( cls, monom: tuple[Any, ...], dmonom: tuple[int, ...] | None, ns: tuple[int, ...] | None, ) -> tuple[int, ...]: if dmonom is not None and ns is not None: return tuple( int((mi * ni).numerator + di) for mi, di, ni in zip(monom, dmonom, ns) ) elif dmonom is not None: return tuple(int(mi.numerator + di) for mi, di in zip(monom, dmonom)) elif ns is not None: return tuple(int((mi * ni).numerator) for mi, ni in zip(monom, ns)) else: return tuple(int(mi.numerator) for mi in monom) def itermonoms(self) -> Iterator[tuple[Any, ...]]: """Iterate over the monomials of a Puiseux polynomial. >>> from sympy import QQ >>> from sympy.polys.puiseux import puiseux_ring >>> R, x, y = puiseux_ring('x, y', QQ) >>> p = 5*x**2 + 7*y**3 >>> list(p.itermonoms()) [(2, 0), (0, 3)] >>> p[(2, 0)] 5 """ monom, ns = self.monom, self.ns for m in self.poly.itermonoms(): yield self._monom_fromint(m, monom, ns) def monoms(self) -> list[tuple[Any, ...]]: """Return a list of the monomials of a Puiseux polynomial.""" return list(self.itermonoms()) def __iter__(self) -> Iterator[tuple[tuple[Any, ...], Any]]: return self.itermonoms() def __getitem__(self, monom: tuple[int, ...]) -> Any: monom = self._monom_toint(monom, self.monom, self.ns) return self.poly[monom] def __len__(self) -> int: return len(self.poly) def iterterms(self) -> Iterator[tuple[tuple[Any, ...], Any]]: """Iterate over the terms of a Puiseux polynomial. >>> from sympy import QQ >>> from sympy.polys.puiseux import puiseux_ring >>> R, x, y = puiseux_ring('x, y', QQ) >>> p = 5*x**2 + 7*y**3 >>> list(p.iterterms()) [((2, 0), 5), ((0, 3), 7)] """ monom, ns = self.monom, self.ns for m, coeff in self.poly.iterterms(): mq = self._monom_fromint(m, monom, ns) yield mq, coeff def terms(self) -> list[tuple[tuple[Any, ...], Any]]: """Return a list of the terms of a Puiseux polynomial.""" return list(self.iterterms()) @property def is_term(self) -> bool: """Return True if the Puiseux polynomial is a single term.""" return self.poly.is_term def to_dict(self) -> dict[tuple[int, ...], Any]: """Return a dictionary representation of a Puiseux polynomial.""" return dict(self.iterterms()) @classmethod def from_dict( cls, terms: dict[tuple[Any, ...], Any], ring: PuiseuxRing ) -> PuiseuxPoly: """Create a Puiseux polynomial from a dictionary of terms. >>> from sympy import QQ >>> from sympy.polys.puiseux import puiseux_ring, PuiseuxPoly >>> R, x = puiseux_ring('x', QQ) >>> PuiseuxPoly.from_dict({(QQ(1,2),): QQ(3)}, R) 3*x**(1/2) >>> R.from_dict({(QQ(1,2),): QQ(3)}) 3*x**(1/2) """ ns = [1] * ring.ngens mon = [0] * ring.ngens for mo in terms: ns = [lcm(n, m.denominator) for n, m in zip(ns, mo)] mon = [min(m, n) for m, n in zip(mo, mon)] if not any(mon): monom = None else: monom = tuple(-int((m * n).numerator) for m, n in zip(mon, ns)) if all(n == 1 for n in ns): ns_final = None else: ns_final = tuple(ns) terms_p = {cls._monom_toint(m, monom, ns_final): coeff for m, coeff in terms.items()} poly = ring.poly_ring.from_dict(terms_p) return cls._new(ring, poly, monom, ns_final) def as_expr(self) -> Expr: """Convert a Puiseux polynomial to :class:`~sympy.core.expr.Expr`. >>> from sympy import QQ, Expr >>> from sympy.polys.puiseux import puiseux_ring >>> R, x = puiseux_ring('x', QQ) >>> p = 5*x**2 + 7*x**3 >>> p.as_expr() 7*x**3 + 5*x**2 >>> isinstance(_, Expr) True """ ring = self.ring dom = ring.domain symbols = ring.symbols terms = [] for monom, coeff in self.iterterms(): coeff_expr = dom.to_sympy(coeff) monoms_expr = [] for i, m in enumerate(monom): monoms_expr.append(symbols[i] ** m) terms.append(Mul(coeff_expr, *monoms_expr)) return Add(*terms) def __repr__(self) -> str: def format_power(base: str, exp: int) -> str: if exp == 1: return base elif exp >= 0 and int(exp) == exp: return f"{base}**{exp}" else: return f"{base}**({exp})" ring = self.ring dom = ring.domain syms = [str(s) for s in ring.symbols] terms_str = [] for monom, coeff in sorted(self.terms()): monom_str = "*".join(format_power(s, e) for s, e in zip(syms, monom) if e) if coeff == dom.one: if monom_str: terms_str.append(monom_str) else: terms_str.append("1") elif not monom_str: terms_str.append(str(coeff)) else: terms_str.append(f"{coeff}*{monom_str}") return " + ".join(terms_str) def _unify( self, other: PuiseuxPoly ) -> tuple[ PolyElement, PolyElement, tuple[int, ...] | None, tuple[int, ...] | None ]: """Bring two Puiseux polynomials to a common monom and ns.""" poly1, monom1, ns1 = self.poly, self.monom, self.ns poly2, monom2, ns2 = other.poly, other.monom, other.ns if monom1 == monom2 and ns1 == ns2: return poly1, poly2, monom1, ns1 if ns1 == ns2: ns = ns1 elif ns1 is not None and ns2 is not None: ns = tuple(lcm(n1, n2) for n1, n2 in zip(ns1, ns2)) f1 = [n // n1 for n, n1 in zip(ns, ns1)] f2 = [n // n2 for n, n2 in zip(ns, ns2)] poly1 = poly1.inflate(f1) poly2 = poly2.inflate(f2) if monom1 is not None: monom1 = tuple(m * f for m, f in zip(monom1, f1)) if monom2 is not None: monom2 = tuple(m * f for m, f in zip(monom2, f2)) elif ns2 is not None: ns = ns2 poly1 = poly1.inflate(ns) if monom1 is not None: monom1 = tuple(m * n for m, n in zip(monom1, ns)) elif ns1 is not None: ns = ns1 poly2 = poly2.inflate(ns) if monom2 is not None: monom2 = tuple(m * n for m, n in zip(monom2, ns)) else: assert False if monom1 == monom2: monom = monom1 elif monom1 is not None and monom2 is not None: monom = tuple(max(m1, m2) for m1, m2 in zip(monom1, monom2)) poly1 = _mul_poly_monom(poly1, _div_monom(monom, monom1)) poly2 = _mul_poly_monom(poly2, _div_monom(monom, monom2)) elif monom2 is not None: monom = monom2 poly1 = _mul_poly_monom(poly1, monom2) elif monom1 is not None: monom = monom1 poly2 = _mul_poly_monom(poly2, monom1) else: assert False return poly1, poly2, monom, ns def __pos__(self) -> PuiseuxPoly: return self def __neg__(self) -> PuiseuxPoly: return self._new_raw(self.ring, -self.poly, self.monom, self.ns) def __add__(self, other: Any) -> PuiseuxPoly: if isinstance(other, PuiseuxPoly): if self.ring != other.ring: raise ValueError("Cannot add Puiseux polynomials from different rings") return self._add(other) domain = self.ring.domain if isinstance(other, int): return self._add_ground(domain.convert_from(QQ(other), QQ)) elif domain.of_type(other): return self._add_ground(other) else: return NotImplemented def __radd__(self, other: Any) -> PuiseuxPoly: domain = self.ring.domain if isinstance(other, int): return self._add_ground(domain.convert_from(QQ(other), QQ)) elif domain.of_type(other): return self._add_ground(other) else: return NotImplemented def __sub__(self, other: Any) -> PuiseuxPoly: if isinstance(other, PuiseuxPoly): if self.ring != other.ring: raise ValueError( "Cannot subtract Puiseux polynomials from different rings" ) return self._sub(other) domain = self.ring.domain if isinstance(other, int): return self._sub_ground(domain.convert_from(QQ(other), QQ)) elif domain.of_type(other): return self._sub_ground(other) else: return NotImplemented def __rsub__(self, other: Any) -> PuiseuxPoly: domain = self.ring.domain if isinstance(other, int): return self._rsub_ground(domain.convert_from(QQ(other), QQ)) elif domain.of_type(other): return self._rsub_ground(other) else: return NotImplemented def __mul__(self, other: Any) -> PuiseuxPoly: if isinstance(other, PuiseuxPoly): if self.ring != other.ring: raise ValueError( "Cannot multiply Puiseux polynomials from different rings" ) return self._mul(other) domain = self.ring.domain if isinstance(other, int): return self._mul_ground(domain.convert_from(QQ(other), QQ)) elif domain.of_type(other): return self._mul_ground(other) else: return NotImplemented def __rmul__(self, other: Any) -> PuiseuxPoly: domain = self.ring.domain if isinstance(other, int): return self._mul_ground(domain.convert_from(QQ(other), QQ)) elif domain.of_type(other): return self._mul_ground(other) else: return NotImplemented def __pow__(self, other: Any) -> PuiseuxPoly: if isinstance(other, int): if other >= 0: return self._pow_pint(other) else: return self._pow_nint(-other) elif QQ.of_type(other): return self._pow_rational(other) else: return NotImplemented def __truediv__(self, other: Any) -> PuiseuxPoly: if isinstance(other, PuiseuxPoly): if self.ring != other.ring: raise ValueError( "Cannot divide Puiseux polynomials from different rings" ) return self._mul(other._inv()) domain = self.ring.domain if isinstance(other, int): return self._mul_ground(domain.convert_from(QQ(1, other), QQ)) elif domain.of_type(other): return self._div_ground(other) else: return NotImplemented def __rtruediv__(self, other: Any) -> PuiseuxPoly: if isinstance(other, int): return self._inv()._mul_ground(self.ring.domain.convert_from(QQ(other), QQ)) elif self.ring.domain.of_type(other): return self._inv()._mul_ground(other) else: return NotImplemented def _add(self, other: PuiseuxPoly) -> PuiseuxPoly: poly1, poly2, monom, ns = self._unify(other) return self._new(self.ring, poly1 + poly2, monom, ns) def _add_ground(self, ground: Any) -> PuiseuxPoly: return self._add(self.ring.ground_new(ground)) def _sub(self, other: PuiseuxPoly) -> PuiseuxPoly: poly1, poly2, monom, ns = self._unify(other) return self._new(self.ring, poly1 - poly2, monom, ns) def _sub_ground(self, ground: Any) -> PuiseuxPoly: return self._sub(self.ring.ground_new(ground)) def _rsub_ground(self, ground: Any) -> PuiseuxPoly: return self.ring.ground_new(ground)._sub(self) def _mul(self, other: PuiseuxPoly) -> PuiseuxPoly: poly1, poly2, monom, ns = self._unify(other) if monom is not None: monom = tuple(2 * e for e in monom) return self._new(self.ring, poly1 * poly2, monom, ns) def _mul_ground(self, ground: Any) -> PuiseuxPoly: return self._new_raw(self.ring, self.poly * ground, self.monom, self.ns) def _div_ground(self, ground: Any) -> PuiseuxPoly: return self._new_raw(self.ring, self.poly / ground, self.monom, self.ns) def _pow_pint(self, n: int) -> PuiseuxPoly: assert n >= 0 monom = self.monom if monom is not None: monom = tuple(m * n for m in monom) return self._new(self.ring, self.poly**n, monom, self.ns) def _pow_nint(self, n: int) -> PuiseuxPoly: return self._inv()._pow_pint(n) def _pow_rational(self, n: Any) -> PuiseuxPoly: if not self.is_term: raise ValueError("Only monomials can be raised to a rational power") [(monom, coeff)] = self.terms() domain = self.ring.domain if not domain.is_one(coeff): raise ValueError("Only monomials can be raised to a rational power") monom = tuple(m * n for m in monom) return self.ring.from_dict({monom: domain.one}) def _inv(self) -> PuiseuxPoly: if not self.is_term: raise ValueError("Only terms can be inverted") [(monom, coeff)] = self.terms() domain = self.ring.domain if not domain.is_Field and not domain.is_one(coeff): raise ValueError("Cannot invert non-unit coefficient") monom = tuple(-m for m in monom) coeff = 1 / coeff return self.ring.from_dict({monom: coeff}) def diff(self, x: PuiseuxPoly) -> PuiseuxPoly: """Differentiate a Puiseux polynomial with respect to a variable. >>> from sympy import QQ >>> from sympy.polys.puiseux import puiseux_ring >>> R, x, y = puiseux_ring('x, y', QQ) >>> p = 5*x**2 + 7*y**3 >>> p.diff(x) 10*x >>> p.diff(y) 21*y**2 """ ring = self.ring i = ring.index(x) g = {} for expv, coeff in self.iterterms(): n = expv[i] if n: e = list(expv) e[i] -= 1 g[tuple(e)] = coeff * n return ring(g)