Skip to content
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

Public API #704

Open
oscarbenjamin opened this issue May 21, 2023 · 26 comments
Open

Public API #704

oscarbenjamin opened this issue May 21, 2023 · 26 comments
Milestone

Comments

@oscarbenjamin
Copy link

Since 3414c6e from gh-701 import sympy fails:

>>> import sympy
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/oscar/current/active/sympy/sympy/__init__.py", line 30, in <module>
    from sympy.core.cache import lazy_function
  File "/home/oscar/current/active/sympy/sympy/core/__init__.py", line 9, in <module>
    from .expr import Expr, AtomicExpr, UnevaluatedExpr
  File "/home/oscar/current/active/sympy/sympy/core/expr.py", line 4159, in <module>
    from .mul import Mul
  File "/home/oscar/current/active/sympy/sympy/core/mul.py", line 2193, in <module>
    from .numbers import Rational
  File "/home/oscar/current/active/sympy/sympy/core/numbers.py", line 30, in <module>
    from mpmath.ctx_mp import mpnumeric
ImportError: cannot import name 'mpnumeric' from 'mpmath.ctx_mp' (/home/oscar/current/active/mpmath/mpmath/ctx_mp.py)

The mpnumeric class is the base class for _mpf and _mpc and is used by sympy for detecting when an object is an instance of mpf or mpc. This is needed since _mpf and _mpc seem to be designated private and mpf and mpc are dynamically generated types (gh-696, gh-532).

The fix for that is straight-forward:

diff --git a/mpmath/ctx_mp.py b/mpmath/ctx_mp.py
index c17017e..6cf8e33 100644
--- a/mpmath/ctx_mp.py
+++ b/mpmath/ctx_mp.py
@@ -31,6 +31,9 @@
     from .ctx_mp_python import PythonMPContext as BaseMPContext
 
 
+from .ctx_mp_python import _mpf, _mpc, mpnumeric
+
+
 class MPContext(BaseMPContext, StandardBaseContext):
     """
     Context for multiprecision arithmetic with a global precision.

However then a new error arises:

>>> import sympy
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/oscar/current/active/sympy/sympy/__init__.py", line 30, in <module>
    from sympy.core.cache import lazy_function
  File "/home/oscar/current/active/sympy/sympy/core/__init__.py", line 9, in <module>
    from .expr import Expr, AtomicExpr, UnevaluatedExpr
  File "/home/oscar/current/active/sympy/sympy/core/expr.py", line 4159, in <module>
    from .mul import Mul
  File "/home/oscar/current/active/sympy/sympy/core/mul.py", line 2193, in <module>
    from .numbers import Rational
  File "/home/oscar/current/active/sympy/sympy/core/numbers.py", line 4554, in <module>
    _sympy_converter[type(mpmath.rational.mpq(1, 2))] = sympify_mpmath_mpq
                          ^^^^^^^^^^^^^^^
AttributeError: module 'mpmath' has no attribute 'rational'

This is because the mpmath.rational module was removed in gh-691. I'm not sure if there ever is a reason that sympy would need to convert an mpmath mpq. If there is a reason though then it is not clear what the replacement should be since gh-691 describes it as private.

This is enough to make import sympy work:

diff --git a/mpmath/__init__.py b/mpmath/__init__.py
index eb7004d..49acc4c 100644
--- a/mpmath/__init__.py
+++ b/mpmath/__init__.py
@@ -2,6 +2,8 @@
 
 from .usertools import monitor, timing
 
+from . import rational
+
 from .ctx_fp import FPContext
 from .ctx_mp import MPContext
 from .ctx_iv import MPIntervalContext
diff --git a/mpmath/ctx_mp.py b/mpmath/ctx_mp.py
index c17017e..6cf8e33 100644
--- a/mpmath/ctx_mp.py
+++ b/mpmath/ctx_mp.py
@@ -31,6 +31,9 @@
     from .ctx_mp_python import PythonMPContext as BaseMPContext
 
 
+from .ctx_mp_python import _mpf, _mpc, mpnumeric
+
+
 class MPContext(BaseMPContext, StandardBaseContext):
     """
     Context for multiprecision arithmetic with a global precision.
diff --git a/mpmath/rational.py b/mpmath/rational.py
new file mode 100644
index 0000000..7fbc2ca
--- /dev/null
+++ b/mpmath/rational.py
@@ -0,0 +1 @@
+from .libmp import MPQ as mpq

I wonder more generally though what should be expected in terms of backwards compatibility here. Since mpmath has not changed much in a long time there will be a lot of downstream code that depends on being able to import different things from different modules and it is not clear exactly what should be considered public or private. In gh-691 then mpq type is described as being "private" but actually it could be imported in a way that does not look like accessing anything private:

from mpmath.rational import mpq

