-
Notifications
You must be signed in to change notification settings - Fork 28
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
Begin adding fmpz_mod_poly #87
Conversation
Main thing I'm not so sure about is the new context extension, whether this should be built on top of the Something like cdef class fmpx_mod_poly_ctx:
def __init__(self, mod_ctx):
self.val = mod_ctx Then the way of working would be: F = fmpz_mod_ctx(163)
a = F(3) # fmpz_mod(3, 163)
R = fmpz_mod_poly_ctx(F)
f = R([1,2,3]) # fmpz_mod_poly([1,2,3], 11) Of course, we could allow either, so we could have: F = fmpz_mod_ctx(163)
R = fmpz_mod_poly_ctx(F)
assert R == fmpz_mod_poly_ctx(163) Where when |
If we were working at a higher level I might suggest something like Otherwise I prefer this version:
The reason I like it is because then If we had cached contexts then it wouldn't matter because |
Made the change above about the |
It is okay for them to hash to the same value. The hash invariant requires only that equal objects have the same hash. It is not required that unequal objects have different hash so if you want all hashes can just be 1. In practice I doubt that it matters much as I can't imagine someone putting these two objects into a dict/set together at least not in a performance sensitive context. |
There's a whole bunch of functions available in flint which might not make it into the first PR for this type, but I might add something like
To entice others to work on it if they're interested in having the feature. |
So I think I'm missing something about how In flint, I call: >>> from flint import *
>>> F = fmpz_mod_ctx(163)
>>> R = fmpz_mod_poly_ctx(F)
>>> f = R([1,2,3])
>>> f.factor()
[(x + 115, 1), (x + 103, 1)] sage: F = GF(163)
sage: R.<x> = PolynomialRing(F)
sage: f = R([1,2,3])
sage: f.factor()
(3) * (x + 103) * (x + 115) We see that the constant term is missing. Now, in the ctypedef struct fmpz_mod_poly_factor_struct:
fmpz_mod_poly_struct * poly
slong *exp
slong num
slong alloc
ctypedef fmpz_mod_poly_factor_struct fmpz_mod_poly_factor_t[1] and there's no storage for this constant term. Is the intention that this is only ever called on monic polynomials? Another option I guess would be to first collect the leading coefficient with |
Added a few methods to do what I suggest above: >>> from flint import *
>>> F = fmpz_mod_ctx(163)
>>> R = fmpz_mod_poly_ctx(F)
>>> f = R([1,2,3])
>>> f.leading_coefficient()
fmpz_mod(3, 163)
>>> f.factor()
(fmpz_mod(3, 163), [(x + 115, 1), (x + 103, 1)])
>>> f.monic()
x^2 + 55*x + 109
>>> f.monic().factor()
(fmpz_mod(1, 163), [(x + 115, 1), (x + 103, 1)]) |
The doc says:
So I guess it always makes the polynomial monic. The docs don't say if the modulus is assumed to be prime but I imagine that many of the factor related functions would only work for prime modulus in which case the polynomial could always be made monic. Looks like the modulus should be prime: In [2]: import flint
In [3]: F = flint.fmpz_mod_ctx(12)
...: R1 = flint.fmpz_mod_poly_ctx(F)
In [4]: p = R1([1, 2, 3, 4])
In [5]: p.factor()
Flint exception (Impossible inverse):
Exception in fmpz_mod_inv: Cannot invert.
Aborted (core dumped) I guess that failure is from trying to make it monic. Here it is monic already but the modulue is not prime: In [1]: import flint
In [2]: F = flint.fmpz_mod_ctx(12)
...: R1 = flint.fmpz_mod_poly_ctx(F)
In [3]: p = R1([4, 3, 2, 1])
In [4]: p.factor()
GR_MUST_SUCCEED failure: src/fmpz_mod_poly/gcd.cAborted (core dumped) |
What's your opinion on adding a |
We probably should have prime checking by default if the alternative is a segfault. If it can be done only once per context then I imagine that is fine. |
Pushed some more work from last night.
|
Added support to get random elements from both |
Let me know if at some point you would just like to get what is here merged and move on. Not everything needs to be added in one pull request. I would like anything that is added to be tested though. We should aim for 100% line coverage in this library because it is mostly wrapper code. The bugs are unlikely to be in the detailed algorithms and are mostly just going to be that calling a function with the wrong arguments causes a segfault or something. Knowing that a function here ever succeeds without a segfault is probably enough to have some reasonable confidence that it works for most generic inputs. It is most important just to see that a very simple test passes on each platform rather than checking particularly complicated or difficult inputs. |
I'm going to clean up what I have an add a few extra functions and then start working on coverage. I had my wedding yesterday and so have been not-coding, but will get this ready for merging this week :) |
src/flint/test/test.py
Outdated
assert raises(lambda: 1 / P([1, 1]), TypeError) | ||
assert raises(lambda: P([1, 2, 1]) / P([1, 1]), TypeError) | ||
assert raises(lambda: P([1, 2, 1]) / P([1, 2]), TypeError) | ||
# TODO: | ||
# I think this should be a ValueError and not a Type Error? | ||
# assert raises(lambda: 1 / P([1, 1]), TypeError) | ||
# assert raises(lambda: P([1, 2, 1]) / P([1, 1]), TypeError) | ||
# assert raises(lambda: P([1, 2, 1]) / P([1, 2]), TypeError) |
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 way this is defined for the other poly types is just that it is a TypeError. Note that P([1, 2, 1]) / P([1, 1])
can be an exact division but it gives a TypeError: you need to use //
or divmod
instead. I think that this was a deliberate choice by @fredrik-johansson so that the operations are always well defined.
I did add scalar division where the ground domain is a field though (nmod_poly and fmpq_poly):
In [14]: flint.fmpq_poly([1, 1]) / 7
Out[14]: 1/7*x + 1/7
In SymPy's algebra system there is another operation which is call exquo that performs an exact division when possible or otherwise raises an ExactQuotientFailed error. The exquo
operation is very useful but it is not implemented as a / b
in SymPy at least. You have to use it explicitly:
In [16]: ZZ.exquo(ZZ(6), ZZ(3))
Out[16]: mpz(2)
In [17]: ZZ.exquo(ZZ(6), ZZ(5))
---------------------------------------------------------------------------
ExactQuotientFailed
Note also that fmpz
refuses an exact division:
In [18]: flint.fmpz(5)/5
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[18], line 1
----> 1 flint.fmpz(5)/5
TypeError: unsupported operand type(s) for /: 'flint.types.fmpz.fmpz' and 'int'
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 ok. Thank you for the detailed reply. Seems like the best thing then is to remove the support for division in this way and potentially have a specialised formula.
For scalar division, this is a TODO atm but could easily be implemented to allow the work to match other polynomials.
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 don't know whether we would potentially want to change a / b
to be something like exquo
. There is a lot of inconsistency in defining a / b
for non-fields between different environments even just within the Python ecosystem. Maybe it is better just to avoid that ambiguity by not allowing it at all.
I don't have a strong preference here.
It is easy to add this in Python though:
|
Ok, made no sqrt be a value error, removed the exact division from Main thing now is get back to 100% coverage and clean up the tests file, and also figure out how to solve the Edit: I also called exact division |
src/flint/types/fmpz_mod_poly.pyx
Outdated
else: | ||
return self.evalutate(input) | ||
|
||
def evalutate(self, input): |
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.
evaluate
That's the thing that concerns me as well. Otherwise I would say let's just merge and continue to improve later but we can't mess around with getting the memory management right: it needs to be correct now and always to the best of our knowledge or otherwise we invite a world of pain in future. |
So I've tried something, but I don't know if I like it... def __cinit__(self, val, ctx):
if not typecheck(ctx, fmpz_mod_poly_ctx):
raise TypeError
self.ctx = ctx
fmpz_mod_poly_init(self.val, self.ctx.mod.val)
def __dealloc__(self):
fmpz_mod_poly_clear(self.val, self.ctx.mod.val)
def __init__(self, val, ctx):
if not typecheck(ctx, fmpz_mod_poly_ctx):
raise TypeError
self.ctx = ctx
check = self.ctx.set_any_as_fmpz_mod_poly(self.val, val)
if check is NotImplemented:
raise TypeError
self.initialized = True If we then call res = fmpz_mod_poly.__new__(fmpz_mod_poly, None, context) Instead of simply: res = fmpz_mod_poly.__new__(fmpz_mod_poly) Then This seems to compile fine though and all the tests pass |
If I think the way that you had the code before was good but just we need to ensure that an def __cinit__(self):
self.initialized = False
def __dealloc__(self):
if self.initialized:
fmpz_mod_poly_clear(self.val, self.ctx.mod.val)
def __init__(self, val, ctx):
if not typecheck(ctx, fmpz_mod_poly_ctx):
raise TypeError
self.ctx = ctx
fmpz_mod_poly_init(obj.val, ctx)
self.initialized = True
check = self.ctx.set_any_as_fmpz_mod_poly(self.val, val)
if check is NotImplemented:
raise TypeError Then when you use obj = fmpz_mod_poly.__new__(fmpz_mod_poly)
fmpz_mod_poly_init(obj.val, ctx) In other words the code using this becomes responsible for calling the init function if it chooses to use The advantage of letting the caller call obj = fmpz_mod_poly.new(ctx) # already initialised
obj = fmpz_mod_poly.new_alloc(ctx, n) # already initalised Alternatively we can have def __cinit__(self, val, ctx):
self.initialized = False
if not typecheck(ctx, fmpz_mod_poly_ctx):
raise TypeError
self.ctx = ctx
fmpz_mod_poly_init(self.val, ctx)
self.initialized = True
def __dealloc__(self):
if self.initialized:
fmpz_mod_poly_clear(self.val, self.ctx) Which can be used like your version but it handles the |
A simpler version without the initialised flag could be: def __cinit__(self, val, ctx):
# self.ctx is already set to None here I think
if not typecheck(ctx, fmpz_mod_poly_ctx):
raise TypeError
fmpz_mod_poly_init(self.val, ctx)
self.ctx = ctx
def __dealloc__(self):
if self.ctx is not None:
fmpz_mod_poly_init(self.val, self.ctx) |
def __cinit__(self, val, ctx): | ||
if not typecheck(ctx, fmpz_mod_poly_ctx): | ||
raise TypeError | ||
self.ctx = ctx | ||
fmpz_mod_poly_init(self.val, self.ctx.mod.val) | ||
|
||
def __dealloc__(self): | ||
if self.ctx is not None: | ||
fmpz_mod_poly_clear(self.val, self.ctx.mod.val) | ||
|
||
def __init__(self, val, ctx): | ||
check = self.ctx.set_any_as_fmpz_mod_poly(self.val, val) | ||
if check is NotImplemented: | ||
raise TypeError |
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.
Okay, I think this is correct now.
src/flint/types/fmpz_mod_poly.pyx
Outdated
|
||
G = fmpz_mod_poly.__new__(fmpz_mod_poly, None, self.ctx) | ||
S = fmpz_mod_poly.__new__(fmpz_mod_poly, None, self.ctx) | ||
T = fmpz_mod_poly.__new__(fmpz_mod_poly, None, self.ctx) |
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 would make a staticmethod on ctx for this:
@staticmethod
cdef fmpz_mod_poly new(fmpz_mod_poly_ctx self):
return fmpz_mod_poly.__new__(fmpz_mod_poly, None, self)
Then it is something like:
cdef fmpz_mod_poly ctx
cdef fmpz_mod_poly G, S, T
ctx = self.ctx
G = ctx.new()
S = ctx.new()
T = ctx.new()
I would check how the generated C code looks to see exactly what is happening though.
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.
Sorry, not a staticmethod, just an ordinary method. I was going to suggest a staticmethod on fmpz_mod_poly
like
G = fmpz_mod_poly.new(ctx)
but then I realised that it makes more sense as an instance method on the context:
G = ctx.new()
I think this PR is probably good to go but I need to find time to read through it in detail. |
I think I’m not quite at 100% coverage again so I’ll address this and the above comments as well. Hopefully by this evening
|
Added a new poly method, which i think makes the code easier to read and recovered 100% coverage. |
res[i] = (u, exp) | ||
return self.leading_coefficient(), res | ||
|
||
def roots(self, multiplicities=True): |
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 generally prefer separate functions rather than a boolean flag
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.
So I think the default should be with multiplicities, so you think a root_without_multiplicities()
function would be better?
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.
Coming up with good names is hard :)
Maybe root_set
?
In this situation I always ant to check what other systems do...
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.
Root set is definitely better than my suggestion. Haha. For SageMath, they have a bool flag.
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.
For SageMath, they have a bool flag.
Yeah, so does SymPy:
In [1]: roots((x**2 - 1)**2)
Out[1]: {-1: 2, 1: 2}
In [2]: roots((x**2 - 1)**2, multiple=True)
Out[2]: [-1, -1, 1, 1]
I think it is better though if a function has a clear return type and so if you want to return different types then you should have different function names. Also it is clearer just to write roots_multiple(...)
rather than e.g. roots(..., multiple=True)
.
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.
So if I thought not multiplicities was default then I'd agree but I think it's fine for a user to essentially never know this is an option as worst case they filter out the multiplicities themselves in python? The bool flag here is really only for people who want it but it's not needed.
Agree about consistent return types tho. Not sure what's best
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 sure what's best either really.
It is usually better just to have separate functions if there is a need to provide different options. It can also be good just to refuse to provide any options that are potentially redundant.
For python-flint right now I would aim to avoid (postpone?) providing anything that is not strictly necessary because it is a lot of work just to provide the strictly necessary things when you consider that all types (fmpz_poly
etc) should ideally support the same usage.
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.
Maybe we could just remove the option to not compute multiplicities then? I'm not at my laptop now but I can edit it out tomorrow.
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 can leave it for now.
I think this looks good. I'll merge once CI completes. |
Okay looks good. Thanks! |
Opening a PR just to have the ability for group discussion while working on this new type. Only work so far is an import of the level0 files, and a simple print test to ensure everything compiles happily.