Preventing an expression from evaluating

322 views
Skip to first unread message

Ben Lucato

unread,
May 31, 2013, 9:31:32 PM5/31/13
to sy...@googlegroups.com
So in my ever growing quest to work with sympy, I have expressions like:

y = 2*(x*sin(x) - sin(x) + cos(x))/cos(x)**2

with x values like pi/6

How do I substitute x into y without having evaluate automatically happen.

i.e. I would like to be able to do something like:

y.xreplace({x: pi/6}) 
>>> 2*((pi/6)*sin(pi/6) - sin(pi/6) + cos(pi/6))/cos(pi/6)**2


It doesn't need to be with xreplace, that's just what I've used here for explaining my goal. 

Thanks!

Aaron Meurer

unread,
Jun 1, 2013, 12:08:53 AM6/1/13
to sy...@googlegroups.com
There have been many discussions on this sort of thing. There is a
semi-standard way to do things by passing evaluate=False, like

In [6]: sin(pi, evaluate=False)
Out[6]: sin(π)

Not all SymPy classes support this though, and it's not easy to use it
with things like xreplace.

Another way is to bypass the constructor of the class, like

In [7]: Basic.__new__(sin, pi)
Out[7]: sin(π)

You can write an algorithm that walks the expression tree and replaces
sin(x) with Basic.__new__(sin, pi) farily easily.

But probably the best way is to just subclass sin and disable evaluation

In [1]: class noevalsin(sin):
...: @classmethod
...: def eval(cls, arg):
...: return
...:

In [2]: noevalsin(pi)
Out[2]: noevalsin(π)

In [4]: noevalsin(x).diff(x)
Out[4]: cos(x)

(the diff shows that it does indeed act like sin otherwise). You can
change the printing by adding some more methods to the class.

You can then use replace to replace instances of sin with noevalsin

In [8]: sin(x).replace(sin, noevalsin)
Out[8]: noevalsin(x)

In [9]: sin(x).replace(sin, noevalsin).subs(x, pi)
Out[9]: noevalsin(π)

This third option is better because any function that rebuilds the
object will keep it as it is. With the other two, the rebuilding will
do sin(pi) and it will go to 0. With this, the rebuild will do
noevalsin(pi) and it will stay unevaluated.

You could also abstract this logic into a helper function (or class or
metaclass).

One issue with all of these is that many algorithms come to rely on
the invariants satisfied by the classes---in this case, that sin(pi)
is never an object---and so they might fail on such expressions. An
easy way to check this is to disable it in the SymPy source and run
the tests.