It is not hard to make changes in sympy that would accommodate these changes in mpmath but it does make me wonder how much other code downstream from mpmath might be broken by these rearrangements of what is publicly importable. It can be difficult to see exactly how some change affects what is importable because things might even be implicitly imported (like the mpmath.rational module which was not explicitly imported in mpmath/__init__.py).

@skirpichev
Copy link
Collaborator

I wonder more generally though what should be expected in terms of backwards compatibility here.

Yes, I think we should decide on what is a part of the API. Or what is not.

things might even be implicitly imported

Well, you could import anything, including underscore-prefixed names.

I think we should be conservative: the current API is anything that is imported into the global mpmath namespace (i.e. with from mpmath import *) or is documented with our prose docs. This is already big enough:

>>> len(dir(mpmath))
397

Or backward compatibility will block even simple changes. An example: #414. Clearly, to_str() has only support for decimal floating point numbers and it should be more generic, but if it is a part of the API - we can't easily change e.g. the dps parameter name (without a deprecation period and so on).

from .ctx_mp_python import _mpf, _mpc, mpnumeric

On another hand, instead of introducing these unused symbols yet in another private module, you could just import the mpnumeric from mpmath.ctx_mp_python... Simple fix, that will work for the master version as well as for released versions of the mpmath.

I'm not sure if there ever is a reason that sympy would need to convert an mpmath mpq.

In fact, this converter was restored in sympy/sympy@751502e by you:) Do you realize why?

I think it could be removed.

@oscarbenjamin
Copy link
Author

This is already big enough:

>>> len(dir(mpmath))
397

With 1.3.0 there are 400 names there and the difference is:

>>> set(names_130) - set(names_master)
{'doctests', 'rational', 'runtests'}

Probably only rational is something that downstream code might be expected to use from there.

Simple fix, that will work for the master version as well as for released versions of the mpmath.

It is not hard to change sympy to work with all versions of mpmath but we also need to remember that other code might be broken by these things. Also the incompatibility between older sympy versions and newer mpmath versions cannot be resolved easily because it is not possible to add a dependency version constraint to existing releases.

backward compatibility will block even simple changes.

With all changes backwards compatibility needs to be considered. There can be good reasons for breaking compatibility but it should not be done without any good reason. The mpq type is an example of something where it is not clear if it was supposed to be public or how it should be imported or used before.

skirpichev added a commit to skirpichev/mpmath that referenced this issue May 22, 2023
@skirpichev
Copy link
Collaborator

Probably only rational is something that downstream code might be expected to use from there.
... The mpq type is an example of something where it is not clear if it was supposed to be public

I think it should be perfectly clear that the code that "only intended for internal use" is, well, "only intended for internal use".

Also the incompatibility between older sympy versions and newer mpmath versions cannot be resolved easily because it is not possible to add a dependency version constraint to existing releases.

I'll add __getattr__ helper for the ctx_mp module, that will provide the mpnumeric attribute with a warning. I think this will fix the current issue, but I'll not close it. Instead, I would suggest retitle it somehow to address our API policy.

With all changes backwards compatibility needs to be considered.

It will be better, if we first could factor out something, that we could call API.

@asmeurer
Copy link
Contributor

I'm not sure if there ever is a reason that sympy would need to convert an mpmath mpq.

This code was added in sympy/sympy@1edecf7 in sympy/sympy#14348

@skirpichev
Copy link
Collaborator

I saw this, but it doesn't explain why SymPy uses this class. Another (only?) use case is the mpelements.py. This block could trivially be changed to use Fraction's.

Unfortunately, proposed above solution will not work:

diff --git a/mpmath/rational.py b/mpmath/rational.py
new file mode 100644
index 0000000..7fbc2ca
--- /dev/null
+++ b/mpmath/rational.py
@@ -0,0 +1 @@
+from .libmp import MPQ as mpq

The old mpq class has a slightly different interface (with p/q attributes, instead of numerator/denominator, despite claiming to support the numbers.Rational interface). The only option I see is restoring this internal class as is and then deprecate it. This sounds rather silly for me: usually "internal use" interfaces by definition could be broken without a warning...

@oscarbenjamin
Copy link
Author

The only option I see is restoring this internal class as is and then deprecate it.

I think that if it is not needed any more by mpmath itself then it is fine to remove it. If there are cases though where mpmath might return an MPQ then there should be some public way for downstream code to access the MPQ type.

SymPy's use of mpmath's mpq is just to convert it into something else. It is only relevant in so far as some mpmath interface might return an mpq that would need to be converted. I'm not sure how or when that would happen so I don't know if it is really necessary. Probably SymPy has no need for this and could just remove it.

This sounds rather silly for me: usually "internal use" interfaces by definition could be broken without a warning...

