From 483f5cfff0cac885b67b8709f3e0064244c8c1b8 Mon Sep 17 00:00:00 2001 From: Chenxin Zhong Date: Mon, 15 Dec 2025 13:08:14 +0800 Subject: [PATCH 1/4] Implement square check for power series over Z/nZ Add Chinese Remainder Theorem support for checking squares in power series over squarefree moduli. --- src/sage/rings/power_series_ring_element.pyx | 95 ++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/src/sage/rings/power_series_ring_element.pyx b/src/sage/rings/power_series_ring_element.pyx index fc456150e23..8c5443b5e9a 100644 --- a/src/sage/rings/power_series_ring_element.pyx +++ b/src/sage/rings/power_series_ring_element.pyx @@ -1575,6 +1575,10 @@ cdef class PowerSeries(AlgebraElement): fraction field and tests whether or not the result lies in the original ring. + For power series over `\ZZ/n\ZZ` where `n` is squarefree (product of + distinct primes), the Chinese Remainder Theorem is used to reduce to + the case of prime fields. + EXAMPLES:: sage: K. = PowerSeriesRing(QQ, 't', 5) @@ -1592,7 +1596,32 @@ cdef class PowerSeries(AlgebraElement): sage: f = (1+t)^100 sage: f.is_square() True + + Power series over `\ZZ/n\ZZ` where `n` is squarefree:: + + sage: R. = PowerSeriesRing(Zmod(6)) + sage: ((x + 1)^2).is_square() + True + sage: R. = PowerSeriesRing(Zmod(15)) + sage: ((x + 2)^2).is_square() + True + sage: (5 + x).is_square() + False + + For prime power moduli, a :exc:`NotImplementedError` is raised:: + + sage: R. = PowerSeriesRing(Zmod(8)) + sage: ((x + 1)^2).is_square() + Traceback (most recent call last): + ... + NotImplementedError: is_square() not implemented for power series over Zmod(p^k) with k > 1 """ + import sage.rings.abc + + # Zero is always a square + if self.is_zero(): + return True + val = self.valuation() if val is not infinity and val % 2 == 1: return False @@ -1600,6 +1629,9 @@ cdef class PowerSeries(AlgebraElement): return False elif self.base_ring() in _Fields: return True + # Check if base ring is IntegerModRing + elif isinstance(self.base_ring(), sage.rings.abc.IntegerModRing): + return self._is_square_crt() else: try: self.parent()(self.sqrt()) @@ -1607,6 +1639,69 @@ cdef class PowerSeries(AlgebraElement): except TypeError: return False + def _is_square_crt(self): + r""" + Check if this power series is a square over `\ZZ/n\ZZ` using CRT. + + This method works for squarefree moduli (products of distinct primes). + For prime power moduli with exponent > 1, it raises NotImplementedError. + + EXAMPLES:: + + sage: R. = PowerSeriesRing(Zmod(6)) + sage: ((x + 1)^2)._is_square_crt() + True + sage: (5 + x)._is_square_crt() + False + sage: R. = PowerSeriesRing(Zmod(15)) + sage: ((2*x + 3)^2)._is_square_crt() + True + + The zero element is always a square:: + + sage: R. = PowerSeriesRing(Zmod(6)) + sage: R(0)._is_square_crt() + True + + Elements that reduce to zero modulo some prime factor:: + + sage: R. = PowerSeriesRing(Zmod(6)) + sage: R(4)._is_square_crt() + True + """ + from sage.rings.finite_rings.integer_mod_ring import IntegerModRing + + base = self.base_ring() + n = base.order() + + # Factor n + factorization = n.factor() + + # Check if any prime power has exponent > 1 + for p, e in factorization: + if e > 1: + raise NotImplementedError( + "is_square() not implemented for power series over Zmod(p^k) with k > 1" + ) + + # n is squarefree - use CRT + # For each prime p, reduce mod p and check if square + primes = [p for p, e in factorization] + + for p in primes: + Zp = IntegerModRing(p) + Rp = self.parent().change_ring(Zp) + # Map self to Rp + fp = Rp([Zp(c) for c in self.list()]).add_bigoh(self.prec()) + # Zero is always a square + if fp.is_zero(): + continue + # Check if square over the prime field + if not fp.is_square(): + return False + + return True + def sqrt(self, prec=None, extend=False, all=False, name=None): r""" Return a square root of ``self``. From 41d36be529ee2983be3aa99b3938cdf3f90c63b6 Mon Sep 17 00:00:00 2001 From: Chenxin Zhong Date: Tue, 16 Dec 2025 16:57:08 +0800 Subject: [PATCH 2/4] Modify radical function to handle input of 0 Change radical(0) to return 0 instead of raising an error. --- src/sage/arith/misc.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/sage/arith/misc.py b/src/sage/arith/misc.py index 9fe86a04c6b..6969cf26cc5 100644 --- a/src/sage/arith/misc.py +++ b/src/sage/arith/misc.py @@ -2775,9 +2775,7 @@ def radical(n, *args, **kwds): sage: radical(2 * 3^2 * 5^5) 30 sage: radical(0) - Traceback (most recent call last): - ... - ArithmeticError: radical of 0 is not defined + 0 sage: K. = QuadraticField(-1) # needs sage.rings.number_field sage: radical(K(2)) # needs sage.rings.number_field i - 1 From 3f578b2287a821786eaa7a9430c0feed9011c5cb Mon Sep 17 00:00:00 2001 From: Chenxin Zhong Date: Tue, 16 Dec 2025 17:01:55 +0800 Subject: [PATCH 3/4] Modify radical method to handle zero input Updated the radical method to return 0 for input 0 instead of raising an error. --- src/sage/categories/unique_factorization_domains.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/sage/categories/unique_factorization_domains.py b/src/sage/categories/unique_factorization_domains.py index a9704872d84..d0af431e855 100644 --- a/src/sage/categories/unique_factorization_domains.py +++ b/src/sage/categories/unique_factorization_domains.py @@ -262,9 +262,7 @@ def radical(self, *args, **kwds): sage: Integer(-100).radical() 10 sage: Integer(0).radical() - Traceback (most recent call last): - ... - ArithmeticError: radical of 0 is not defined + 0 The next example shows how to compute the radical of a number, assuming no prime > 100000 has exponent > 1 in the factorization:: @@ -281,7 +279,7 @@ def radical(self, *args, **kwds): 10 """ if self.is_zero(): - raise ArithmeticError("radical of 0 is not defined") + return self try: decomp = self.squarefree_decomposition() except AttributeError: From b427964e11070b00cce3538169f95371197d3dee Mon Sep 17 00:00:00 2001 From: Chenxin Zhong Date: Tue, 16 Dec 2025 20:50:24 +0800 Subject: [PATCH 4/4] Refactor is_square method for power series --- src/sage/rings/power_series_ring_element.pyx | 66 +++++++++++++++----- 1 file changed, 52 insertions(+), 14 deletions(-) diff --git a/src/sage/rings/power_series_ring_element.pyx b/src/sage/rings/power_series_ring_element.pyx index 8c5443b5e9a..5fd8217bbc2 100644 --- a/src/sage/rings/power_series_ring_element.pyx +++ b/src/sage/rings/power_series_ring_element.pyx @@ -1575,10 +1575,6 @@ cdef class PowerSeries(AlgebraElement): fraction field and tests whether or not the result lies in the original ring. - For power series over `\ZZ/n\ZZ` where `n` is squarefree (product of - distinct primes), the Chinese Remainder Theorem is used to reduce to - the case of prime fields. - EXAMPLES:: sage: K. = PowerSeriesRing(QQ, 't', 5) @@ -1597,24 +1593,38 @@ cdef class PowerSeries(AlgebraElement): sage: f.is_square() True - Power series over `\ZZ/n\ZZ` where `n` is squarefree:: + Over a ring with nilpotent elements, odd valuation is possible for squares:: + + sage: R. = PowerSeriesRing(Zmod(4)) + sage: (2*x).is_square() # (2*x)^2 has odd valuation but 2^2 = 0 mod 4 + Traceback (most recent call last): + ... + NotImplementedError: is_square() not implemented for power series over rings with nonzero nilradical + + Power series over `\Zmod{n}` where `n` is squarefree:: sage: R. = PowerSeriesRing(Zmod(6)) sage: ((x + 1)^2).is_square() True + sage: (x^2).is_square() + True + sage: R(0).is_square() + True + sage: R(4).is_square() + True sage: R. = PowerSeriesRing(Zmod(15)) sage: ((x + 2)^2).is_square() True sage: (5 + x).is_square() False - For prime power moduli, a :exc:`NotImplementedError` is raised:: + For prime power moduli with exponent > 1, a :exc:`NotImplementedError` is raised:: sage: R. = PowerSeriesRing(Zmod(8)) sage: ((x + 1)^2).is_square() Traceback (most recent call last): ... - NotImplementedError: is_square() not implemented for power series over Zmod(p^k) with k > 1 + NotImplementedError: is_square() not implemented for power series over rings with nonzero nilradical """ import sage.rings.abc @@ -1624,12 +1634,27 @@ cdef class PowerSeries(AlgebraElement): val = self.valuation() if val is not infinity and val % 2 == 1: + # Odd valuation can only happen for a square if the leading coefficient + # squares to zero, i.e., the base ring has nonzero nilradical. + # Check if the base ring has zero nilradical. + try: + nilrad = self.base_ring().nilradical() + if not nilrad.is_zero(): + raise NotImplementedError( + "is_square() not implemented for power series over rings with nonzero nilradical" + ) + except (AttributeError, NotImplementedError, ArithmeticError): + # If nilradical() is not available, try to check if leading coeff^2 = 0 + if self[val]**2 == 0: + raise NotImplementedError( + "is_square() not implemented for power series over rings with nonzero nilradical" + ) return False elif not self[val].is_square(): return False elif self.base_ring() in _Fields: return True - # Check if base ring is IntegerModRing + # Check if base ring is IntegerModRing - use CRT for squarefree moduli elif isinstance(self.base_ring(), sage.rings.abc.IntegerModRing): return self._is_square_crt() else: @@ -1641,7 +1666,7 @@ cdef class PowerSeries(AlgebraElement): def _is_square_crt(self): r""" - Check if this power series is a square over `\ZZ/n\ZZ` using CRT. + Check if this power series is a square over `\Zmod{n}` using CRT. This method works for squarefree moduli (products of distinct primes). For prime power moduli with exponent > 1, it raises NotImplementedError. @@ -1657,20 +1682,36 @@ cdef class PowerSeries(AlgebraElement): sage: ((2*x + 3)^2)._is_square_crt() True + Elements with even valuation:: + + sage: R. = PowerSeriesRing(Zmod(6)) + sage: (x^2)._is_square_crt() + True + sage: (4*x^2)._is_square_crt() + True + The zero element is always a square:: sage: R. = PowerSeriesRing(Zmod(6)) sage: R(0)._is_square_crt() True - Elements that reduce to zero modulo some prime factor:: + Constant elements:: sage: R. = PowerSeriesRing(Zmod(6)) sage: R(4)._is_square_crt() True + sage: R(3)._is_square_crt() + True + sage: R(2)._is_square_crt() + False """ from sage.rings.finite_rings.integer_mod_ring import IntegerModRing + # Zero is always a square + if self.is_zero(): + return True + base = self.base_ring() n = base.order() @@ -1681,7 +1722,7 @@ cdef class PowerSeries(AlgebraElement): for p, e in factorization: if e > 1: raise NotImplementedError( - "is_square() not implemented for power series over Zmod(p^k) with k > 1" + "is_square() not implemented for power series over rings with nonzero nilradical" ) # n is squarefree - use CRT @@ -1693,9 +1734,6 @@ cdef class PowerSeries(AlgebraElement): Rp = self.parent().change_ring(Zp) # Map self to Rp fp = Rp([Zp(c) for c in self.list()]).add_bigoh(self.prec()) - # Zero is always a square - if fp.is_zero(): - continue # Check if square over the prime field if not fp.is_square(): return False