Description
CC @Jake-Moss
The idea of higher-level interfaces is discussed in gh-53. I would imagine that ultimately the usual way to create a multivariate polynomial would look something like:
from flint import ZZ
R, [x, y] = ZZ.mpoly_ring(['x', 'y'])
p = x**2 - y**2
p.factor()
Other options like monomial ordering could be keyword arguments.
In that sense we can think of the current interface as being sort of "low-level" but still I think it can be nicer than it is. This is currently the way to construct and use a multivariate polynomial context:
ctx = fmpz_mpoly_ctx(3, Ordering.lex, ["x", "y", "z"])
x, y, z = ctx.gens()
(x**2 - y**2).factor()
I think it is reasonable that the generators are not returned by default but the constructor syntax is awkward. It is redundant to specify the number of generators if we are already supplying a list of names. Also most of the time using lex as the default ordering is fine. I doubt many people will ever care about the monomial ordering.
It would be better to change it so that you can do
ctx = fmpz_mpoly_ctx(["x", "y", "z"])
and get a lex ordered ring by default. I think it would be reasonable to say that the ordering parameter could be a keyword-only argument like:
ctx = fmpz_mpoly_ctx(["x", "y", "z"], order='degrevlex')
Another problem is that the monomial ordering has to be passed as an enum which is also awkward e.g.:
import flint
ctx = flint.fmpz_mpoly_ctx(["x", "y", "z"], order=flint.Ordering.degrevlex)
It is reasonable to use these enums but it should be possible to just pass a string when calling the constructor. I think that a string should be displayed in the repr as well:
>>> ctx
fmpz_mpoly_ctx(["x", "y", "z"], order="lex")
The reason that the first argument to the current constructor is an integer is because it is envisaged that you would do:
>>> ctx = fmpz_mpoly_ctx(3, Ordering.lex, "x")
>>> ctx.gens()
(x1, x2, x3)
I think that a better way of handling that is to allow a symbol name to be a tuple of name and count e.g.:
>>> ctx = fmpz_mpoly_ctx([("x", 3), "y"])
>>> ctx.gens()
(x1,x2,x3,y)
The docstring for fmpz_mpoly_ctx
says not to call it directly. I believe this is because get_context
manages a cache of contexts. That is reasonable but in that case I think that calling the constructor directly should give an error and then my comments above apply to get_context
as well.
The interface for get_context
is:
In [11]: ctx = fmpz_mpoly_ctx.get_context(3, Ordering.lex, names="x")
In [12]: ctx.gens()
Out[12]: (x0, x1, x2)
In [14]: ctx = fmpz_mpoly_ctx.get_context(3, Ordering.lex, nametup=("x", "y", "z"))
In [15]: ctx.gens()
Out[15]: (x, y, z)
Again I think that the argument are somewhat backwards in:
fmpz_mpoly_ctx.get_context(3, Ordering.lex, nametup=("x", "y", "z"))
Ideally it should be
fmpz_mpoly_ctx.get_context(["x", "y", "z"])
The name get_context
is a bit confusing for a method that creates a new context. I would perhaps rename that to new
like:
fmpz_mpoly_ctx.new(["x", "y", "z"])
Also the parameter names are strange because names
plural is what is used when passing a single string and nametup
is what I would have naturally called names
. The tup
emphasises that it needs to be a tuple but really it should be okay to have a list here:
>>> ctx = fmpz_mpoly_ctx.get_context(3, Ordering.lex, nametup=["x", "y", "z"])
...
TypeError: Argument 'nametup' has incorrect type (expected tuple, got list)
I'm not sure we need the names
and nametup
arguments at all though. This is how I think I would do it:
ctx = fmpz_mpoly_ctx.new(["x", "y", "z"]) # x,y,z
ctx = fmpz_mpoly_ctx.new([("x", 2)]) # x1,x2
ctx = fmpz_mpoly_ctx.new([("x", 2), y]) # x1,x2,y
Another option seen in SymPy is:
In [17]: symbols('x:3, y')
Out[17]: (x0, x1, x2, y)
That looks nice in some situations but it is actually awkward when the number of symbols is a variable because you have to format it into a string:
In [18]: n = 3
In [19]: symbols(f'x:{n}, y')
Out[19]: (x0, x1, x2, y)
Also I think it is better to keep things simple at this level and avoid any sort of string parsing.