We have to be careful about this. A general problem in Python is that it is not often clear what is internal vs public and by default everything is made publicly accessible by the language itself. Relying on documentation to clarify this is problematic because unless all docs consistently describe what is or is not public then we cannot really expect that someone would even look at the docs to discover it.

@skirpichev
Copy link
Collaborator

If there are cases though where mpmath might return an MPQ

I doubt there are.

there should be some public way for downstream code to access the MPQ type

It's just a numbers.Rational implementation (Fraction or gmpy2.mpq). BTW, you can import this type from the backend module (like MPZ).

SymPy's use of mpmath's mpq is just to convert it into something else.

I don't think there is a real need for this.

Relying on documentation to clarify this is problematic because unless all docs consistently describe what is or is not public

My proposal above is coming from the PEP 8: "All undocumented interfaces should be assumed to be internal." (c)

I'm open to other suggestions. I realize, that some projects (mostly sympy;)) use much more "private" and/or low-level API of the mpmath (e.g. functions from libmp and so on) and we must take this situation into account. (I hope it's temporary, because SymPy's mpmath integration looks as a half-finished to me.)

by default everything is made publicly accessible by the language itself

That's not true, e.g. PEP 8 has leading underscore special convention as a weak "internal use" indicator. Another mechanism, suggested by PEP 8 is using __all__ module attribute to declare its public API.

skirpichev added a commit to skirpichev/mpmath that referenced this issue Jun 3, 2023
skirpichev added a commit to skirpichev/mpmath that referenced this issue Jun 3, 2023
skirpichev added a commit to skirpichev/mpmath that referenced this issue Jun 3, 2023
skirpichev added a commit to skirpichev/mpmath that referenced this issue Jun 3, 2023
@skirpichev
Copy link
Collaborator

With #703 the issue with mpnumeric import should be fixed. I think, that other stuff (related to using mpmath.rational.mpq) should be fixed on the SymPy side.

Let's rename this issue somehow, to reflect the fact it's about the mpmath API.

@skirpichev skirpichev added this to the next-release milestone Jun 22, 2023
@skirpichev skirpichev changed the title import sympy fails with mpmath master Public API Sep 5, 2023
@skirpichev
Copy link
Collaborator

A general problem in Python is that it is not often clear what is internal vs public and by default everything is made publicly accessible by the language itself.

The PEP 8 has this guideline: "Documented interfaces are considered public, unless the documentation explicitly declares them to be provisional or internal interfaces exempt from the usual backwards compatibility guarantees. All undocumented interfaces should be assumed to be internal."

There is a slight uncertainty in what we should treat as "a documented interface", i.e. if it just has a docstring or if it was mentioned also in the sphinx docs. I think we should use the second version.

With a simple GH search is seems that the most heavy user of mpmath.libmp.* stuff is the SymPy. So I suggest to consider these functions (i.e. only a subset, imported by sympy) to be public as well for a while. As an undocumented feature. Does it make sense for sympy people?

In a long term, I think it's a good idea to rewrite code in dependents like the Sympy. The only good reason to not use high-level classes like mpf is #696. But some imports could be removed right now. I.e. why depend on the bitcount()? Just use bit_length() method!

@oscarbenjamin
Copy link
Author

In a long term, I think it's a good idea to rewrite code in dependents like the Sympy.

I agree. I would be happy to do that immediately in SymPy if we had a clear list of what should not be used.

Currently:

$ git grep import | grep 'mpmath\.'
... < lots of stuff >

Mostly it is:

  • prec_to_dps, dps_to_prec, repr_dps, to_str

Removing those we are left with:

