-
Notifications
You must be signed in to change notification settings - Fork 27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make fmpq_poly.factor() return primitive factors #189
Make fmpq_poly.factor() return primitive factors #189
Conversation
@GiacomoPope @Jake-Moss does either of you want to review this? The main change here is that In [7]: from flint import *
In [8]: x = fmpq_poly([0, 1])
In [9]: p = (x + fmpq(1,2))*(x - fmpq(1,4))/7
In [10]: p
Out[10]: 1/7*x^2 + 1/28*x + (-1/56)
In [11]: p.factor()
Out[11]: (1/56, [(4*x + (-1), 1), (2*x + 1, 1)]) This makes I also found some other issues:
There were inconsistent types in some places e.g. The The mpoly types need to have a For In [6]: p = fq_default_ctx(163)
In [7]: x = p(1)
In [8]: x.ctx
---------------------------------------------------------------------------
AttributeError I don't see any way to get the context, characteristic, etc from The Also I have emphasised this a lot but let me just point out again that a carefully designed test like the |
Is there a particular reason for this? Does something depend on this behaviour? It seems like an unnecessary conversion to me, any use with another
Yes they will, I'll add those pending the above |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Other than my two comments it looks good to me
src/flint/types/fmpz_mod_poly.pyx
Outdated
@@ -1798,6 +1798,20 @@ cdef class fmpz_mod_poly(flint_poly): | |||
if not self.ctx.is_prime(): | |||
raise NotImplementedError("factor algorithm assumes that the modulus is prime") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new fmpz_mod_mpoly
and nmod_mpoly
are inconsistent with this exception type, they raise a DomainError
instead. Seems like this isn't consistent across the code base, we have DomainError
, NotImplementedError
, and ValueError
for similar exception reasons. What should the preference here?
Exported grep results:
src/flint/types/nmod_mpoly.pyx:312: raise DomainError("division with non-prime modulus is not supported")
src/flint/types/nmod_mpoly.pyx:759: raise DomainError("gcd with non-prime modulus is not supported")
src/flint/types/nmod_mpoly.pyx:780: raise DomainError("square root with non-prime modulus is not supported")
src/flint/types/nmod_mpoly.pyx:811: raise DomainError("factorisation with non-prime modulus is not supported")
src/flint/types/nmod_mpoly.pyx:853: raise DomainError("factorisation with non-prime modulus is not supported")
src/flint/types/fmpz_mod.pyx:344: raise NotImplementedError("algorithm assumes modulus is prime")
src/flint/types/fq_default.pyx:98: raise ValueError("characteristic is not prime")
src/flint/types/fq_default.pyx:108: raise ValueError("either a prime or modulus must be passed for construction")
src/flint/types/fq_default.pyx:133: raise ValueError("characteristic is not prime")
src/flint/types/fq_default.pyx:156: raise ValueError("characteristic is not prime")
src/flint/types/fmpz_mod_poly.pyx:285: raise NotImplementedError("minpoly algorithm assumes that the modulus is prime")
src/flint/types/fmpz_mod_poly.pyx:1270: raise NotImplementedError("gcd algorithm assumes that the modulus is prime")
src/flint/types/fmpz_mod_poly.pyx:1765: raise NotImplementedError("factor_squarefree algorithm assumes that the modulus is prime")
src/flint/types/fmpz_mod_poly.pyx:1799: raise NotImplementedError("factor algorithm assumes that the modulus is prime")
src/flint/types/fmpz_mod_poly.pyx:1844: raise NotImplementedError("factor algorithm assumes that the modulus is prime")
src/flint/types/fmpz_mod_mpoly.pyx:333: raise DomainError("division with non-prime modulus is not supported")
src/flint/types/fmpz_mod_mpoly.pyx:783: raise DomainError("gcd with non-prime modulus is not supported")
src/flint/types/fmpz_mod_mpoly.pyx:804: raise DomainError("square root with non-prime modulus is not supported")
src/flint/types/fmpz_mod_mpoly.pyx:835: raise DomainError("factorisation with non-prime modulus is not supported")
src/flint/types/fmpz_mod_mpoly.pyx:878: raise DomainError("factorisation with non-prime modulus is not supported")
src/flint/test/test_all.py:3757: assert raises(lambda: flint.fq_default_ctx(modulus=mod_not_prime), ValueError)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that there should be a special exception type like NonPrimeModulusError
because it is such a common case in the codebase.
It is also something that much real downstream code will never had to deal with because using a non-prime modulus is not that common or at least if using it then you would already know know not to use things like division.
It definitely matters whether we return an In [1]: from flint import *
In [2]: lc_int = 3
In [3]: lc_nmod = nmod(3, 7)
In [4]: 1/lc_int
Out[4]: 0.3333333333333333
In [5]: 1/lc_nmod
Out[5]: 5 As for the unnecessary conversion there needs to be a conversion either way. The Flint routine returns a One small difference actually is that CPython interns small integers (up to 255?) to avoid a heap allocation. We could do something similar with |
3178aa5
to
01e6418
Compare
I just found a similar issue with In [7]: ctx = fmpz_mod_mpoly_ctx(2, Ordering.lex, ["x", "y"], 11)
In [8]: x = ctx.gen(0)
In [9]: x.coeffs()
Out[9]: [1]
In [10]: type(x.coeffs()[0])
Out[10]: flint.types.fmpz.fmpz
In [11]: ctx = nmod_mpoly_ctx(2, Ordering.lex, ["x", "y"], 11)
In [12]: x = ctx.gen(0)
In [13]: type(x.coeffs()[0])
Out[13]: int Likewise Probably there are also inconsistencies in the |
Sorry @Jake-Moss, you reviewed this and then I made it 3x bigger. If you are still up for reviewing then please do... The changes here are:
What I have not fixed here is the type pf the coefficients for |
No worries, will do tonight |
I'm sorry I quoted the wrong section of your message. The above makes complete sense but I was look at this change where the exponent is created as an fmpz, then immediately converted to an int.
|
It's important that the types are consistent and we don't get into a situation where we don't know what they are supposed to be any more. If it is supposed to be an I'm a bit surprised that an fmpz is used at all for the multiplicity in the factor struct. It suggests great ambition about the sizes of polynomials that can be factorised. The python-flint/src/flint/types/nmod_poly.pyx Lines 640 to 641 in 464a276
On a 64 bit system it is inconceivable that an I suppose you can have something like: In [14]: from flint import *
In [15]: ctx = fmpz_mpoly_ctx(2, Ordering.lex, ["x", "y"])
In [16]: x, y = ctx.gens()
In [17]: ((x*y)**(2**64)).factor()
Out[17]: (1, [(x, 18446744073709551616), (y, 18446744073709551616)]) It's hard to imagine that a nontrivial polynomial could be factorised and have a multiplicity that does not fit slong/ulong. For dense polynomials it is perhaps impossible: In [18]: p = nmod_poly(0, 11)
In [19]: p[2**32] = 1
Flint exception (General error):
Unable to allocate memory (34359738376).
Aborted (core dumped) (Ideally we would have a way of preventing that core dump...) In any case though we need the types to be consistent. The multiplicity is never going to be large enough to need to be anything other than an |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again all looks good to me
), | ||
] | ||
|
||
|
||
def test_mpolys(): | ||
for P, get_context, S, is_field in _all_mpolys(): | ||
for P, get_context, S, is_field, characteristic in _all_mpolys(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This new argument is unused in this test function, should the characteristic_zero
be replaced with a test on this instead? Functionally it's no different but is inconsistent with the new test_factor_poly_mpoly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, good point. I changed the division_not_supported
to:
composite_characteristic = characteristic != 0 and not characteristic.is_prime()
That makes more sense because things like factor
etc also depend on it.
src/flint/types/fmpz_mod_poly.pyx
Outdated
other = self.ctx.any_as_fmpz_mod_poly(other) | ||
if other is NotImplemented: | ||
raise TypeError(f"Cannot interpret {other} as a polynomial") | ||
|
||
if not self.ctx.is_prime(): | ||
raise NotImplementedError("gcd algorithm assumes that the modulus is prime") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like this one was missed
raise NotImplementedError("gcd algorithm assumes that the modulus is prime") | |
raise DomainError("gcd algorithm assumes that the modulus is prime") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, yeah. Good point.
I've added this case to the tests and fixed it.
assert P([1, 1]) // 2 == P([0, 0]) | ||
assert P([1, 1]) % 2 == P([1, 1]) | ||
elif nmod_poly_will_crash: | ||
pass |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Presumable these passes will be replace when #179 is merged
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I'd rather not have a variable called nmod_poly_will_crash
in the test code :)
Fact is it does crash a lot though...
Thanks make sense, more than reasonable to have that. |
c = fmpz.__new__(fmpz) | ||
fmpz_set((<fmpz>c).val, &fac.exp[i]) | ||
|
||
res[i] = (u, c) | ||
res[i] = (u, int(c)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could likely get away with using fmpz_get_ui
here although it would be fiddly to do correctly.
Probably not worth it.
elif factor_type == 'irreducible': | ||
lead = nmod_poly_factor(fac, self.val) | ||
elif factor_type == 'squarefree': | ||
nmod_poly_factor_squarefree(fac, self.val) | ||
lead = (<nmod>self.leading_coefficient()).val |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not 100% sure about this.
The doc says:
void nmod_poly_factor_squarefree(nmod_poly_factor_t res, const nmod_poly_t f)
Sets res to a square-free factorization of f.
Whereas nmod_poly_factor
returns the leading coefficient nmod_poly_factor_squarefree
does not mention anything about it.
The factorisation seems to be just all monic factors though so I presume we just need to take the leading coefficient like this:
In [2]: x = nmod_poly([0, 1], 7)
In [3]: (3*(x + 1)*(x + 4)**2).factor_squarefree()
Out[3]: (3, [(x + 1, 1), (x + 4, 2)])
ctypedef struct nmod_poly_factor_struct: | ||
nmod_poly_struct *p | ||
long *exp | ||
long num | ||
long alloc | ||
slong *exp | ||
slong num | ||
slong alloc |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was out of data compared to what I see in the Flint code.
On 64-bit Windows at least long
and slong
are not equivalent and this has been a common cause of Windows-specific crashes.
I think over time Flint itself has cleaned up its use of slong in place of long for many interfaces. Some of python-flint's code predates Flint cleaning these things up though so we have some things like this that are out of date.
It is likely that almost all of these 635 lines should change from long to slong:
$ git grep ' long ' | wc -l
635
Certainly these are out of date because prec
should be slong
:
$ git grep long src/flint/flintlib/arb.pxd | head
src/flint/flintlib/arb.pxd:from flint.flintlib.flint cimport ulong, slong, flint_rand_t
src/flint/flintlib/arb.pxd: void arb_set_round(arb_t z, const arb_t x, long prec)
src/flint/flintlib/arb.pxd: void arb_neg_round(arb_t x, const arb_t y, long prec)
src/flint/flintlib/arb.pxd: void arb_set_si(arb_t x, long y)
src/flint/flintlib/arb.pxd: void arb_set_ui(arb_t x, ulong y)
src/flint/flintlib/arb.pxd: void arb_set_round_fmpz_2exp(arb_t y, const fmpz_t x, const fmpz_t exp, long prec)
src/flint/flintlib/arb.pxd: void arb_set_round_fmpz(arb_t y, const fmpz_t x, long prec)
src/flint/flintlib/arb.pxd: void arb_printd(const arb_t x, long digits)
src/flint/flintlib/arb.pxd: void arb_mul_2exp_si(arb_t y, const arb_t x, long e)
src/flint/flintlib/arb.pxd: void arb_get_abs_ubound_arf(arf_t u, const arb_t x, long prec)
I see that gh-190 is waiting on this so I'll merge once tests pass. |
Thanks for the reviews! |
See #164 (comment)