From 7fba4575e05f13e992ed8a6002311d82fd8e9eb9 Mon Sep 17 00:00:00 2001 From: Oscar Benjamin Date: Wed, 12 Mar 2025 18:55:08 +0000 Subject: [PATCH] Make all polynomials handle negative powers Also add is_constant() method to all polynomial types. --- src/flint/flint_base/flint_base.pyx | 16 +++--- src/flint/test/test_all.py | 84 +++++++++++++++++++++++++++-- src/flint/types/fmpq_mpoly.pyx | 13 +++++ src/flint/types/fmpq_poly.pyx | 21 +++++++- src/flint/types/fmpz_mod_mpoly.pyx | 13 +++++ src/flint/types/fmpz_mod_poly.pyx | 3 +- src/flint/types/fmpz_mpoly.pyx | 14 +++++ src/flint/types/fmpz_poly.pyx | 29 +++++++++- src/flint/types/fq_default_poly.pyx | 3 +- src/flint/types/nmod_mpoly.pyx | 9 +++- src/flint/types/nmod_poly.pyx | 31 ++++++++++- 11 files changed, 218 insertions(+), 18 deletions(-) diff --git a/src/flint/flint_base/flint_base.pyx b/src/flint/flint_base/flint_base.pyx index 16564b12..70e4d696 100644 --- a/src/flint/flint_base/flint_base.pyx +++ b/src/flint/flint_base/flint_base.pyx @@ -6,6 +6,7 @@ from flint.flintlib.types.flint cimport ( ) from flint.utils.flint_exceptions import DomainError from flint.flintlib.types.mpoly cimport ordering_t +from flint.flintlib.functions.fmpz cimport fmpz_cmp_si from flint.flint_base.flint_context cimport thectx from flint.utils.typecheck cimport typecheck cimport libc.stdlib @@ -725,14 +726,15 @@ cdef class flint_mpoly(flint_elem): def __pow__(self, other, modulus): if modulus is not None: raise NotImplementedError("cannot specify modulus outside of the context") - elif typecheck(other, fmpz): - return self._pow_(other) - other = any_as_fmpz(other) - if other is NotImplemented: - return NotImplemented - elif other < 0: - raise ValueError("cannot raise to a negative power") + if not typecheck(other, fmpz): + other = any_as_fmpz(other) + if other is NotImplemented: + return NotImplemented + + if fmpz_cmp_si((other).val, 0) < 0: + self = 1 / self + other = -other return self._pow_(other) diff --git a/src/flint/test/test_all.py b/src/flint/test/test_all.py index eac44e83..4ed607b5 100644 --- a/src/flint/test/test_all.py +++ b/src/flint/test/test_all.py @@ -402,7 +402,7 @@ def test_fmpz_poly(): assert raises(lambda: [] // Z([1,2]), TypeError) assert raises(lambda: [] % Z([1,2]), TypeError) assert raises(lambda: divmod([], Z([1,2])), TypeError) - assert raises(lambda: Z([1,2,3]) ** -1, (OverflowError, ValueError)) + assert raises(lambda: Z([1,2,3]) ** -1, DomainError) assert raises(lambda: Z([1,2,3]) ** Z([1,2]), TypeError) assert raises(lambda: Z([1,2]) // Z([]), ZeroDivisionError) assert raises(lambda: Z([]) // Z([]), ZeroDivisionError) @@ -2109,7 +2109,7 @@ def test_fmpz_mod_poly(): assert (f + 1) // f == 1 # pow - assert raises(lambda: f**(-2), ValueError) + assert raises(lambda: f**(-2), DomainError) assert f*f == f**2 assert f*f == f**fmpz(2) @@ -2768,7 +2768,7 @@ def setbad(obj, i, val): assert P([1, 1]) ** 0 == P([1]) assert P([1, 1]) ** 1 == P([1, 1]) assert P([1, 1]) ** 2 == P([1, 2, 1]) - assert raises(lambda: P([1, 1]) ** -1, ValueError) + assert raises(lambda: P([1, 1]) ** -1, DomainError) assert raises(lambda: P([1, 1]) ** None, TypeError) # XXX: Not sure what this should do in general: @@ -3254,7 +3254,7 @@ def quick_poly(): (0, 1): 4, (0, 0): 1, }) - assert raises(lambda: P(ctx=ctx) ** -1, ValueError) + assert raises(lambda: P(ctx=ctx) ** -1, ZeroDivisionError) assert raises(lambda: P(ctx=ctx) ** None, TypeError) # # XXX: Not sure what this should do in general: @@ -3464,6 +3464,32 @@ def _all_polys_mpolys(): yield P, S, [x, y], is_field, characteristic +def test_properties_poly_mpoly(): + """Test is_zero, is_one etc for all polynomials.""" + for P, S, [x, y], is_field, characteristic in _all_polys_mpolys(): + + zero = 0*x + one = zero + 1 + two = one + 1 + + assert zero.is_zero() is True + assert one.is_zero() is False + assert two.is_zero() is False + assert x.is_zero() is False + + assert zero.is_one() is False + assert one.is_one() is True + assert two.is_one() is False + assert x.is_one() is False + + assert zero.is_constant() is True + assert one.is_constant() is True + assert two.is_constant() is True + assert x.is_constant() is False + + # is_gen? + + def test_factor_poly_mpoly(): """Test that factor() is consistent across different poly/mpoly types.""" @@ -3671,6 +3697,52 @@ def factor_sqf(p): assert (2*(x+y)).gcd(4*(x+y)**2) == x + y +def test_division_poly_mpoly(): + """Test that division is consistent across different poly/mpoly types.""" + + Z = flint.fmpz + + for P, S, [x, y], is_field, characteristic in _all_polys_mpolys(): + + if characteristic != 0 and not characteristic.is_prime(): + # nmod_poly crashes for many operations with non-prime modulus + # https://github.com/flintlib/python-flint/issues/124 + # so we can't even test it... + nmod_poly_will_crash = type(x) is flint.nmod_poly + if nmod_poly_will_crash: + continue + + one = x**0 # 1 as a polynomial + two = one + one + + if is_field or characteristic == 0: + assert x / x == x**0 == 1 == one + assert x / 1 == x / S(1) == x / one == x**1 == x + assert 1 / one == one**-1 == one**Z(-1) == 1, type(one) + assert -1 / one == 1 / -one == (-one)**-1 == (-one)**Z(-1) == -one == -1 + assert (-one) ** -2 == (-one)**Z(-2) == one + assert raises(lambda: 1 / x, DomainError) + assert raises(lambda: x ** -1, DomainError) + + if is_field: + half = S(1)/2 * one # 1/2 as a polynomial + assert half == S(1)/2 + assert x / half == 2*x + assert 1 / half == S(1) / half == one / half == one / (S(1)/2) == 2 + assert half ** -1 == half ** Z(-1) == 2 + assert two ** -1 == two ** Z(-1) == half + elif characteristic == 0: + assert raises(lambda: x / 2, DomainError) + assert raises(lambda: x / two, DomainError), characteristic + assert raises(lambda: two ** -1, DomainError) + assert raises(lambda: two ** Z(-1), DomainError) + else: + # Non-prime modulus... + # nmod can crash and fmpz_mod_poly won't crash but has awkward + # behaviour under division. + pass + + def _all_matrices(): """Return a list of matrix types and scalar types.""" R163 = flint.fmpz_mod_ctx(163) @@ -4569,7 +4641,7 @@ def test_fq_default_poly(): # pow # assert ui and fmpz exp agree for polynomials and generators R_gen = R_test.gen() - assert raises(lambda: f**(-2), ValueError) + assert raises(lambda: f**(-2), DomainError) assert pow(f, 2**60, g) == pow(pow(f, 2**30, g), 2**30, g) assert pow(R_gen, 2**60, g) == pow(pow(R_gen, 2**30, g), 2**30, g) assert raises(lambda: pow(f, -2, g), ValueError) @@ -4698,7 +4770,9 @@ def test_all_tests(): test_division_poly, test_division_matrix, + test_properties_poly_mpoly, test_factor_poly_mpoly, + test_division_poly_mpoly, test_polys, test_mpolys, diff --git a/src/flint/types/fmpq_mpoly.pyx b/src/flint/types/fmpq_mpoly.pyx index 2cc586c4..f5151941 100644 --- a/src/flint/types/fmpq_mpoly.pyx +++ b/src/flint/types/fmpq_mpoly.pyx @@ -275,6 +275,19 @@ cdef class fmpq_mpoly(flint_mpoly): def is_one(self): return fmpq_mpoly_is_one(self.val, self.ctx.val) + def is_constant(self): + """ + Returns True if this is a constant polynomial. + + >>> R = fmpq_mpoly_ctx.get(['x', 'y']) + >>> x, y = R.gens() + >>> x.is_constant() + False + >>> (0*x + 1).is_constant() + True + """ + return self.total_degree() <= 0 + def __richcmp__(self, other, int op): if not (op == Py_EQ or op == Py_NE): return NotImplemented diff --git a/src/flint/types/fmpq_poly.pyx b/src/flint/types/fmpq_poly.pyx index ff4879d4..4262dcf7 100644 --- a/src/flint/types/fmpq_poly.pyx +++ b/src/flint/types/fmpq_poly.pyx @@ -171,11 +171,29 @@ cdef class fmpq_poly(flint_poly): return not fmpq_poly_is_zero(self.val) def is_zero(self): + """ + Returns True if this is the zero polynomial. + """ return fmpq_poly_is_zero(self.val) def is_one(self): + """ + Returns True if this polynomial is equal to 1. + """ return fmpq_poly_is_one(self.val) + def is_constant(self): + """ + Returns True if this polynomial is a scalar (constant). + + >>> f = fmpq_poly([0, 1]) + >>> f + x + >>> f.is_constant() + False + """ + return fmpq_poly_degree(self.val) <= 0 + def leading_coefficient(self): """ Returns the leading coefficient of the polynomial. @@ -374,7 +392,8 @@ cdef class fmpq_poly(flint_poly): if mod is not None: raise NotImplementedError("fmpz_poly modular exponentiation") if exp < 0: - raise ValueError("fmpq_poly negative exponent") + self = 1 / self + exp = -exp res = fmpq_poly.__new__(fmpq_poly) fmpq_poly_pow(res.val, self.val, exp) return res diff --git a/src/flint/types/fmpz_mod_mpoly.pyx b/src/flint/types/fmpz_mod_mpoly.pyx index 02f99bc4..4a23fd54 100644 --- a/src/flint/types/fmpz_mod_mpoly.pyx +++ b/src/flint/types/fmpz_mod_mpoly.pyx @@ -296,6 +296,19 @@ cdef class fmpz_mod_mpoly(flint_mpoly): def is_one(self): return fmpz_mod_mpoly_is_one(self.val, self.ctx.val) + def is_constant(self): + """ + Returns True if this is a constant polynomial. + + >>> R = fmpz_mod_mpoly_ctx.get(['x', 'y'], modulus=11) + >>> x, y = R.gens() + >>> x.is_constant() + False + >>> (0*x + 1).is_constant() + True + """ + return self.total_degree() <= 0 + def __richcmp__(self, other, int op): if not (op == Py_EQ or op == Py_NE): return NotImplemented diff --git a/src/flint/types/fmpz_mod_poly.pyx b/src/flint/types/fmpz_mod_poly.pyx index 5204db35..3ab4f2bb 100644 --- a/src/flint/types/fmpz_mod_poly.pyx +++ b/src/flint/types/fmpz_mod_poly.pyx @@ -544,7 +544,8 @@ cdef class fmpz_mod_poly(flint_poly): cdef fmpz_mod_poly res if e < 0: - raise ValueError("Exponent must be non-negative") + self = 1 / self + e = -e cdef ulong e_ulong = e res = self.ctx.new_ctype_poly() diff --git a/src/flint/types/fmpz_mpoly.pyx b/src/flint/types/fmpz_mpoly.pyx index 61fce82a..716b3c19 100644 --- a/src/flint/types/fmpz_mpoly.pyx +++ b/src/flint/types/fmpz_mpoly.pyx @@ -261,6 +261,20 @@ cdef class fmpz_mpoly(flint_mpoly): def is_one(self): return fmpz_mpoly_is_one(self.val, self.ctx.val) + def is_constant(self): + """ + Returns True if this is a constant polynomial. + + >>> ctx = fmpz_mpoly_ctx.get(['x', 'y']) + >>> x, y = ctx.gens() + >>> p = x**2 + y + >>> p.is_constant() + False + >>> (0*p + 1).is_constant() + True + """ + return self.total_degree() <= 0 + def __richcmp__(self, other, int op): if not (op == Py_EQ or op == Py_NE): return NotImplemented diff --git a/src/flint/types/fmpz_poly.pyx b/src/flint/types/fmpz_poly.pyx index 7b4618c3..1c85163c 100644 --- a/src/flint/types/fmpz_poly.pyx +++ b/src/flint/types/fmpz_poly.pyx @@ -141,11 +141,36 @@ cdef class fmpz_poly(flint_poly): return not fmpz_poly_is_zero(self.val) def is_zero(self): + """ + True if this polynomial is the zero polynomial. + + >>> fmpz_poly([]).is_zero() + True + """ return fmpz_poly_is_zero(self.val) def is_one(self): + """ + True if this polynomial is equal to one. + + >>> fmpz_poly([2]).is_one() + False + """ return fmpz_poly_is_one(self.val) + def is_constant(self): + """ + True if this is a constant polynomial. + + >>> x = fmpz_poly([0, 1]) + >>> two = fmpz_poly([2]) + >>> x.is_constant() + False + >>> two.is_constant() + True + """ + return fmpz_poly_degree(self.val) <= 0 + def leading_coefficient(self): """ Returns the leading coefficient of the polynomial. @@ -348,7 +373,9 @@ cdef class fmpz_poly(flint_poly): if mod is not None: raise NotImplementedError("fmpz_poly modular exponentiation") if exp < 0: - raise ValueError("fmpz_poly negative exponent") + if not fmpz_poly_is_unit(self.val): + raise DomainError("fmpz_poly negative exponent, non-unit base") + exp = -exp res = fmpz_poly.__new__(fmpz_poly) fmpz_poly_pow(res.val, self.val, exp) return res diff --git a/src/flint/types/fq_default_poly.pyx b/src/flint/types/fq_default_poly.pyx index 8b323b57..c5dcd3bc 100644 --- a/src/flint/types/fq_default_poly.pyx +++ b/src/flint/types/fq_default_poly.pyx @@ -642,7 +642,8 @@ cdef class fq_default_poly(flint_poly): cdef fq_default_poly res if e < 0: - raise ValueError("Exponent must be non-negative") + self = 1 / self + e = -e if e == 2: return self.square() diff --git a/src/flint/types/nmod_mpoly.pyx b/src/flint/types/nmod_mpoly.pyx index c3723747..c7ec97ce 100644 --- a/src/flint/types/nmod_mpoly.pyx +++ b/src/flint/types/nmod_mpoly.pyx @@ -275,6 +275,12 @@ cdef class nmod_mpoly(flint_mpoly): def is_one(self): return nmod_mpoly_is_one(self.val, self.ctx.val) + def is_constant(self): + """ + Returns True if this is a constant polynomial. + """ + return self.total_degree() <= 0 + def __richcmp__(self, other, int op): if not (op == Py_EQ or op == Py_NE): return NotImplemented @@ -294,7 +300,8 @@ cdef class nmod_mpoly(flint_mpoly): self.ctx.val ) elif isinstance(other, int): - return (op == Py_NE) ^ nmod_mpoly_equal_ui(self.val, other, self.ctx.val) + omod = other % self.ctx.modulus() + return (op == Py_NE) ^ nmod_mpoly_equal_ui(self.val, omod, self.ctx.val) else: return NotImplemented diff --git a/src/flint/types/nmod_poly.pyx b/src/flint/types/nmod_poly.pyx index 8727d601..ea46a944 100644 --- a/src/flint/types/nmod_poly.pyx +++ b/src/flint/types/nmod_poly.pyx @@ -184,12 +184,40 @@ cdef class nmod_poly(flint_poly): return not nmod_poly_is_zero(self.val) def is_zero(self): + """ + Returns True if this is the zero polynomial. + """ return nmod_poly_is_zero(self.val) def is_one(self): + """ + Returns True if this polynomial is equal to 1. + """ return nmod_poly_is_one(self.val) + def is_constant(self): + """ + Returns True if this is a constant polynomial. + + >>> nmod_poly([0, 1], 3).is_constant() + False + >>> nmod_poly([1], 3).is_constant() + True + """ + return nmod_poly_degree(self.val) <= 0 + def is_gen(self): + """ + Returns True if this polynomial is equal to the generator x. + + >>> x = nmod_poly([0, 1], 3) + >>> x + x + >>> x.is_gen() + True + >>> (2*x).is_gen() + False + """ return nmod_poly_is_gen(self.val) def reverse(self, degree=None): @@ -498,7 +526,8 @@ cdef class nmod_poly(flint_poly): if mod is not None: return self.pow_mod(exp, mod) if exp < 0: - raise ValueError("negative exponent") + self = 1 / self + exp = -exp res = nmod_poly.__new__(nmod_poly) nmod_poly_init_preinv(res.val, (self).val.mod.n, (self).val.mod.ninv) nmod_poly_pow(res.val, self.val, exp)