sympy/combinatorics/permutations.py:from mpmath.libmp.libintmath import ifac
sympy/core/evalf.py:import mpmath.libmp as libmp
sympy/core/evalf.py:from mpmath.libmp import (from_int, from_man_exp, from_rational, fhalf,
sympy/core/evalf.py:from mpmath.libmp import bitcount as mpmath_bitcount
sympy/core/evalf.py:from mpmath.libmp.backend import MPZ
sympy/core/evalf.py:from mpmath.libmp.libmpc import _infs_nan
sympy/core/numbers.py:import mpmath.libmp as mlib
sympy/core/numbers.py:from mpmath.libmp import bitcount, round_nearest as rnd
sympy/core/numbers.py:from mpmath.libmp.backend import MPZ
sympy/core/numbers.py:from mpmath.libmp import mpf_pow, mpf_pi, mpf_e, phi_fixed
sympy/core/numbers.py:from mpmath.ctx_mp_python import mpnumeric
sympy/core/numbers.py:from mpmath.libmp.libmpf import (
sympy/core/numbers.py:    from mpmath.libmp.backend import MPZ
sympy/core/tests/test_evalf.py:from mpmath.libmp.libmpf import from_float, fzero
sympy/core/tests/test_evalf.py:    from mpmath.libmp import finf
sympy/core/tests/test_numbers.py:    from mpmath.libmp.libmpf import _normalize
sympy/core/tests/test_numbers.py:    import mpmath.libmp as mlib
sympy/core/tests/test_numbers.py:    from mpmath.libmp.libmpf import fnan
sympy/core/tests/test_numbers.py:    from mpmath.libmp.libmpf import finf, fninf
sympy/external/ntheory.py:import mpmath.libmp as mlib
sympy/functions/combinatorial/numbers.py:from mpmath.libmp import ifib as _ifib
sympy/matrices/immutable.py:from mpmath.matrices.matrices import _matrix
sympy/ntheory/partitions_.py:from mpmath.libmp import (fzero, from_int, from_rational,
sympy/physics/quantum/constants.py:import mpmath.libmp as mlib
sympy/physics/quantum/qubit.py:from mpmath.libmp.libintmath import bitcount
sympy/polys/domains/mpelements.py:from mpmath.ctx_mp_python import PythonMPContext, _mpf, _mpc, _constant
sympy/polys/domains/mpelements.py:from mpmath.libmp import (MPZ_ONE, fzero, fone, finf, fninf, fnan,
sympy/polys/polytools.py:from mpmath.libmp.libhyper import NoConvergence
sympy/polys/ring_series.py:from mpmath.libmp.libintmath import ifac
sympy/polys/ring_series.py:from mpmath.libmp.libintmath import giant_steps

There are three places that import lots of things. In evalf we have

import mpmath.libmp as libmp
from mpmath import (
    make_mpc, make_mpf, mp, mpc, mpf, nsum, quadts, quadosc, workprec)
from mpmath import inf as mpmath_inf
from mpmath.libmp import (from_int, from_man_exp, from_rational, fhalf,
        fnan, finf, fninf, fnone, fone, fzero, mpf_abs, mpf_add,
        mpf_atan, mpf_atan2, mpf_cmp, mpf_cos, mpf_e, mpf_exp, mpf_log, mpf_lt,
        mpf_mul, mpf_neg, mpf_pi, mpf_pow, mpf_pow_int, mpf_shift, mpf_sin,
        mpf_sqrt, normalize, round_nearest, to_int, to_str)
from mpmath.libmp import bitcount as mpmath_bitcount
from mpmath.libmp.backend import MPZ
from mpmath.libmp.libmpc import _infs_nan

In numbers.py we have

import mpmath
import mpmath.libmp as mlib
from mpmath.libmp import bitcount, round_nearest as rnd
from mpmath.libmp.backend import MPZ
from mpmath.libmp import mpf_pow, mpf_pi, mpf_e, phi_fixed
from mpmath.ctx_mp_python import mpnumeric
from mpmath.libmp.libmpf import (
    finf as _mpf_inf, fninf as _mpf_ninf,
    fnan as _mpf_nan, fzero, _normalize as mpf_normalize,
    prec_to_dps, dps_to_prec)

partitions.py has

from mpmath.libmp import (fzero, from_int, from_rational,
    fone, fhalf, bitcount, to_int, to_str, mpf_mul, mpf_div, mpf_sub,
    mpf_add, mpf_sqrt, mpf_pi, mpf_cosh_sinh, mpf_cos, mpf_sin)

mpelements.py has

from mpmath.ctx_mp_python import PythonMPContext, _mpf, _mpc, _constant
from mpmath.libmp import (MPZ_ONE, fzero, fone, finf, fninf, fnan,
    round_nearest, mpf_mul, repr_dps, int_types,
    from_int, from_float, from_str, to_rational)

The most invasive thing is subclassing MPContext:
https://github.com/sympy/sympy/blob/a95eb0860343ae245499cb1ada0573495cafacdc/sympy/polys/domains/mpelements.py#L45

Some things could easily be removed like bitcount as you say. Others like prec_to_dps should at least be defined or imported in a compatibility layer rather than imported directly from mpmath all over the codebase.

I'm not sure why there is a subclass of MPContext but I would definitely like to remove that: it is a bad idea to have a subclass in a different code base from its superclass because it is too strong a coupling.

Otherwise do you see anything obvious that should be considered private to mpmath?

@skirpichev
Copy link
Collaborator

I would be happy to do that immediately in SymPy if we had a clear list of what should not be used.

All mpmath.libmp.* stuff. Why not use high-level interfaces (from mpmath import *) instead? E.g. the mpf type in the Float class? Using low-level functions from the libmp seems to be a historical artifact. Too tight binding with the mpmath, unstable (nonexistent?) high-level interfaces at time of writing the evalf module and so on.

@oscarbenjamin
Copy link
Author

All mpmath.libmp.* stuff. Why not use high-level interfaces (from mpmath import *) instead? E.g. the mpf type in the Float class?

Okay, I agree with this. However looking at it a little bit this is not quite as simple as I hoped.

The highest level mpmath APIs all use a global precision in the mpmath module context objects. At minimum SymPy should be using its own MPContext for isolation from any users messing with mpmath.mp.dps. However there are a mixture of uses of mpmath already in the SymPy codebase with some parts depending on (and mutating!) mpmath's global context. Most of SymPy's use of mpmath does not depend on the global context settings precisely because it uses the libmp things but some functions are only usable by manipulating the global context e.g.:
https://github.com/sympy/sympy/blob/2c96e3de418f78703c04dd73234b81df015f72a8/sympy/core/function.py#L1612-L1629

@skirpichev
Copy link
Collaborator

The highest level mpmath APIs all use a global precision in the mpmath module context objects.

Yes, that's an issue and we have #683.

At minimum SymPy should be using its own MPContext for isolation from any users messing with mpmath.mp.dps.

The domains module code already does this, but with a some reinvention of the MPContext class.

@skirpichev
Copy link
Collaborator

skirpichev commented Oct 19, 2023

With some refactoring of mpmath.libmp imports in the diofant (diofant/diofant#1357 and diofant/diofant#1361) I got this list:

$ python ~/s.py .
['ComplexResult', 'MPZ', 'MPZ_ONE', 'NoConvergence', 'dps_to_prec', 'fhalf',
 'finf', 'fnan', 'fninf', 'fnone', 'fone', 'from_Decimal', 'from_float', 'from_int',
 'from_man_exp', 'from_rational', 'from_str', 'fzero', 'ifac', 'ifib', 'int_types', 'mpc_abs',
 'mpc_exp', 'mpc_pow', 'mpc_pow_int', 'mpc_pow_mpf', 'mpc_sqrt', 'mpf_abs', 'mpf_add',
 'mpf_atan', 'mpf_atan2', 'mpf_bernoulli', 'mpf_ceil', 'mpf_cmp', 'mpf_cos', 'mpf_cosh_sinh',
 'mpf_div', 'mpf_eq', 'mpf_exp', 'mpf_floor', 'mpf_ge', 'mpf_gt', 'mpf_le', 'mpf_log',
 'mpf_lt', 'mpf_mod', 'mpf_mul', 'mpf_neg', 'mpf_pi', 'mpf_pow', 'mpf_pow_int', 'mpf_shift',
 'mpf_sin', 'mpf_sqrt', 'mpf_sub', 'normalize', 'pi_fixed', 'prec_to_dps', 'repr_dps',
 'round_nearest', 'sqrtrem', 'to_float', 'to_int', 'to_rational', 'to_str']

All imports are from mpmath.libmp namespace, except for the giant_steps() function (which was added to in #722).

I would expect same situation for the SymPy.

Script to find imports It should be possible to add few more cases to workaround various import patterns in the Sympy.
import ast
import os
import sys


sys.setrecursionlimit(10000)  # for sympy

libmp_imports = set()


class ImportVisitor(ast.NodeVisitor):
    def visit_ImportFrom(self, node):
        if node.module == 'mpmath.libmp':
            for n in node.names:
                libmp_imports.add(n.name)


for root, sdirs, files in os.walk(sys.argv[1]):
    for fname in files:
        if fname.endswith('.py'):
            with open(os.path.join(root, fname), 'r') as file:
                source = file.read()
            tree = ast.parse(source)
            ImportVisitor().visit(tree)

print(sorted(libmp_imports))

@oscarbenjamin
Copy link
Author

Thinking about this a bit I think that what SymPy really needs is pure functions like mpf_add that take precision as an argument rather than anything based on contexts or global precision settings. Otherwise it is just awkward to make things thread-safe and to manage a global context etc. It would be better if there were functions that worked with an mpf type rather than just tuples but otherwise I don't think that using contexts is what is wanted.

If the functions in libmp are to be considered private then I don't see what the public API is that replaces them.

@skirpichev
Copy link
Collaborator

what SymPy really needs is pure functions like mpf_add that take precision as an argument rather than anything based on contexts or global precision settings. Otherwise it is just awkward to make things thread-safe and to manage a global context etc.

That's true in the current shape. But we have #683 for this. So, I don't see here big obstacles to use (eventually) a high-level interface (context & mpf/mpc classes).

It would be better if there were functions that worked with an mpf type

And that's definitely possible, see e.g. gmpy2. I think this kind of high-level API is better.

If the functions in libmp are to be considered private

No. But I definitely would like to restrict this part of the API to some minimum, based on requirements of major its consumers (SymPy).

@oscarbenjamin
Copy link
Author

It looks like you are preparing for a new release of mpmath. Currently mpmath 1.4.0 is incompatible with the latest release of sympy (1.12) that is available on PyPI so an mpmath release right now would break sympy.

I am making changes to sympy to account for this (sympy/sympy#26269) and aim to put out a sympy 1.13 release soon.

I think though that before there is an mpmath release one or (ideally) both of these things are needed:

  1. sympy should have a release that will be compatible with mpmath 1.4.0.
  2. mpmath should have some compatibility code so that it works with sympy 1.12 (if only with deprecation warnings).

Both sympy and sage reach deeply into mpmath (see also #727 (comment)). It would be good to reduce that but also it would be good for a new release of mpmath not to have changes that are known to be breaking for at least some downstream projects. It seems likely that other projects would also be affected by these changes as well.

@skirpichev
Copy link
Collaborator

skirpichev commented Feb 25, 2024

It looks like you are preparing for a new release of mpmath.

No. It's far, far away... Some issues I just can't solve alone, e.g.: #709

But I think it's a good idea to do alpha release.

mpmath should have some compatibility code so that it works with sympy 1.12 (if only with deprecation warnings).

So far, we should restore some code for the private rational module and to_pickable() function, right? BTW, you could use simpler code for the Float.__getnewargs_ex__(): mpz's actually can be pickled. See diofant's code.

@oscarbenjamin
Copy link
Author

So far, we should restore some code for the private rational module and to_pickable() function, right?

The only other thing I noticed was a change to parsing that I accounted for in sympy/sympy@eef227f but that was just to reject certain string inputs.

I'm not sure how likely it is that other projects might depend on these things besides the obvious ones. It seems likely though that if mpmath goes from being relatively dormant to making more active changes then some things might pop out.

@oscarbenjamin
Copy link
Author

I'm not sure if some people are now picking up the mpmath prerelease somehow (sympy/sympy#26273).

@cbm755
Copy link
Contributor

cbm755 commented Feb 26, 2024

It looks correct on PyPI to me, so probably people are using --pre:

image

@skirpichev
Copy link
Collaborator

It looks correct on PyPI to me, so probably people are using --pre:

Or add a specific version requirements. But not in this case, unfortunately.

I'm not sure if some people are now picking up the mpmath prerelease somehow (sympy/sympy#26273).

People here are using ancient packaging techniques (setup.py install, etc). These doesn't ignore pre-release (per PEP 440) versions. I think this issue is relevant: pypa/setuptools#855. You also could reproduce that with a simple setup.py (and you will see a lot of DeprecationWarning's!):

from setuptools import setup
setup(name='a',
      version='0.1',
      #install_requires = ['mpmath<=1.3.0'],
      install_requires = ['mpmath'],
      packages = ['a'])

Commented out line should help sympy people to solve sympy/sympy#26273.

@skirpichev
Copy link
Collaborator

I am making changes to sympy to account for this (sympy/sympy#26269) and aim to put out a sympy 1.13 release soon.

#769 will include some compatibility code for this. I did basic local tests: sympy v1.12 imports, pass tests in sympy/core/tests/test_numbers.py and sympy/core/tests/test_sympify.py, pickling - works for Float's.

@skirpichev
Copy link
Collaborator

It seems, this is the libmp namespace, used both SymPy (since v1.12) and latest Diofant (diofant_imports, sympy_imports and sympy_112_imports are from run of find-libmp.py script on the latest Diofant, SymPy and sympy-1.12, respectively):

In [7]: sympy_imports.issubset(sympy_112_imports)
Out[7]: True

In [8]: diofant_import.issubset(sympy_112_imports)
Out[8]: False

In [9]: {_ for _ in diofant_import|sympy_112_imports if not _.startswith('_')}
Out[9]:
{ComplexResult, MPZ, MPZ_ONE, NoConvergence, bin_to_radix, bitcount, catalan_fixed,
dps_to_prec, fhalf, finf, fnan, fninf, fnone, fone, from_float, from_int, from_man_exp,
from_rational, from_str, fzero, giant_steps, ifac, ifib, int_types, isqrt, mpc_abs, mpc_exp,
mpc_pow, mpc_pow_int, mpc_pow_mpf, mpc_sqrt, mpf_abs, mpf_add, mpf_atan, mpf_atan2,
mpf_bernoulli, mpf_ceil, mpf_cmp, mpf_cos, mpf_cosh_sinh, mpf_div, mpf_e, mpf_eq, mpf_exp,
mpf_floor, mpf_ge, mpf_gt, mpf_le, mpf_log, mpf_lt, mpf_mod, mpf_mul, mpf_neg, mpf_pi, mpf_pow,
mpf_pow_int, mpf_shift, mpf_sign, mpf_sin, mpf_sqrt, mpf_sub, normalize, numeral, phi_fixed,
prec_to_dps, repr_dps, round_nearest, sqrtrem, to_float, to_int, to_man_exp, to_pickable,
to_rational, to_str}

In [10]: len(_9)
Out[10]: 74
In [11]: mpmath_libmp_names-_9
Out[11]: 
{BACKEND, MPQ, MPZ_FIVE, MPZ_THREE, MPZ_TWO, MPZ_ZERO, agm_fixed, apery_fixed, bernfrac,
complex_int_pow, degree_fixed, e_fixed, euler_fixed, eulernum, fnzero, from_Decimal,
from_npfloat, from_pickable, ften, ftwo, gcd, glaisher_fixed, gmpy, isprime, isqrt_fast,
isqrt_small, khinchin_fixed, list_primes, ln10_fixed, ln2_fixed, log_int_fixed,
make_hyp_summator, mertens_fixed, moebius, mpc_acos, mpc_acosh, mpc_add, mpc_add_mpf,
mpc_agm, mpc_agm1, mpc_altzeta, mpc_arg, mpc_asin, mpc_asinh, mpc_atan, mpc_atanh,
mpc_besseljn, mpc_cbrt, mpc_ceil, mpc_ci, mpc_conjugate, mpc_cos, mpc_cos_pi, mpc_cos_sin,
mpc_cos_sin_pi, mpc_cosh, mpc_div, mpc_div_mpf, mpc_e1, mpc_ei, mpc_ellipe, mpc_ellipk,
mpc_expj, mpc_expjpi, mpc_factorial, mpc_fibonacci, mpc_floor, mpc_frac, mpc_gamma, mpc_half,
mpc_harmonic, mpc_hash, mpc_is_inf, mpc_is_infnan, mpc_is_nonzero, mpc_log, mpc_loggamma,
mpc_mpf_div, mpc_mul, mpc_mul_int, mpc_mul_mpf, mpc_neg, mpc_nint, mpc_nthroot, mpc_one,
mpc_pos, mpc_psi, mpc_psi0, mpc_reciprocal, mpc_rgamma, mpc_shift, mpc_si, mpc_sin,
mpc_sin_pi, mpc_sinh, mpc_square, mpc_sub, mpc_sub_mpf, mpc_tan, mpc_tanh, mpc_to_complex,
mpc_to_str, mpc_two, mpc_zero, mpc_zeta, mpc_zetasum, mpci_abs, mpci_add, mpci_cos,
mpci_div, mpci_exp, mpci_factorial, mpci_gamma, mpci_log, mpci_loggamma, mpci_mul, mpci_neg,
mpci_pos, mpci_pow, mpci_rgamma, mpci_sin, mpci_sub, mpf_acos, mpf_acosh, mpf_agm,
mpf_agm1, mpf_altzeta, mpf_apery, mpf_asin, mpf_asinh, mpf_atanh, mpf_besseljn, mpf_catalan,
mpf_cbrt, mpf_ci, mpf_ci_si, mpf_cos_pi, mpf_cos_sin, mpf_cos_sin_pi, mpf_cosh, mpf_degree,
mpf_e1, mpf_ei, mpf_ellipe, mpf_ellipk, mpf_erf, mpf_erfc, mpf_euler, mpf_expint,
mpf_expj, mpf_expjpi, mpf_factorial, mpf_fibonacci, mpf_frac, mpf_frexp, mpf_gamma,
mpf_gamma_int, mpf_glaisher, mpf_harmonic, mpf_hash, mpf_hypot, mpf_khinchin, mpf_ln10,
mpf_ln2, mpf_log_hypot, mpf_loggamma, mpf_mertens, mpf_mul_int, mpf_nint, mpf_nthroot,
mpf_perturb, mpf_phi, mpf_pos, mpf_psi, mpf_psi0, mpf_rand, mpf_rdiv_int, mpf_rgamma,
mpf_si, mpf_sin_pi, mpf_sinh, mpf_sum, mpf_tan, mpf_tanh, mpf_twinprime, mpf_zeta,
mpf_zeta_int, mpf_zetasum, mpi_abs, mpi_add, mpi_atan, mpi_atan2, mpi_cos, mpi_cos_sin,
mpi_cot, mpi_delta, mpi_div, mpi_eq, mpi_exp, mpi_factorial, mpi_from_str, mpi_gamma, mpi_ge,
mpi_gt, mpi_le, mpi_log, mpi_loggamma, mpi_lt, mpi_mid, mpi_mul, mpi_ne, mpi_neg, mpi_pos,
mpi_pow, mpi_pow_int, mpi_rgamma, mpi_sin, mpi_sqrt, mpi_str, mpi_sub, mpi_tan, mpi_to_str,
pi_fixed, round_ceiling, round_down, round_floor, round_int, round_up, sqrt_fixed, stirling1,
stirling2, str_to_man_exp, to_digits_exp, to_fixed, trailing, twinprime_fixed}

SymPy also uses private names: {'_normalize', '_infs_nan'}.

Lets start from the following namespace of mpmath.libmp: _9. The rest (_11) should be treated as private and may be removed from the libmp namespace, unless mentioned in docs (as BACKEND, for example).

script find-libmp.py
import ast
import os
import sys

import mpmath


sys.setrecursionlimit(10000)  # for sympy

libmp_imports = set()


class ImportVisitor(ast.NodeVisitor):
    def __init__(self):
        self.names = set()

    def visit_ImportFrom(self, node):
        # from mpmath.libmp import foo, bar
        if node.module == 'mpmath.libmp':
            for n in node.names:
                libmp_imports.add(n.name)
        if node.module and node.module.startswith('mpmath.libmp.'):
            # from mpmath.libmp.libmpf import foo
            for n in node.names:
                libmp_imports.add(n.name)
            m = node.module
            m = m.replace('mpmath.libmp.', '')
            assert '.' not in m
            libmp_imports.add(m)
        # from mpmath import foo, bar, libmp
        if node.module == 'mpmath':
            for n in node.names:
                if n.name == 'libmp':
                    self.names.add(n.asname if n.asname else n.name)

    def visit_Import(self, node):
        # import mpmath.libmp
        # import mpmath.libmp as libm
        # import mpmath.libmp.libmpf
        # import mpmath.libmp.backend as b
        for n in node.names:
            if n.name == 'mpmath.libmp':
                self.names.add(n.asname if n.asname else n.name)
            elif n.name.startswith('mpmath.libmp.'):
                self.names.add(n.asname if n.asname else n.name)

    def visit_Attribute(self, node):
        v = node.value
        if isinstance(v, ast.Name):
            if v.id in self.names:
                a = node.attr
                assert a in dir(mpmath.libmp)
                libmp_imports.add(a)
        if isinstance(v, ast.Attribute):
            if isinstance(v.value, ast.Name):
                if v.value.id == 'mpmath' and v.attr == 'libmp':
                    a = node.attr
                    assert a in dir(mpmath.libmp)
                    libmp_imports.add(a)


for root, sdirs, files in os.walk(sys.argv[1]):
    for fname in files:
        if fname.endswith('.py'):
            with open(os.path.join(root, fname), 'r') as file:
                source = file.read()
            tree = ast.parse(source)
            ImportVisitor().visit(tree)

libmp_imports -= {'backend', 'gammazeta', 'libelefun', 'libhyper',
                  'libintmath', 'libmpc', 'libmpf', 'libmpi'}


print("mpmath.libmp imports:")
print(set(sorted(libmp_imports)))

print("missing in latest namespace:")
print(set(sorted(_ for _ in libmp_imports if _ not in dir(mpmath.libmp))))

@oscarbenjamin
Copy link
Author

A new error for SymPy and mpmath master:

>>> import sympy
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/enojb/work/dev/sympy/sympy/__init__.py", line 74, in <module>
    from .polys import (Poly, PurePoly, poly_from_expr, parallel_poly_from_expr,
  File "/Users/enojb/work/dev/sympy/sympy/polys/__init__.py", line 68, in <module>
    from .polytools import (Poly, PurePoly, poly_from_expr,
  File "/Users/enojb/work/dev/sympy/sympy/polys/polytools.py", line 27, in <module>
    from sympy.polys.constructor import construct_domain
  File "/Users/enojb/work/dev/sympy/sympy/polys/constructor.py", line 7, in <module>
    from sympy.polys.domains import ZZ, QQ, ZZ_I, QQ_I, EX
  File "/Users/enojb/work/dev/sympy/sympy/polys/domains/__init__.py", line 17, in <module>
    from .realfield import RealField, RR
  File "/Users/enojb/work/dev/sympy/sympy/polys/domains/realfield.py", line 167, in <module>
    RR = RealField()
         ^^^^^^^^^^^
  File "/Users/enojb/work/dev/sympy/sympy/polys/domains/realfield.py", line 47, in __init__
    context = MPContext(prec, dps, tol, True)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/enojb/work/dev/sympy/sympy/polys/domains/mpelements.py", line 51, in __init__
    ctx._set_prec(prec)
  File "/Users/enojb/work/dev/mpmath/mpmath/ctx_mp_python.py", line 777, in _set_prec
    if not ctx.isfinite(n):
           ^^^^^^^^^^^^
AttributeError: 'MPContext' object has no attribute 'isfinite'

This one is coming from dcc591c (gh-792).

In SymPy mpmath's PythonMPContext is subclassed (I would like to remove this). The subclass does not define isfinite the way that MPContext does. What this means is that PythonMPContext._set_prec does not work any more except in the MPContext subclass.

@skirpichev
Copy link
Collaborator

In SymPy mpmath's PythonMPContext is subclassed (I would like to remove this).

Good idea. After all, this class seems to be a private one. It's not documented or exported to the global namespace.

The subclass does not define isfinite the way that MPContext does.

We could use ctx.isinf() insted. Or move isfinite method.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants