Subclassing sympy symbols and functions to attach additional attributes

512 views
Skip to first unread message

Bogdan Opanchuk

unread,
Feb 4, 2016, 8:26:09 PM2/4/16
to sympy
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.

Aaron Meurer

unread,
Feb 5, 2016, 4:46:14 PM2/5/16
to sy...@googlegroups.com
If you want to give the fields names I guess you should use UndefinedFunction. Note that UndefinedFunction('f') dynamically creates a subclass of AppliedUndef called "f". 

I think what you are doing is right, although let us know if you run into issues (SymPy has many bugs where things don't work correctly if you subclass things).  It's clear to me that we should make this sort of thing easier, though.

Aaron Meurer

--
You received this message because you are subscribed to the Google Groups "sympy" group.
To unsubscribe from this group and stop receiving emails from it, send an email to sympy+un...@googlegroups.com.
To post to this group, send email to sy...@googlegroups.com.
Visit this group at https://groups.google.com/group/sympy.
To view this discussion on the web visit https://groups.google.com/d/msgid/sympy/4daad849-0456-4a85-9faf-fe930d75f161%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Bogdan Opanchuk

unread,
Feb 5, 2016, 5:37:27 PM2/5/16
to sympy
Hi Aaron,

Thank you for your reply.


If you want to give the fields names I guess you should use UndefinedFunction. Note that UndefinedFunction('f') dynamically creates a subclass of AppliedUndef called "f". 

The problem is that I need this object to participate in expressions even without being applied (that is, both `f` and `f(x)` should be usable in expressions), and UndefinedFunction cannot do that. I was thinking about subclassing a Symbol and overriding __call__ to return an applied function (that's where the error checking will happen, too). Another variant would be to give the user a function applied to native dimensions right away, and override __call__ in the UndefinedFunction derivative (that would make printing more verbose than necessary, but I can live with that). Which way do you think fits better in sympy design?
 
I think what you are doing is right, although let us know if you run into issues (SymPy has many bugs where things don't work correctly if you subclass things).  It's clear to me that we should make this sort of thing easier, though.

The main problem is that I do not know which conventions should my subclassed objects conform to (besides the primary invariant). For instance, do I understand it correctly that sympy caches objects purely for performance reasons, and for two objects to be equal it is enough to have equal _hashable_content()? If I want a custom differentiation behavior (e.g. `f.diff(x)` equivalent to `f(x).diff(x)`), do I need to override diff(), or some underscored method?

Aaron S. Meurer

unread,
Feb 5, 2016, 5:44:30 PM2/5/16
to sy...@googlegroups.com
In that case, it would be better to model your object after Predicate and AppliedPredicate, which work the same way (except in Boolean expressions).

Aaron Meurer

--
You received this message because you are subscribed to the Google Groups "sympy" group.
To unsubscribe from this group and stop receiving emails from it, send an email to sympy+un...@googlegroups.com.
To post to this group, send email to sy...@googlegroups.com.
Visit this group at https://groups.google.com/group/sympy.
Reply all
Reply to author
Forward
0 new messages