389 lines
11 KiB
Python
389 lines
11 KiB
Python
|
"""
|
||
|
Module to efficiently partition SymPy objects.
|
||
|
|
||
|
This system is introduced because class of SymPy object does not always
|
||
|
represent the mathematical classification of the entity. For example,
|
||
|
``Integral(1, x)`` and ``Integral(Matrix([1,2]), x)`` are both instance
|
||
|
of ``Integral`` class. However the former is number and the latter is
|
||
|
matrix.
|
||
|
|
||
|
One way to resolve this is defining subclass for each mathematical type,
|
||
|
such as ``MatAdd`` for the addition between matrices. Basic algebraic
|
||
|
operation such as addition or multiplication take this approach, but
|
||
|
defining every class for every mathematical object is not scalable.
|
||
|
|
||
|
Therefore, we define the "kind" of the object and let the expression
|
||
|
infer the kind of itself from its arguments. Function and class can
|
||
|
filter the arguments by their kind, and behave differently according to
|
||
|
the type of itself.
|
||
|
|
||
|
This module defines basic kinds for core objects. Other kinds such as
|
||
|
``ArrayKind`` or ``MatrixKind`` can be found in corresponding modules.
|
||
|
|
||
|
.. notes::
|
||
|
This approach is experimental, and can be replaced or deleted in the future.
|
||
|
See https://github.com/sympy/sympy/pull/20549.
|
||
|
"""
|
||
|
|
||
|
from collections import defaultdict
|
||
|
|
||
|
from .cache import cacheit
|
||
|
from sympy.multipledispatch.dispatcher import (Dispatcher,
|
||
|
ambiguity_warn, ambiguity_register_error_ignore_dup,
|
||
|
str_signature, RaiseNotImplementedError)
|
||
|
|
||
|
|
||
|
class KindMeta(type):
|
||
|
"""
|
||
|
Metaclass for ``Kind``.
|
||
|
|
||
|
Assigns empty ``dict`` as class attribute ``_inst`` for every class,
|
||
|
in order to endow singleton-like behavior.
|
||
|
"""
|
||
|
def __new__(cls, clsname, bases, dct):
|
||
|
dct['_inst'] = {}
|
||
|
return super().__new__(cls, clsname, bases, dct)
|
||
|
|
||
|
|
||
|
class Kind(object, metaclass=KindMeta):
|
||
|
"""
|
||
|
Base class for kinds.
|
||
|
|
||
|
Kind of the object represents the mathematical classification that
|
||
|
the entity falls into. It is expected that functions and classes
|
||
|
recognize and filter the argument by its kind.
|
||
|
|
||
|
Kind of every object must be carefully selected so that it shows the
|
||
|
intention of design. Expressions may have different kind according
|
||
|
to the kind of its arguments. For example, arguments of ``Add``
|
||
|
must have common kind since addition is group operator, and the
|
||
|
resulting ``Add()`` has the same kind.
|
||
|
|
||
|
For the performance, each kind is as broad as possible and is not
|
||
|
based on set theory. For example, ``NumberKind`` includes not only
|
||
|
complex number but expression containing ``S.Infinity`` or ``S.NaN``
|
||
|
which are not strictly number.
|
||
|
|
||
|
Kind may have arguments as parameter. For example, ``MatrixKind()``
|
||
|
may be constructed with one element which represents the kind of its
|
||
|
elements.
|
||
|
|
||
|
``Kind`` behaves in singleton-like fashion. Same signature will
|
||
|
return the same object.
|
||
|
|
||
|
"""
|
||
|
def __new__(cls, *args):
|
||
|
if args in cls._inst:
|
||
|
inst = cls._inst[args]
|
||
|
else:
|
||
|
inst = super().__new__(cls)
|
||
|
cls._inst[args] = inst
|
||
|
return inst
|
||
|
|
||
|
|
||
|
class _UndefinedKind(Kind):
|
||
|
"""
|
||
|
Default kind for all SymPy object. If the kind is not defined for
|
||
|
the object, or if the object cannot infer the kind from its
|
||
|
arguments, this will be returned.
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy import Expr
|
||
|
>>> Expr().kind
|
||
|
UndefinedKind
|
||
|
"""
|
||
|
def __new__(cls):
|
||
|
return super().__new__(cls)
|
||
|
|
||
|
def __repr__(self):
|
||
|
return "UndefinedKind"
|
||
|
|
||
|
UndefinedKind = _UndefinedKind()
|
||
|
|
||
|
|
||
|
class _NumberKind(Kind):
|
||
|
"""
|
||
|
Kind for all numeric object.
|
||
|
|
||
|
This kind represents every number, including complex numbers,
|
||
|
infinity and ``S.NaN``. Other objects such as quaternions do not
|
||
|
have this kind.
|
||
|
|
||
|
Most ``Expr`` are initially designed to represent the number, so
|
||
|
this will be the most common kind in SymPy core. For example
|
||
|
``Symbol()``, which represents a scalar, has this kind as long as it
|
||
|
is commutative.
|
||
|
|
||
|
Numbers form a field. Any operation between number-kind objects will
|
||
|
result this kind as well.
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy import S, oo, Symbol
|
||
|
>>> S.One.kind
|
||
|
NumberKind
|
||
|
>>> (-oo).kind
|
||
|
NumberKind
|
||
|
>>> S.NaN.kind
|
||
|
NumberKind
|
||
|
|
||
|
Commutative symbol are treated as number.
|
||
|
|
||
|
>>> x = Symbol('x')
|
||
|
>>> x.kind
|
||
|
NumberKind
|
||
|
>>> Symbol('y', commutative=False).kind
|
||
|
UndefinedKind
|
||
|
|
||
|
Operation between numbers results number.
|
||
|
|
||
|
>>> (x+1).kind
|
||
|
NumberKind
|
||
|
|
||
|
See Also
|
||
|
========
|
||
|
|
||
|
sympy.core.expr.Expr.is_Number : check if the object is strictly
|
||
|
subclass of ``Number`` class.
|
||
|
|
||
|
sympy.core.expr.Expr.is_number : check if the object is number
|
||
|
without any free symbol.
|
||
|
|
||
|
"""
|
||
|
def __new__(cls):
|
||
|
return super().__new__(cls)
|
||
|
|
||
|
def __repr__(self):
|
||
|
return "NumberKind"
|
||
|
|
||
|
NumberKind = _NumberKind()
|
||
|
|
||
|
|
||
|
class _BooleanKind(Kind):
|
||
|
"""
|
||
|
Kind for boolean objects.
|
||
|
|
||
|
SymPy's ``S.true``, ``S.false``, and built-in ``True`` and ``False``
|
||
|
have this kind. Boolean number ``1`` and ``0`` are not relevant.
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy import S, Q
|
||
|
>>> S.true.kind
|
||
|
BooleanKind
|
||
|
>>> Q.even(3).kind
|
||
|
BooleanKind
|
||
|
"""
|
||
|
def __new__(cls):
|
||
|
return super().__new__(cls)
|
||
|
|
||
|
def __repr__(self):
|
||
|
return "BooleanKind"
|
||
|
|
||
|
BooleanKind = _BooleanKind()
|
||
|
|
||
|
|
||
|
class KindDispatcher:
|
||
|
"""
|
||
|
Dispatcher to select a kind from multiple kinds by binary dispatching.
|
||
|
|
||
|
.. notes::
|
||
|
This approach is experimental, and can be replaced or deleted in
|
||
|
the future.
|
||
|
|
||
|
Explanation
|
||
|
===========
|
||
|
|
||
|
SymPy object's :obj:`sympy.core.kind.Kind()` vaguely represents the
|
||
|
algebraic structure where the object belongs to. Therefore, with
|
||
|
given operation, we can always find a dominating kind among the
|
||
|
different kinds. This class selects the kind by recursive binary
|
||
|
dispatching. If the result cannot be determined, ``UndefinedKind``
|
||
|
is returned.
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
Multiplication between numbers return number.
|
||
|
|
||
|
>>> from sympy import NumberKind, Mul
|
||
|
>>> Mul._kind_dispatcher(NumberKind, NumberKind)
|
||
|
NumberKind
|
||
|
|
||
|
Multiplication between number and unknown-kind object returns unknown kind.
|
||
|
|
||
|
>>> from sympy import UndefinedKind
|
||
|
>>> Mul._kind_dispatcher(NumberKind, UndefinedKind)
|
||
|
UndefinedKind
|
||
|
|
||
|
Any number and order of kinds is allowed.
|
||
|
|
||
|
>>> Mul._kind_dispatcher(UndefinedKind, NumberKind)
|
||
|
UndefinedKind
|
||
|
>>> Mul._kind_dispatcher(NumberKind, UndefinedKind, NumberKind)
|
||
|
UndefinedKind
|
||
|
|
||
|
Since matrix forms a vector space over scalar field, multiplication
|
||
|
between matrix with numeric element and number returns matrix with
|
||
|
numeric element.
|
||
|
|
||
|
>>> from sympy.matrices import MatrixKind
|
||
|
>>> Mul._kind_dispatcher(MatrixKind(NumberKind), NumberKind)
|
||
|
MatrixKind(NumberKind)
|
||
|
|
||
|
If a matrix with number element and another matrix with unknown-kind
|
||
|
element are multiplied, we know that the result is matrix but the
|
||
|
kind of its elements is unknown.
|
||
|
|
||
|
>>> Mul._kind_dispatcher(MatrixKind(NumberKind), MatrixKind(UndefinedKind))
|
||
|
MatrixKind(UndefinedKind)
|
||
|
|
||
|
Parameters
|
||
|
==========
|
||
|
|
||
|
name : str
|
||
|
|
||
|
commutative : bool, optional
|
||
|
If True, binary dispatch will be automatically registered in
|
||
|
reversed order as well.
|
||
|
|
||
|
doc : str, optional
|
||
|
|
||
|
"""
|
||
|
def __init__(self, name, commutative=False, doc=None):
|
||
|
self.name = name
|
||
|
self.doc = doc
|
||
|
self.commutative = commutative
|
||
|
self._dispatcher = Dispatcher(name)
|
||
|
|
||
|
def __repr__(self):
|
||
|
return "<dispatched %s>" % self.name
|
||
|
|
||
|
def register(self, *types, **kwargs):
|
||
|
"""
|
||
|
Register the binary dispatcher for two kind classes.
|
||
|
|
||
|
If *self.commutative* is ``True``, signature in reversed order is
|
||
|
automatically registered as well.
|
||
|
"""
|
||
|
on_ambiguity = kwargs.pop("on_ambiguity", None)
|
||
|
if not on_ambiguity:
|
||
|
if self.commutative:
|
||
|
on_ambiguity = ambiguity_register_error_ignore_dup
|
||
|
else:
|
||
|
on_ambiguity = ambiguity_warn
|
||
|
kwargs.update(on_ambiguity=on_ambiguity)
|
||
|
|
||
|
if not len(types) == 2:
|
||
|
raise RuntimeError(
|
||
|
"Only binary dispatch is supported, but got %s types: <%s>." % (
|
||
|
len(types), str_signature(types)
|
||
|
))
|
||
|
|
||
|
def _(func):
|
||
|
self._dispatcher.add(types, func, **kwargs)
|
||
|
if self.commutative:
|
||
|
self._dispatcher.add(tuple(reversed(types)), func, **kwargs)
|
||
|
return _
|
||
|
|
||
|
def __call__(self, *args, **kwargs):
|
||
|
if self.commutative:
|
||
|
kinds = frozenset(args)
|
||
|
else:
|
||
|
kinds = []
|
||
|
prev = None
|
||
|
for a in args:
|
||
|
if prev is not a:
|
||
|
kinds.append(a)
|
||
|
prev = a
|
||
|
return self.dispatch_kinds(kinds, **kwargs)
|
||
|
|
||
|
@cacheit
|
||
|
def dispatch_kinds(self, kinds, **kwargs):
|
||
|
# Quick exit for the case where all kinds are same
|
||
|
if len(kinds) == 1:
|
||
|
result, = kinds
|
||
|
if not isinstance(result, Kind):
|
||
|
raise RuntimeError("%s is not a kind." % result)
|
||
|
return result
|
||
|
|
||
|
for i,kind in enumerate(kinds):
|
||
|
if not isinstance(kind, Kind):
|
||
|
raise RuntimeError("%s is not a kind." % kind)
|
||
|
|
||
|
if i == 0:
|
||
|
result = kind
|
||
|
else:
|
||
|
prev_kind = result
|
||
|
|
||
|
t1, t2 = type(prev_kind), type(kind)
|
||
|
k1, k2 = prev_kind, kind
|
||
|
func = self._dispatcher.dispatch(t1, t2)
|
||
|
if func is None and self.commutative:
|
||
|
# try reversed order
|
||
|
func = self._dispatcher.dispatch(t2, t1)
|
||
|
k1, k2 = k2, k1
|
||
|
if func is None:
|
||
|
# unregistered kind relation
|
||
|
result = UndefinedKind
|
||
|
else:
|
||
|
result = func(k1, k2)
|
||
|
if not isinstance(result, Kind):
|
||
|
raise RuntimeError(
|
||
|
"Dispatcher for {!r} and {!r} must return a Kind, but got {!r}".format(
|
||
|
prev_kind, kind, result
|
||
|
))
|
||
|
|
||
|
return result
|
||
|
|
||
|
@property
|
||
|
def __doc__(self):
|
||
|
docs = [
|
||
|
"Kind dispatcher : %s" % self.name,
|
||
|
"Note that support for this is experimental. See the docs for :class:`KindDispatcher` for details"
|
||
|
]
|
||
|
|
||
|
if self.doc:
|
||
|
docs.append(self.doc)
|
||
|
|
||
|
s = "Registered kind classes\n"
|
||
|
s += '=' * len(s)
|
||
|
docs.append(s)
|
||
|
|
||
|
amb_sigs = []
|
||
|
|
||
|
typ_sigs = defaultdict(list)
|
||
|
for sigs in self._dispatcher.ordering[::-1]:
|
||
|
key = self._dispatcher.funcs[sigs]
|
||
|
typ_sigs[key].append(sigs)
|
||
|
|
||
|
for func, sigs in typ_sigs.items():
|
||
|
|
||
|
sigs_str = ', '.join('<%s>' % str_signature(sig) for sig in sigs)
|
||
|
|
||
|
if isinstance(func, RaiseNotImplementedError):
|
||
|
amb_sigs.append(sigs_str)
|
||
|
continue
|
||
|
|
||
|
s = 'Inputs: %s\n' % sigs_str
|
||
|
s += '-' * len(s) + '\n'
|
||
|
if func.__doc__:
|
||
|
s += func.__doc__.strip()
|
||
|
else:
|
||
|
s += func.__name__
|
||
|
docs.append(s)
|
||
|
|
||
|
if amb_sigs:
|
||
|
s = "Ambiguous kind classes\n"
|
||
|
s += '=' * len(s)
|
||
|
docs.append(s)
|
||
|
|
||
|
s = '\n'.join(amb_sigs)
|
||
|
docs.append(s)
|
||
|
|
||
|
return '\n\n'.join(docs)
|