Finally, we often don't use best practices so that classes are
subclassable (see
https://code.google.com/p/sympy/issues/detail?id=3652). If you come
across something that converts noevalsin back into regular sin, that's
probably a bug, or it might mean that you need to define more methods
on the class. For example, noevalsin(x).diff(x, x) will be -sin(x),
not -noevalsin(x), because cos(x).diff(x) remains unchanged.

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 http://groups.google.com/group/sympy?hl=en-US.
> For more options, visit https://groups.google.com/groups/opt_out.
>
>

Ben Lucato

unread,
Jun 1, 2013, 1:32:07 AM6/1/13
to sy...@googlegroups.com
Since there have been many discussions on this - can I just say thankyou for writing such a detailed answer.

I initially tried the third recommendation which worked a treat - except for when it came to printing. It would always print out noevalsin instead of sin.

So I tried the first way you mentioned.

Though it is a lengthy expression, it does things exactly how I want it to:

self.derivative.replace(sympy.sin(a), sympy.sin(self.x_value, evaluate=False)).replace(sympy.cos(a), sympy.cos(self.x_value, evaluate=False))

PERFECT. 

I imagine there would be some way to modify noevalsin so that it prints as sin, but alas I am but a simple noob at object-oriented in Python.

Thankyou!!!

Lucas Wilkins

unread,
Jun 1, 2013, 8:25:33 PM6/1/13
to sy...@googlegroups.com
Is there some reason not to do this? (except for the way it prints)

hold = Function("Hold")

def release(expr):
    return expr.replace(hold, Id)

print sin(pi), sin(hold(pi)), release(sin(hold(pi)))

:L

Aaron Meurer

unread,
Jun 1, 2013, 9:52:59 PM6/1/13
to sy...@googlegroups.com
That's basically the same thing as my solution, since Function('f') is
basically just class f(Function):pass, except my way automatically
works with all the methods of sin because it subclasses it directly.

Regarding printing, you can add the various printing methods to the
class (see http://docs.sympy.org/0.7.2/modules/printing.html). It
boils down to writing some methods on the class.

You could also just call it sin, like

class sin(sin):
...

You would need to handle your namespaces properly to do this (i.e.,
use sympy.sin to get the original sin), but it should work just fine
otherwise (although maybe it won't, because unfortunately there are
some things that are incorrectly using the name of the class to
recognize it, so if something doesn't work with that, just let us
know).

Aaron Meurer

Ben Lucato

unread,
Sep 19, 2013, 1:51:05 AM9/19/13
to sy...@googlegroups.com
I know this is looking back a month or two, but I have just started to use your idea about subclassing.

However, it is having unintended side effects.

Including the code:

class exp(sympy.exp):
    @classmethod
    def eval(cls, arg):
        return

in a module like noevals.py, and then subsequently importing noevals has disabled eval in the regular sympy.exp. I am super new to subclassing in general, but I know it's this code because when I delete these 4 lines the problem goes away. How do I retain this subclass without altering the parent's behaviour?

Aaron Meurer

unread,
Sep 19, 2013, 2:02:12 PM9/19/13
to sy...@googlegroups.com
I can only guess at what's happening, but does it work if you don't
name the class "exp"? There is (unfortunately) a global class registry
in SymPy that uses the name of the class, so a subclass called "exp"
might inadvertently override exp for other parts of SymPy.

If this is the issue, you can always call the class something like
UnevaluatedExp, and then put "exp = UnevaluatedExp" at the end of the
definition for convenience (or always import it as "from module import
UnevaluatedExp as exp"). The important thing is the __name__ of the
class.

Of course, we really should get rid of the class registry. But it
hasn't been done yet.

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 http://groups.google.com/group/sympy.

Ben Lucato

unread,
Sep 19, 2013, 7:12:00 PM9/19/13
to sy...@googlegroups.com
Hey Aaron, you were spot on with your guess. Renaming the subclass to noevalexp solves the issue. You rock!!!!!


You received this message because you are subscribed to a topic in the Google Groups "sympy" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/sympy/zilEXwN26so/unsubscribe.
To unsubscribe from this group and all its topics, send an email to sympy+un...@googlegroups.com.

To post to this group, send email to sy...@googlegroups.com.
Visit this group at http://groups.google.com/group/sympy.
For more options, visit https://groups.google.com/groups/opt_out.



--
 
 
Ben Lucato
----------------------------------------------------------------------------------

Aaron Meurer

unread,
Sep 19, 2013, 7:19:51 PM9/19/13
to sy...@googlegroups.com
Great to hear that. I wish we could get rid of the class registry so
this issue wouldn't happen.

Another thing I though of. Depending on what you're doing, you might
also want to override the string printer, so that your class prints as
exp(x) instead of noevalexp(x) (unless you want it to print as
noevalexp(x)).

And as I think I mentioned earlier, there are several places in SymPy
that are not written correctly to be nice to subclasses (see
https://code.google.com/p/sympy/issues/detail?id=3652), so if you come
across something else that doesn't work, there's a good chance that it
too is a SymPy issue, not your issue.

Aaron Meurer

Ben Lucato

unread,
Sep 19, 2013, 11:50:09 PM9/19/13
to sy...@googlegroups.com
Yeah you mentioned that about the subclasses, which is why my first instinct was to come here rather than stackoverflow when I encountered this problem. Also about the string printer, I'm exclusively using the latex printer so this is not a problem :-). Thanks

Ben Lucato

unread,
Oct 1, 2013, 5:45:11 AM10/1/13
to sy...@googlegroups.com
I have been doing some more work on this, and wanted to further include sympy.Add, sympy.Mul and sympy.Pow, however it doesn't work the same.

For instance, doing noevalAdd(1, 2) returns 3, whereas sympy.Add(1, 2, evaluate=False) returns 1 + 2.


Is there a way to do the same thing for these 'core' classes?

Matthew Rocklin

unread,
Oct 1, 2013, 10:23:59 AM10/1/13
to sy...@googlegroups.com
I support a change to the SymPy core classes so that `Add` never evaluates but we add a function `add` which does evaluate.  
The uppercase-does-not-evaluate convention works very well in MatrixExprs.


--
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.

Aaron Meurer

unread,
Oct 1, 2013, 12:26:26 PM10/1/13
to sy...@googlegroups.com
This is probably an instance of the sort of thing I mentioned earlier in this thread where some classes do not play nicely with potential subclasses. What is the exact code you are using for noevalAdd?

Aaron Meurer
--

Ben Lucato

unread,
Oct 2, 2013, 11:35:04 AM10/2/13
to sy...@googlegroups.com
The exact code being used is:

class noevalAdd(sympy.Add):
    @classmethod
    def eval(cls, arg):
        return

--
You received this message because you are subscribed to a topic in the Google Groups "sympy" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/sympy/zilEXwN26so/unsubscribe.
To unsubscribe from this group and all its topics, send an email to sympy+un...@googlegroups.com.

To post to this group, send email to sy...@googlegroups.com.
Visit this group at http://groups.google.com/group/sympy.
For more options, visit https://groups.google.com/groups/opt_out.

Aaron Meurer

unread,
Oct 3, 2013, 11:18:57 PM10/3/13
to sy...@googlegroups.com
Oh, this one should have been obvious. The solution you are using with
eval only works on subclasses of Function. Add and Mul are not
subclasses of Function. To get them to work, you need to override the
flatten classmethod. The API for flatten is a little different. Look
at the current implementation for more details, but basically it needs
to take in a sequence and spit out two sequences and some
order_symbols thing (I'm not entirely sure what the API for this
should be, but if you never use O, just return None). So for instance,
if you only care about commutatives and no order symbols, you can use

class noevalAdd(Add):
@classmethod
def flatten(cls, seq):
return seq, [], None

Alternately, you can just override __new__. You may need to redo some
logic, though (or at the very least make sure to call some superclass
initializers correctly). The advantage of this is that 0 is removed in
__new__, not flatten, so the above will still remove 0. Take a look at
AssocOp.__new__.

Aaron Meurer

Ben Lucato

unread,
Oct 11, 2013, 10:21:08 AM10/11/13
to sy...@googlegroups.com
This is awesome!!! Your simple fix with overriding flatten() works perfectly. My last question is -- how do you do the same thing with Pow? It is subclassed differently to Add, Mul (which both subclass "AssocOp").
I have taken a look at the source code (go me!!!) - it seems that in this case, because Pow is defined like this:

"def __new__(cls, b, e, evaluate=True):
b = _sympify(b) e = _sympify(e) if evaluate: if e is S.Zero: return S.One elif e is S.One: return b else: obj = b._eval_power(e) if obj is not None: return obj obj = Expr.__new__(cls, b, e) obj.is_commutative = (b.is_commutative and e.is_commutative) return obj"


So it seems a change to __new__ is necessary, right? It looks like evaluate=False is not even supported for Pow - am I right? (just a noob asking questions)

Thanks!!

Ben Lucato

unread,
Oct 12, 2013, 5:29:15 AM10/12/13
to sy...@googlegroups.com
Interestingly, with noevalMul:

noevalMul(2, 3)
>>> 2*3

noevalMul(-2, 3)
>>> -6


occurs because of some combination of "as_coeff_Mul" and "_keep_coeff", and the SymPy printer calls the "_keep_coeff" owned by Mul, not noevalMul, so I'm not sure how to override this behaviour in noevalMul.

Aaron Meurer

unread,
Oct 12, 2013, 12:21:56 PM10/12/13
to sy...@googlegroups.com
The issue there is with the printer, not the object. You can see that
the object is find by looking at .args.

I guess you can create your own custom printer for the object. We
could also fix this upstream.

Aaron Meurer

Aaron Meurer

unread,
Oct 12, 2013, 12:27:23 PM10/12/13
to sy...@googlegroups.com
Yes, for general objects, you have to override __new__. In general,
you just need to define a __new__ that looks like the one above,
except without the "if evaluate" branch (if you don't care about
noncommutatives you can skip that bit too). Basically

- sympify the arguments. _sympify is used so that strings aren't supported.
- Create a new Expr of the given class. If your class doesn't subclass
from Expr, use Basic instead. This contains some vital logic such as
setting up the hash, so don't skip it.
- Return that object.

By the way, for your Add and Mul, in your flatten, you may want to
override flatten to actually flatten things, so that NoevalAdd(x, y,
NoevalAdd(z, t)) is the same as NoevalAdd(x, y, z, t). Otherwise,
there's little point in overriding flatten instead of __new__ there.

Aaron Meurer

Ben Lucato

unread,
Oct 13, 2013, 5:35:52 AM10/13/13
to sy...@googlegroups.com
Thanks for your help - I've been doing a bit of work on this.

Another thing: 

x = noevalMul(2, 5)
y = -x

The type of y is a sympy.core.Integer - i.e. the assignment of y to -x evaluates x in the process. I'm not sure what behaviour I need to override for this.

BTW what I did with the printing is subclass LatexPrinter so I keep a lot of the already-done work.


You received this message because you are subscribed to a topic in the Google Groups "sympy" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/sympy/zilEXwN26so/unsubscribe.
To unsubscribe from this group and all its topics, send an email to sympy+un...@googlegroups.com.

To post to this group, send email to sy...@googlegroups.com.
Visit this group at http://groups.google.com/group/sympy.
For more options, visit https://groups.google.com/groups/opt_out.

Aaron Meurer

unread,
Oct 13, 2013, 2:04:18 PM10/13/13
to sy...@googlegroups.com
You'll need to override __neg__.

Aaron Meurer

Chris Smith

unread,
Dec 7, 2013, 1:32:06 PM12/7/13
to sy...@googlegroups.com
I wonder if we had a Hold class that takes arguments (class, args) that prints as class(*args, evaluate=False) might be a way to go and then have a `touch` or `release` function that converts Hold to class(*args) might be a way to go.

>> Hold(Add, 3, 4)
3 + 4
>> touch(_)
7


Reply all
Reply to author
Forward
0 new messages