Hello,
I have a question about best practices in subclassing sympy. If there is a good tutorial out there, I would be grateful for a direction, but I could not find anything myself. The explanation of what I'm trying to achieve is somewhat lengthy.
I would like to use sympy to declare differential equations that are later fed to a numerical solver. To simplify things for users, I want to have two kinds of objects:
Dimension:
- behaves like a Symbol with respect to sympy algorithms and printing
- carries additional attributes (in particular, the parameters of the associated grid)
- the equality for these objects is checked based on the name _and_ the attributes
For instance, one would define
x = Dimension('x', 0, 1, 50) # a dimension 'x' with the grid of 50 points on [0, 1]
y = Dimension('y', 0, 1, 50)
x2 = Dimension('x', 0, 1, 20)
xp = Dimension('x', 0, 1, 50)
For all sympy algorithms the relation between `x` and `y` (or `x2`) is the same as the relation between `Symbol('x')` and `Symbol('y')` (or `Symbol('x2')`), and the relation between `x` and `xp` is the same as the relation between `Symbol('x')` and `Symbol('x')` (that is, they're the same object).
Field:
- is initialized with a list of Dimension objects, which denote its "native" dimensions (similarly to how one would write "f = f(x, y, t)" in a paper)
- in sympy expressions behaves just as an undefined function called with its native dimensions
That is, if we have `f = Field('f', x, y)`, we can write, say `(f + x).diff(x)` which will be equivalent to `(f(x, y) + x).diff(x)`
- the equality is checked based on the name _and_ the list of native dimensions
- it can be applied like a Function object; the resulting object still carries around the original object's set of native dimensions
In other words, if I have `f = Field('f', x, y)`, I can use `f(xp, y)` in an expression, but I should be able to obtain the references to `x` and `y` from it during the traversal.
- during the application some arbitrary error-checking may be executed (I just want to be able to intercept such an event, for example, to check that integer dimensions are used for integer dimensions etc)
- optionally, a field not applied to anything, or applied to its "native" dimensions is printed just as its name
I was able to implement a Dimension object (based on the implementation of Symbol) as
import numpy
from sympy import Symbol
from sympy.core.cache import cacheit
class Dimension(Symbol):
def __new_stage2__(cls, name, start, stop, points):
obj = super(Dimension, cls).__xnew__(cls, name, real=True)
obj.params = (start, stop, points)
obj.grid = numpy.linspace(start, stop, points, endpoint=False)
return obj
def __new__(cls, name, *args, **kwds):
obj = Dimension.__xnew_cached_(cls, name, *args, **kwds)
return obj
__xnew__ = staticmethod(__new_stage2__)
__xnew_cached_ = staticmethod(cacheit(__new_stage2__))
def _hashable_content(self):
return (Symbol._hashable_content(self), self.params)
which seems to behave in the required way. I am still unclear how to implement Field properly: should I subclass Function, or AppliedUndef, or something else? Or am I doing something completely incompatible with sympy's design?
Thank you in advance.