-
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
Add method to compute integer roots #72
Conversation
src/flint/types/fmpz_poly.pyx
Outdated
fmpz_poly_factor(fac, self.val) | ||
for 0 <= i < fac.num: | ||
deg = fmpz_poly_degree(&fac.p[i]) | ||
fmpz_poly_get_coeff_fmpz(linear_coeff.val, &fac.p[i], 1) |
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.
It would be much easier to implement this as just Python code:
In [26]: x = flint.fmpz_poly([0, 1])
In [27]: p = x*(x - 1)*(x**2 + 2)
In [28]: [(-fac[0], mul) for fac, mul in p.factor()[1] if fac.degree() == 1]
Out[28]: [(0, 1), (1, 1)]
Then it could be a generic method defined in the superclass.
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 kept this as cython in case it helped with performance but agree that we could instead write it in python this way.
If the code was in the base superclass is the best thing then to have import statements in the doctest for each flint type?
For the arb and acb we can simply have roots write over the superclass for the specific impl which currently exist?
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.
Performance of anything else is unlikely to be a significant concern if the base routine is factor. Other algorithms could be much more efficient in common cases e.g. using the rational root theorem (and derivative for multiplicity):
https://en.wikipedia.org/wiki/Rational_root_theorem
Note also that Python code in a .pyx file will run faster than Python code in a .py file: Cython already translates the code to C to some extent even if it just looks like Python code.
If Flint had dedicated algorithms for this then it would be worth calling those from cython. Otherwise I don't think it is worth implementing anything nontrivial here and we should just try to keep the code here as simple as possible: it is better to spend time expanding the coverage of Flint's existing functionality than trying to optimise things that are not already optimised in Flint.
For the arb and acb we can simply have roots write over the superclass for the specific impl which currently exist?
Yes, and if any specialised algorithms are implemented in future for other cases like fmpz_poly
they can override the generic method as well.
New commit adds a pythonic way to find roots.
>>> from flint import *
>>> fmpz_poly([12,7,1]).roots()
[(-3, 1), (-4, 1)]
>>> fmpq_poly([12,7,1], 163).roots()
[(-3, 1), (-4, 1)]
>>> nmod_poly([12,7,1], 11).roots()
[(8, 1), (7, 1)]
>>> acb_poly([12,7,1]).roots()
[[-4.0000000 +/- 1.96e-8] + [+/- 1.50e-8]j, [-3.00000000 +/- 7.46e-9] + [+/- 7.46e-9]j]
>>> arb_poly([12,7,1]).roots()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "src/flint/flint_base/flint_base.pyx", line 104, in flint.flint_base.flint_base.flint_poly.roots
raise NotImplementedError("Polynomial has no factor method, roots cannot be determined")
NotImplementedError: Polynomial has no factor method, roots cannot be determined |
That is a shame because this would be useful. Actually for SymPy what would be even more useful is a function that can compute non-intersecting arb real roots of a (square free or irreducible) |
For real roots, we could take the complex roots and then simply filter those which are purely real? Not pretty, but it would at least "work". I'm not familiar with what the state of the art, or best idea would be for this, but I'm happy to do the "stupid" idea following the above. |
src/flint/flint_base/flint_base.pyx
Outdated
>>> from flint import fmpz_poly | ||
>>> fmpz_poly([]).roots() | ||
[] | ||
>>> fmpz_poly([1]).roots() | ||
[] | ||
>>> fmpz_poly([2, 1]).roots() | ||
[(-2, 1)] | ||
>>> fmpz_poly([1, 2]).roots() | ||
[] | ||
>>> fmpz_poly([12, 7, 1]).roots() | ||
[(-3, 1), (-4, 1)] | ||
>>> (fmpz_poly([1, 2]) * fmpz_poly([-3,1]) * fmpz_poly([1, 2, 3]) * fmpz_poly([12, 7, 1])).roots() | ||
[(-3, 1), (-4, 1), (3, 1)] | ||
>>> from flint import nmod_poly | ||
>>> nmod_poly([1], 11).roots() | ||
[] | ||
>>> nmod_poly([1, 2, 3], 11).roots() | ||
[(8, 1), (6, 1)] | ||
>>> nmod_poly([1, 6, 1, 8], 11).roots() | ||
[(5, 3)] | ||
>>> from flint import fmpq_poly | ||
>>> fmpq_poly([1,2]).roots() | ||
[(-1/2, 1)] | ||
>>> fmpq_poly([2,1]).roots() | ||
[(-2, 1)] | ||
>>> f = fmpq_poly([fmpq(1,3), fmpq(3,5)]) * fmpq_poly([fmpq(4,11), fmpq(9)]) | ||
>>> f.roots() | ||
[(-4/99, 1), (-5/9, 1)] |
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.
It would be better to add most of these examples just to the tests rather than the docstring. The primary purpose of the docstring should be for users to read as documentation rather than to function as a test suite. In that sense it is probably also good to mention how to find complex roots here because I expect some users would come to this function expecting it to do that.
What I want is a function that provably separates the real and non-real roots and then bounds each real root into an interval However given an |
Current state of polynomial roots: >>> from flint import *
>>> fmpz_poly([1,2]).roots()
[]
>>> fmpz_poly([2,1]).roots()
[(-2, 1)]
>>> fmpz_poly([1,2]).complex_roots()
[(-0.500000000000000, 1)]
>>> fmpz_poly([2,1]).complex_roots()
[(-2.00000000000000, 1)]
>>>
>>> fmpq_poly([2,1]).roots()
[(-2, 1)]
>>> fmpq_poly([1,2]).roots()
[(-1/2, 1)]
>>> fmpq_poly([1,2]).complex_roots()
[(-0.500000000000000, 1)]
>>> fmpq_poly([2,1]).complex_roots()
[(-2.00000000000000, 1)]
>>>
>>> nmod_poly([1,2],11).roots()
[(5, 1)]
>>> nmod_poly([1,2],11).complex_roots()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'flint.types.nmod_poly.nmod_poly' object has no attribute 'complex_roots'
>>>
>>> acb_poly([1,2]).roots()
[-0.500000000000000]
>>> acb_poly([1,2]).complex_roots()
[-0.500000000000000]
>>>
>>> arb_poly([1,2]).roots()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "src/flint/flint_base/flint_base.pyx", line 88, in flint.flint_base.flint_base.flint_poly.roots
raise NotImplementedError("Polynomial has no factor method, roots cannot be determined")
NotImplementedError: Polynomial has no factor method, roots cannot be determined
>>> arb_poly([1,2]).complex_roots()
[-0.500000000000000] I think the complex method should include multiplicities, but there's no support for squareful input atm? >>> acb_poly([1,2,1]).roots()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "src/flint/types/acb_poly.pyx", line 402, in flint.types.acb_poly.acb_poly.roots
raise ValueError("roots() failed to converge: insufficient precision, or squareful input")
ValueError: roots() failed to converge: insufficient precision, or squareful input |
Perhaps |
For |
It is not possible to distinguish close roots vs multiple roots if the coefficients are not exact so multiplicity is not well defined there. The situation in practice would be that you
There are different ways to arrange the code. Possibly if the other poly types all have this method then |
I guess my issue is that we have a divergence of behaviour here: >>> from flint import *
>>> fmpz_poly([1,2,1]).roots()
[(-1, 2)]
>>> fmpz_poly([1,2,1]).complex_roots()
[(-1.00000000000000, 2)]
>>> acb_poly([1,2,1]).complex_roots()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "src/flint/types/acb_poly.pyx", line 402, in flint.types.acb_poly.acb_poly.roots
raise ValueError("roots() failed to converge: insufficient precision, or squareful input")
ValueError: roots() failed to converge: insufficient precision, or squareful input |
Added an >>> from flint import *
>>>
>>> fmpz_poly([1,2,1]).roots()
[(-1, 2)]
>>> fmpz_poly([1,2,1]).complex_roots()
[(-1.00000000000000, 2)]
>>> fmpq_poly([1,2,1]).roots()
[(-1, 2)]
>>> fmpq_poly([1,2,1]).complex_roots()
[(-1.00000000000000, 2)]
>>> nmod_poly([1,2,1], 13).roots()
[(12, 2)]
>>> nmod_poly([1,2,1], 13).complex_roots()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "src/flint/flint_base/flint_base.pyx", line 100, in flint.flint_base.flint_base.flint_poly.complex_roots
raise AttributeError("Complex roots are not supported for this polynomial")
AttributeError: Complex roots are not supported for this polynomial
>>> arb_poly([1,2,1]).roots()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "src/flint/flint_base/flint_base.pyx", line 89, in flint.flint_base.flint_base.flint_poly.roots
raise NotImplementedError("Polynomial has no factor method, roots cannot be determined")
NotImplementedError: Polynomial has no factor method, roots cannot be determined
>>> arb_poly([1,2,1]).complex_roots()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "src/flint/types/arb_poly.pyx", line 121, in flint.types.arb_poly.arb_poly.complex_roots
return acb_poly(self).roots(**kwargs)
File "src/flint/types/acb_poly.pyx", line 402, in flint.types.acb_poly.acb_poly.roots
raise ValueError("roots() failed to converge: insufficient precision, or squareful input")
ValueError: roots() failed to converge: insufficient precision, or squareful input
>>> arb_poly([1,2]).complex_roots()
[-0.500000000000000]
>>> acb_poly([1,2,1]).complex_roots()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "src/flint/types/acb_poly.pyx", line 402, in flint.types.acb_poly.acb_poly.roots
raise ValueError("roots() failed to converge: insufficient precision, or squareful input")
ValueError: roots() failed to converge: insufficient precision, or squareful input
>>> acb_poly([1,2,1]).roots()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "src/flint/types/acb_poly.pyx", line 402, in flint.types.acb_poly.acb_poly.roots
raise ValueError("roots() failed to converge: insufficient precision, or squareful input")
ValueError: roots() failed to converge: insufficient precision, or squareful input
>>> acb_poly([1,2]).roots()
[-0.500000000000000] |
I'm marking this ready for review as it does what I thought needed to be done. There's a question about the consistency of the complex roots for various methods, but this is a different problem I suppose. |
return roots | ||
|
||
def complex_roots(self): | ||
raise AttributeError("Complex roots are not supported for this polynomial") |
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.
AttributeError in particular makes no sense here rather than e.g. NotImplementedError
Ideally it would be like DomainError
or something at least for nmod_poly
.
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.
Perhaps not for this PR but exception handling needs a broad cleanup.
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.
def complex_roots(self):
raise NotImplementedError("The method complex_roots is only defined for certain polynomial rings")
Might be better but I agree something like DomainError
would be nice.
Okay, let's get this in. |
I'm not sure what in general can be done about this. Essentially computing roots of polynomials with approximate coefficients is not something that can be completely well defined. I know that in context I would always use this the way that |
I guess fundementally the difference is for |
This is a work in progress. Currently both
fmpz_poly
andfmpq_poly
have.roots()
methods which return complex roots of the polynomial. The hope is to address issue #62.This first set of commits renames these methods to
complex_roots()
and also introduces a new method to compute integer roots onfmpz_poly()
which is named.roots()
I have updated and included a few more tests and added doctests to the new function.
If this is something the flint team like, I can add similar methods to
fmpq_poly
for rational roots and also look at the other polynomial classes.