635 lines
20 KiB
Python
635 lines
20 KiB
Python
"""sympify -- convert objects SymPy internal format"""
|
|
|
|
from __future__ import annotations
|
|
from typing import Any, Callable
|
|
|
|
from inspect import getmro
|
|
import string
|
|
from sympy.core.random import choice
|
|
|
|
from .parameters import global_parameters
|
|
|
|
from sympy.utilities.exceptions import sympy_deprecation_warning
|
|
from sympy.utilities.iterables import iterable
|
|
|
|
|
|
class SympifyError(ValueError):
|
|
def __init__(self, expr, base_exc=None):
|
|
self.expr = expr
|
|
self.base_exc = base_exc
|
|
|
|
def __str__(self):
|
|
if self.base_exc is None:
|
|
return "SympifyError: %r" % (self.expr,)
|
|
|
|
return ("Sympify of expression '%s' failed, because of exception being "
|
|
"raised:\n%s: %s" % (self.expr, self.base_exc.__class__.__name__,
|
|
str(self.base_exc)))
|
|
|
|
|
|
converter: dict[type[Any], Callable[[Any], Basic]] = {}
|
|
|
|
#holds the conversions defined in SymPy itself, i.e. non-user defined conversions
|
|
_sympy_converter: dict[type[Any], Callable[[Any], Basic]] = {}
|
|
|
|
#alias for clearer use in the library
|
|
_external_converter = converter
|
|
|
|
class CantSympify:
|
|
"""
|
|
Mix in this trait to a class to disallow sympification of its instances.
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import sympify
|
|
>>> from sympy.core.sympify import CantSympify
|
|
|
|
>>> class Something(dict):
|
|
... pass
|
|
...
|
|
>>> sympify(Something())
|
|
{}
|
|
|
|
>>> class Something(dict, CantSympify):
|
|
... pass
|
|
...
|
|
>>> sympify(Something())
|
|
Traceback (most recent call last):
|
|
...
|
|
SympifyError: SympifyError: {}
|
|
|
|
"""
|
|
|
|
__slots__ = ()
|
|
|
|
|
|
def _is_numpy_instance(a):
|
|
"""
|
|
Checks if an object is an instance of a type from the numpy module.
|
|
"""
|
|
# This check avoids unnecessarily importing NumPy. We check the whole
|
|
# __mro__ in case any base type is a numpy type.
|
|
return any(type_.__module__ == 'numpy'
|
|
for type_ in type(a).__mro__)
|
|
|
|
|
|
def _convert_numpy_types(a, **sympify_args):
|
|
"""
|
|
Converts a numpy datatype input to an appropriate SymPy type.
|
|
"""
|
|
import numpy as np
|
|
if not isinstance(a, np.floating):
|
|
if np.iscomplex(a):
|
|
return _sympy_converter[complex](a.item())
|
|
else:
|
|
return sympify(a.item(), **sympify_args)
|
|
else:
|
|
try:
|
|
from .numbers import Float
|
|
prec = np.finfo(a).nmant + 1
|
|
# E.g. double precision means prec=53 but nmant=52
|
|
# Leading bit of mantissa is always 1, so is not stored
|
|
a = str(list(np.reshape(np.asarray(a),
|
|
(1, np.size(a)))[0]))[1:-1]
|
|
return Float(a, precision=prec)
|
|
except NotImplementedError:
|
|
raise SympifyError('Translation for numpy float : %s '
|
|
'is not implemented' % a)
|
|
|
|
|
|
def sympify(a, locals=None, convert_xor=True, strict=False, rational=False,
|
|
evaluate=None):
|
|
"""
|
|
Converts an arbitrary expression to a type that can be used inside SymPy.
|
|
|
|
Explanation
|
|
===========
|
|
|
|
It will convert Python ints into instances of :class:`~.Integer`, floats
|
|
into instances of :class:`~.Float`, etc. It is also able to coerce
|
|
symbolic expressions which inherit from :class:`~.Basic`. This can be
|
|
useful in cooperation with SAGE.
|
|
|
|
.. warning::
|
|
Note that this function uses ``eval``, and thus shouldn't be used on
|
|
unsanitized input.
|
|
|
|
If the argument is already a type that SymPy understands, it will do
|
|
nothing but return that value. This can be used at the beginning of a
|
|
function to ensure you are working with the correct type.
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy import sympify
|
|
|
|
>>> sympify(2).is_integer
|
|
True
|
|
>>> sympify(2).is_real
|
|
True
|
|
|
|
>>> sympify(2.0).is_real
|
|
True
|
|
>>> sympify("2.0").is_real
|
|
True
|
|
>>> sympify("2e-45").is_real
|
|
True
|
|
|
|
If the expression could not be converted, a SympifyError is raised.
|
|
|
|
>>> sympify("x***2")
|
|
Traceback (most recent call last):
|
|
...
|
|
SympifyError: SympifyError: "could not parse 'x***2'"
|
|
|
|
Locals
|
|
------
|
|
|
|
The sympification happens with access to everything that is loaded
|
|
by ``from sympy import *``; anything used in a string that is not
|
|
defined by that import will be converted to a symbol. In the following,
|
|
the ``bitcount`` function is treated as a symbol and the ``O`` is
|
|
interpreted as the :class:`~.Order` object (used with series) and it raises
|
|
an error when used improperly:
|
|
|
|
>>> s = 'bitcount(42)'
|
|
>>> sympify(s)
|
|
bitcount(42)
|
|
>>> sympify("O(x)")
|
|
O(x)
|
|
>>> sympify("O + 1")
|
|
Traceback (most recent call last):
|
|
...
|
|
TypeError: unbound method...
|
|
|
|
In order to have ``bitcount`` be recognized it can be imported into a
|
|
namespace dictionary and passed as locals:
|
|
|
|
>>> ns = {}
|
|
>>> exec('from sympy.core.evalf import bitcount', ns)
|
|
>>> sympify(s, locals=ns)
|
|
6
|
|
|
|
In order to have the ``O`` interpreted as a Symbol, identify it as such
|
|
in the namespace dictionary. This can be done in a variety of ways; all
|
|
three of the following are possibilities:
|
|
|
|
>>> from sympy import Symbol
|
|
>>> ns["O"] = Symbol("O") # method 1
|
|
>>> exec('from sympy.abc import O', ns) # method 2
|
|
>>> ns.update(dict(O=Symbol("O"))) # method 3
|
|
>>> sympify("O + 1", locals=ns)
|
|
O + 1
|
|
|
|
If you want *all* single-letter and Greek-letter variables to be symbols
|
|
then you can use the clashing-symbols dictionaries that have been defined
|
|
there as private variables: ``_clash1`` (single-letter variables),
|
|
``_clash2`` (the multi-letter Greek names) or ``_clash`` (both single and
|
|
multi-letter names that are defined in ``abc``).
|
|
|
|
>>> from sympy.abc import _clash1
|
|
>>> set(_clash1) # if this fails, see issue #23903
|
|
{'E', 'I', 'N', 'O', 'Q', 'S'}
|
|
>>> sympify('I & Q', _clash1)
|
|
I & Q
|
|
|
|
Strict
|
|
------
|
|
|
|
If the option ``strict`` is set to ``True``, only the types for which an
|
|
explicit conversion has been defined are converted. In the other
|
|
cases, a SympifyError is raised.
|
|
|
|
>>> print(sympify(None))
|
|
None
|
|
>>> sympify(None, strict=True)
|
|
Traceback (most recent call last):
|
|
...
|
|
SympifyError: SympifyError: None
|
|
|
|
.. deprecated:: 1.6
|
|
|
|
``sympify(obj)`` automatically falls back to ``str(obj)`` when all
|
|
other conversion methods fail, but this is deprecated. ``strict=True``
|
|
will disable this deprecated behavior. See
|
|
:ref:`deprecated-sympify-string-fallback`.
|
|
|
|
Evaluation
|
|
----------
|
|
|
|
If the option ``evaluate`` is set to ``False``, then arithmetic and
|
|
operators will be converted into their SymPy equivalents and the
|
|
``evaluate=False`` option will be added. Nested ``Add`` or ``Mul`` will
|
|
be denested first. This is done via an AST transformation that replaces
|
|
operators with their SymPy equivalents, so if an operand redefines any
|
|
of those operations, the redefined operators will not be used. If
|
|
argument a is not a string, the mathematical expression is evaluated
|
|
before being passed to sympify, so adding ``evaluate=False`` will still
|
|
return the evaluated result of expression.
|
|
|
|
>>> sympify('2**2 / 3 + 5')
|
|
19/3
|
|
>>> sympify('2**2 / 3 + 5', evaluate=False)
|
|
2**2/3 + 5
|
|
>>> sympify('4/2+7', evaluate=True)
|
|
9
|
|
>>> sympify('4/2+7', evaluate=False)
|
|
4/2 + 7
|
|
>>> sympify(4/2+7, evaluate=False)
|
|
9.00000000000000
|
|
|
|
Extending
|
|
---------
|
|
|
|
To extend ``sympify`` to convert custom objects (not derived from ``Basic``),
|
|
just define a ``_sympy_`` method to your class. You can do that even to
|
|
classes that you do not own by subclassing or adding the method at runtime.
|
|
|
|
>>> from sympy import Matrix
|
|
>>> class MyList1(object):
|
|
... def __iter__(self):
|
|
... yield 1
|
|
... yield 2
|
|
... return
|
|
... def __getitem__(self, i): return list(self)[i]
|
|
... def _sympy_(self): return Matrix(self)
|
|
>>> sympify(MyList1())
|
|
Matrix([
|
|
[1],
|
|
[2]])
|
|
|
|
If you do not have control over the class definition you could also use the
|
|
``converter`` global dictionary. The key is the class and the value is a
|
|
function that takes a single argument and returns the desired SymPy
|
|
object, e.g. ``converter[MyList] = lambda x: Matrix(x)``.
|
|
|
|
>>> class MyList2(object): # XXX Do not do this if you control the class!
|
|
... def __iter__(self): # Use _sympy_!
|
|
... yield 1
|
|
... yield 2
|
|
... return
|
|
... def __getitem__(self, i): return list(self)[i]
|
|
>>> from sympy.core.sympify import converter
|
|
>>> converter[MyList2] = lambda x: Matrix(x)
|
|
>>> sympify(MyList2())
|
|
Matrix([
|
|
[1],
|
|
[2]])
|
|
|
|
Notes
|
|
=====
|
|
|
|
The keywords ``rational`` and ``convert_xor`` are only used
|
|
when the input is a string.
|
|
|
|
convert_xor
|
|
-----------
|
|
|
|
>>> sympify('x^y',convert_xor=True)
|
|
x**y
|
|
>>> sympify('x^y',convert_xor=False)
|
|
x ^ y
|
|
|
|
rational
|
|
--------
|
|
|
|
>>> sympify('0.1',rational=False)
|
|
0.1
|
|
>>> sympify('0.1',rational=True)
|
|
1/10
|
|
|
|
Sometimes autosimplification during sympification results in expressions
|
|
that are very different in structure than what was entered. Until such
|
|
autosimplification is no longer done, the ``kernS`` function might be of
|
|
some use. In the example below you can see how an expression reduces to
|
|
$-1$ by autosimplification, but does not do so when ``kernS`` is used.
|
|
|
|
>>> from sympy.core.sympify import kernS
|
|
>>> from sympy.abc import x
|
|
>>> -2*(-(-x + 1/x)/(x*(x - 1/x)**2) - 1/(x*(x - 1/x))) - 1
|
|
-1
|
|
>>> s = '-2*(-(-x + 1/x)/(x*(x - 1/x)**2) - 1/(x*(x - 1/x))) - 1'
|
|
>>> sympify(s)
|
|
-1
|
|
>>> kernS(s)
|
|
-2*(-(-x + 1/x)/(x*(x - 1/x)**2) - 1/(x*(x - 1/x))) - 1
|
|
|
|
Parameters
|
|
==========
|
|
|
|
a :
|
|
- any object defined in SymPy
|
|
- standard numeric Python types: ``int``, ``long``, ``float``, ``Decimal``
|
|
- strings (like ``"0.09"``, ``"2e-19"`` or ``'sin(x)'``)
|
|
- booleans, including ``None`` (will leave ``None`` unchanged)
|
|
- dicts, lists, sets or tuples containing any of the above
|
|
|
|
convert_xor : bool, optional
|
|
If true, treats ``^`` as exponentiation.
|
|
If False, treats ``^`` as XOR itself.
|
|
Used only when input is a string.
|
|
|
|
locals : any object defined in SymPy, optional
|
|
In order to have strings be recognized it can be imported
|
|
into a namespace dictionary and passed as locals.
|
|
|
|
strict : bool, optional
|
|
If the option strict is set to ``True``, only the types for which
|
|
an explicit conversion has been defined are converted. In the
|
|
other cases, a SympifyError is raised.
|
|
|
|
rational : bool, optional
|
|
If ``True``, converts floats into :class:`~.Rational`.
|
|
If ``False``, it lets floats remain as it is.
|
|
Used only when input is a string.
|
|
|
|
evaluate : bool, optional
|
|
If False, then arithmetic and operators will be converted into
|
|
their SymPy equivalents. If True the expression will be evaluated
|
|
and the result will be returned.
|
|
|
|
"""
|
|
# XXX: If a is a Basic subclass rather than instance (e.g. sin rather than
|
|
# sin(x)) then a.__sympy__ will be the property. Only on the instance will
|
|
# a.__sympy__ give the *value* of the property (True). Since sympify(sin)
|
|
# was used for a long time we allow it to pass. However if strict=True as
|
|
# is the case in internal calls to _sympify then we only allow
|
|
# is_sympy=True.
|
|
#
|
|
# https://github.com/sympy/sympy/issues/20124
|
|
is_sympy = getattr(a, '__sympy__', None)
|
|
if is_sympy is True:
|
|
return a
|
|
elif is_sympy is not None:
|
|
if not strict:
|
|
return a
|
|
else:
|
|
raise SympifyError(a)
|
|
|
|
if isinstance(a, CantSympify):
|
|
raise SympifyError(a)
|
|
|
|
cls = getattr(a, "__class__", None)
|
|
|
|
#Check if there exists a converter for any of the types in the mro
|
|
for superclass in getmro(cls):
|
|
#First check for user defined converters
|
|
conv = _external_converter.get(superclass)
|
|
if conv is None:
|
|
#if none exists, check for SymPy defined converters
|
|
conv = _sympy_converter.get(superclass)
|
|
if conv is not None:
|
|
return conv(a)
|
|
|
|
if cls is type(None):
|
|
if strict:
|
|
raise SympifyError(a)
|
|
else:
|
|
return a
|
|
|
|
if evaluate is None:
|
|
evaluate = global_parameters.evaluate
|
|
|
|
# Support for basic numpy datatypes
|
|
if _is_numpy_instance(a):
|
|
import numpy as np
|
|
if np.isscalar(a):
|
|
return _convert_numpy_types(a, locals=locals,
|
|
convert_xor=convert_xor, strict=strict, rational=rational,
|
|
evaluate=evaluate)
|
|
|
|
_sympy_ = getattr(a, "_sympy_", None)
|
|
if _sympy_ is not None:
|
|
try:
|
|
return a._sympy_()
|
|
# XXX: Catches AttributeError: 'SymPyConverter' object has no
|
|
# attribute 'tuple'
|
|
# This is probably a bug somewhere but for now we catch it here.
|
|
except AttributeError:
|
|
pass
|
|
|
|
if not strict:
|
|
# Put numpy array conversion _before_ float/int, see
|
|
# <https://github.com/sympy/sympy/issues/13924>.
|
|
flat = getattr(a, "flat", None)
|
|
if flat is not None:
|
|
shape = getattr(a, "shape", None)
|
|
if shape is not None:
|
|
from sympy.tensor.array import Array
|
|
return Array(a.flat, a.shape) # works with e.g. NumPy arrays
|
|
|
|
if not isinstance(a, str):
|
|
if _is_numpy_instance(a):
|
|
import numpy as np
|
|
assert not isinstance(a, np.number)
|
|
if isinstance(a, np.ndarray):
|
|
# Scalar arrays (those with zero dimensions) have sympify
|
|
# called on the scalar element.
|
|
if a.ndim == 0:
|
|
try:
|
|
return sympify(a.item(),
|
|
locals=locals,
|
|
convert_xor=convert_xor,
|
|
strict=strict,
|
|
rational=rational,
|
|
evaluate=evaluate)
|
|
except SympifyError:
|
|
pass
|
|
else:
|
|
# float and int can coerce size-one numpy arrays to their lone
|
|
# element. See issue https://github.com/numpy/numpy/issues/10404.
|
|
for coerce in (float, int):
|
|
try:
|
|
return sympify(coerce(a))
|
|
except (TypeError, ValueError, AttributeError, SympifyError):
|
|
continue
|
|
|
|
if strict:
|
|
raise SympifyError(a)
|
|
|
|
if iterable(a):
|
|
try:
|
|
return type(a)([sympify(x, locals=locals, convert_xor=convert_xor,
|
|
rational=rational, evaluate=evaluate) for x in a])
|
|
except TypeError:
|
|
# Not all iterables are rebuildable with their type.
|
|
pass
|
|
|
|
if not isinstance(a, str):
|
|
try:
|
|
a = str(a)
|
|
except Exception as exc:
|
|
raise SympifyError(a, exc)
|
|
sympy_deprecation_warning(
|
|
f"""
|
|
The string fallback in sympify() is deprecated.
|
|
|
|
To explicitly convert the string form of an object, use
|
|
sympify(str(obj)). To add define sympify behavior on custom
|
|
objects, use sympy.core.sympify.converter or define obj._sympy_
|
|
(see the sympify() docstring).
|
|
|
|
sympify() performed the string fallback resulting in the following string:
|
|
|
|
{a!r}
|
|
""",
|
|
deprecated_since_version='1.6',
|
|
active_deprecations_target="deprecated-sympify-string-fallback",
|
|
)
|
|
|
|
from sympy.parsing.sympy_parser import (parse_expr, TokenError,
|
|
standard_transformations)
|
|
from sympy.parsing.sympy_parser import convert_xor as t_convert_xor
|
|
from sympy.parsing.sympy_parser import rationalize as t_rationalize
|
|
|
|
transformations = standard_transformations
|
|
|
|
if rational:
|
|
transformations += (t_rationalize,)
|
|
if convert_xor:
|
|
transformations += (t_convert_xor,)
|
|
|
|
try:
|
|
a = a.replace('\n', '')
|
|
expr = parse_expr(a, local_dict=locals, transformations=transformations, evaluate=evaluate)
|
|
except (TokenError, SyntaxError) as exc:
|
|
raise SympifyError('could not parse %r' % a, exc)
|
|
|
|
return expr
|
|
|
|
|
|
def _sympify(a):
|
|
"""
|
|
Short version of :func:`~.sympify` for internal usage for ``__add__`` and
|
|
``__eq__`` methods where it is ok to allow some things (like Python
|
|
integers and floats) in the expression. This excludes things (like strings)
|
|
that are unwise to allow into such an expression.
|
|
|
|
>>> from sympy import Integer
|
|
>>> Integer(1) == 1
|
|
True
|
|
|
|
>>> Integer(1) == '1'
|
|
False
|
|
|
|
>>> from sympy.abc import x
|
|
>>> x + 1
|
|
x + 1
|
|
|
|
>>> x + '1'
|
|
Traceback (most recent call last):
|
|
...
|
|
TypeError: unsupported operand type(s) for +: 'Symbol' and 'str'
|
|
|
|
see: sympify
|
|
|
|
"""
|
|
return sympify(a, strict=True)
|
|
|
|
|
|
def kernS(s):
|
|
"""Use a hack to try keep autosimplification from distributing a
|
|
a number into an Add; this modification does not
|
|
prevent the 2-arg Mul from becoming an Add, however.
|
|
|
|
Examples
|
|
========
|
|
|
|
>>> from sympy.core.sympify import kernS
|
|
>>> from sympy.abc import x, y
|
|
|
|
The 2-arg Mul distributes a number (or minus sign) across the terms
|
|
of an expression, but kernS will prevent that:
|
|
|
|
>>> 2*(x + y), -(x + 1)
|
|
(2*x + 2*y, -x - 1)
|
|
>>> kernS('2*(x + y)')
|
|
2*(x + y)
|
|
>>> kernS('-(x + 1)')
|
|
-(x + 1)
|
|
|
|
If use of the hack fails, the un-hacked string will be passed to sympify...
|
|
and you get what you get.
|
|
|
|
XXX This hack should not be necessary once issue 4596 has been resolved.
|
|
"""
|
|
hit = False
|
|
quoted = '"' in s or "'" in s
|
|
if '(' in s and not quoted:
|
|
if s.count('(') != s.count(")"):
|
|
raise SympifyError('unmatched left parenthesis')
|
|
|
|
# strip all space from s
|
|
s = ''.join(s.split())
|
|
olds = s
|
|
# now use space to represent a symbol that
|
|
# will
|
|
# step 1. turn potential 2-arg Muls into 3-arg versions
|
|
# 1a. *( -> * *(
|
|
s = s.replace('*(', '* *(')
|
|
# 1b. close up exponentials
|
|
s = s.replace('** *', '**')
|
|
# 2. handle the implied multiplication of a negated
|
|
# parenthesized expression in two steps
|
|
# 2a: -(...) --> -( *(...)
|
|
target = '-( *('
|
|
s = s.replace('-(', target)
|
|
# 2b: double the matching closing parenthesis
|
|
# -( *(...) --> -( *(...))
|
|
i = nest = 0
|
|
assert target.endswith('(') # assumption below
|
|
while True:
|
|
j = s.find(target, i)
|
|
if j == -1:
|
|
break
|
|
j += len(target) - 1
|
|
for j in range(j, len(s)):
|
|
if s[j] == "(":
|
|
nest += 1
|
|
elif s[j] == ")":
|
|
nest -= 1
|
|
if nest == 0:
|
|
break
|
|
s = s[:j] + ")" + s[j:]
|
|
i = j + 2 # the first char after 2nd )
|
|
if ' ' in s:
|
|
# get a unique kern
|
|
kern = '_'
|
|
while kern in s:
|
|
kern += choice(string.ascii_letters + string.digits)
|
|
s = s.replace(' ', kern)
|
|
hit = kern in s
|
|
else:
|
|
hit = False
|
|
|
|
for i in range(2):
|
|
try:
|
|
expr = sympify(s)
|
|
break
|
|
except TypeError: # the kern might cause unknown errors...
|
|
if hit:
|
|
s = olds # maybe it didn't like the kern; use un-kerned s
|
|
hit = False
|
|
continue
|
|
expr = sympify(s) # let original error raise
|
|
|
|
if not hit:
|
|
return expr
|
|
|
|
from .symbol import Symbol
|
|
rep = {Symbol(kern): 1}
|
|
def _clear(expr):
|
|
if isinstance(expr, (list, tuple, set)):
|
|
return type(expr)([_clear(e) for e in expr])
|
|
if hasattr(expr, 'subs'):
|
|
return expr.subs(rep, hack2=True)
|
|
return expr
|
|
expr = _clear(expr)
|
|
# hope that kern is not there anymore
|
|
return expr
|
|
|
|
|
|
# Avoid circular import
|
|
from .basic import Basic
|