sympy / sympy

A computer algebra system written in pure Python

Home Page:https://sympy.org/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

FiniteField.__call__ returns different types depending on whether flint is installed or not

bjodah opened this issue · comments

On sympy master:

>>> import flint
ModuleNotFoundError: No module named 'flint'
>>> type(sympy.FF(7)(3))
<class 'sympy.polys.domains.modularinteger.ModularIntegerFactory.<locals>.cls'>

in an environment where flint is installed:

>>> type(sympy.FF(7)(3))
flint.types.nmod.nmod

I think it's suboptimal if the underlying backend used by sympy "leaks" out into the public sympy API, no?

(discovered as test failure in symengine.py)

I think this return call should wrap the result in a sympy type:

perhaps ModularIntegerFactory?

I think it's suboptimal if the underlying backend used by sympy "leaks" out into the public sympy API, no?

This is already the case for other domains like ZZ:

In [1]: type(ZZ(1))
Out[1]: mpz

In [1]: type(ZZ(1))
Out[1]: flint.types.fmpz.fmpz

In [1]: type(ZZ(1))
Out[1]: int

Ultimately if we use types from gmpy2 and python-flint directly as domain elements then this is inevitable.

What should matter is that they are functionally equivalent.

I think this return call should wrap the result in a sympy type:

If you mean that this should dynamically create a class then I disagree. In fact I think that it is already a problem that dynamically created classes are used i.e. this should be changed:

class cls(ModularInteger):
mod, dom, sym = _mod, _dom, _sym
_parent = parent

The way that python-flint does it is better i.e. you have a type that has a modulus attribute and it has a two argument constructor.

Yes, I agree that the dynamic class design is... weird (I never understood it). But whatever is returned (flint.types.nmod.nmod or something else), it would be nice if there is a well defined set of methods/attributes that can be used on the return value (otherwise consuming code will have to handle API-differences of the returned object in if isinstance(...) blocks).

Domain elements should only be used in a context where you have the domain object and it is that domain object that provides a consistent interface:

In [2]: K = GF(2)

In [3]: K.of_type(K(1))
Out[3]: True

In [4]: K.to_sympy(K(1))
Out[4]: 1

In [5]: ZZ.convert_from(K(1), K)
Out[5]: 1

If you have the domain element but not the domain then you are not using the domains in the intended way. Code that is intended to operate on generic domain elements should not presume any interface of them beyond ring operations:

class RingElement(Protocol):
"""A ring element.
Must support ``+``, ``-``, ``*``, ``**`` and ``-``.
"""
def __add__(self: T, other: T, /) -> T: ...
def __sub__(self: T, other: T, /) -> T: ...
def __mul__(self: T, other: T, /) -> T: ...
def __pow__(self: T, other: int, /) -> T: ...
def __neg__(self: T, /) -> T: ...

Yes, I agree that the dynamic class design is... weird (I never understood it).

I think it is a micro-optimisation for arithmetic operations so that you can do:

def __add__(self, other):
    if type(self) == type(other):
        return self.new(self.val + other.val)

Otherwise it would be:

def __add__(self, other):
    if isinstance(other, ModularInteger) and self.modulus == other.modulus:
        return self.new(self.val + other.val, self.modulus)

I presume that at some point someone benchmarked this and found that the dynamically generated classes were slightly faster.

The same design ended up being used for GF, RR, CC, PolyElement, etc. It is also used in mpmath for mpfs with different contexts.

Alright, I see, thank you for explaining this. Then perhaps there isn't a simple fix here. We could close this issue from my point of view.

Okay, let's close this.