123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634 |
- """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
|