I'll add my thoughts in a lengthy post here. Firstly some background on why the existing Eq (full name Equality) class is problematic. The Eq class is a Boolean and its intention is to represent the truth of an expression. What this means is that it will often evaluate to True or False e.g.:
>>> x = Symbol('x') >>> Eq(x, 1) Eq(x, 1) >>> Eq(1, 2) False >>> Eq(1, 1) True This is useful in contexts where we want to use the Boolean-ness because it allows simplifications to happen. Here simply substituting a value for x into Eq(x, 1) turns it into a True or False and then the Piecewise can simplify automatically. >>> p = Piecewise((1, Eq(x, 1)), (2, True)) >>> p Piecewise((1, Eq(x, 1)), (2, True)) >>> p.subs(x, 1) 1 >>> p.subs(x, 2) 2 However this can be quite awkward in other contexts when you want to manipulate equations. Any code that operates with instances of Eq needs to be prepared for the possibility that any operation (subs, simplify, evalf etc) might happen to turn the Eq into True or False which then does not have the same methods or attributes as Eq. For example: >>> eq = Eq(x, 2) >>> eq Eq(x, 2) >>> eq.lhs x >>> eq.subs(x, 1) False >>> eq.subs(x, 1).lhs Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'BooleanFalse' object has no attribute 'lhs' That means that code that operates on Eq needs a lot of checking to be robust (and many bugs in sympy come from forgetting this). It's also necessary to check for these every time you have a function like solve or linsolve etc that can take a list of equations as inputs. The possible presence of True/False needs to be checked in all inputs every time before we can begin to e.g. ask for the rhs of the equation. For interactive use it would be nice to be able to do things like 2*eq to multiply both sides of an equation. We could make that work with Eq but we would have the danger that at any time any operation might turn into True/False and then subsequent arithmetic operations would fail because 2*True is meaningless. It's not just arithmetic but any operation that you might want to apply to one or both sides of an equation. For example to factor both sides of an equation you have to do eq = Eq(factor(eq.lhs), factor(eq.rhs)) which doesn't look so bad but then for example integrating both sides wrt x from 0 to 1 looks like: eq = Eq(Integral(eq.lhs, (x, 0, 1)), Integral(eq.rhs, (x, 0, 1)) which begins to show the awkwardness of needing to repeat the same operation. There are ugly constructs to work around the need to avoid repetition such as eq = Eq(*(Integral(side, (x, 0, 1)) for side in [eq.lhs, eq.rhs])) but that's fairly awkward and cryptic in itself. The proposal here adds a new Eqn (full name Equation) class that does not evaluate to True/False. It also defined various operations like 2*eq to make interactive manipulation of equations easier. Here's a demonstration using that to derive a formula for cos in terms of exp: >>> theta = Symbol('theta', real=True) >>> eq = Eqn(exp(I*theta), cos(theta) + I*sin(theta)) >>> eq exp(I*theta) = I*sin(theta) + cos(theta) >>> conjugate(eq) exp(-I*theta) = -I*sin(theta) + cos(theta) >>> (eq + conjugate(eq))/2 exp(I*theta)/2 + exp(-I*theta)/2 = cos(theta) >>> _.reversed cos(theta) = exp(I*theta)/2 + exp(-I*theta)/2 This kind of derivation won't work in general with Eq because at any step it could evaluate to True. All of the equations above are true - that's the whole point in a derivation! So I really like this feature and I want to get some form of it into the next release. There is a longstanding issue to add this to sympy: https://github.com/sympy/sympy/pull/19479 Some time ago I had a quick go at adding it myself but it turned out other things needed to be fixed first. In the end I fixed those other things but didn't get round to adding the new Equation class itself. It is worth trying to get the details right though. In this proposal there are several mechanisms for applying an operation to either the lhs, rhs, or both sides of an equation: 1. Methods apply/applylhs/appylrhs to apply a function to either/both sides 2. Methods do/dolhs/dorhs to call a method on either/both sides 3. Some functions work on the equation like together(eq) 4. Some methods are defined on the equation like eq.together() Here's a demo of these: >>> x = Symbol('x') >>> eq = Eqn(x*(x + 1), x**2 + x) >>> eq x*(x + 1) = x**2 + x >>> expand(eq) x**2 + x = x**2 + x >>> eq.applylhs(expand) x**2 + x = x**2 + x >>> factor(eq) x*(x + 1) = x*(x + 1) >>> eq.applyrhs(factor) x*(x + 1) = x*(x + 1) >>> eq.dorhs.factor() x*(x + 1) = x*(x + 1) >>> eq.factor() x*(x + 1) = x*(x + 1) The first question that comes to mind is do we need all of these? If eq.dorhs was callable then it could replace eq.applyrhs and we could use eq.dorhs(factor) instead of eq.applyrhs(factor). For many operations that only apply to one side only it is not that bad to do eq = Eqn(eq.lhs, factor(eq.rhs)). We could even have a setrhs method so it becomes eq = eq.setrhs(factor(eq.rhs)) although doesn't seem like a major advantage. For common operations like factor it maybe makes sense to add those as methods on Eqn but otherwise are we going to want to add basically all of the methods that Expr has? Functions can be made to work with Eqn like factor(eq) but where do we draw the line with this? As soon as we have Eqn and there are some functions that can work with it then there will be an expectation to be able to pass Eqn to almost any possible function and we will have to add support for it everywhere. The PR adds a special case in Function.__new__ to make the following work: >>> eq x*(x + 1) = x**2 + x >>> cos(eq) cos(x*(x + 1)) = cos(x**2 + x) Adding that special case in Function.__new__ makes it work for most common mathematical functions but then the question is how to handle functions that take more than one argument: >>> atan2(eq, 1) atan2(x*(x + 1), 1) = atan2(x**2 + x, 1) >>> atan2(1, eq) atan2(1, x*(x + 1) = x**2 + x) >>> atan2(eq, eq) atan2(x*(x + 1), x*(x + 1) = x**2 + x) = atan2(x**2 + x, x*(x + 1) = x**2 + x) The last example has generated an equation with nonsensical objects on each side. Of course that aspect of the PR can be improved but I show that example to illustrate the more general point that if we have an expectation that we can pass an Eqn in place of an expression to any function then we need a way to draw the line between what should work and what should not. Also adding this to Function.__new__ covers a lot of Expr subclasses but there are still plenty more that have their own __new__ methods and the expectation will arise that all of them should be able to handle Eqn. This is my biggest concern: making things like cos(eq) work in a way that seems coherent for users requires adding little bits of support code widely across the codebase. However we do that there will always be gaps and in general I think it will give the impression that sympy is buggy (other things like this already have that effect). I would much rather stick to an API that can work in general without risk of bugs and I would prefer users to learn something that will always work. That means that rather than having cos(eq) I think it would be better to use eq.apply(cos) or perhaps there could be an apply function like apply(eq, cos). More complicated cases can be handled with a lambda function like apply(eq, lambda x: (cos(x) + 1)/2) With a mechanism like apply it's clear that the function we apply to the sides of the equation needs to be a callable that can only take one argument so there is no confusion with something like atan2. This approach also generalises completely to any function that you could apply to the sides including both symbolic functions like cos and manipulation routines like trigsimp. That way users only have to learn one thing that can always work and is always well defined. It also means that there is no need to add haphazard support for Eqn throughout the codebase. There are a couple of other quirks in the PR such as: >>> Derivative(eq) Derivative(x*(x + 1) = x**2 + x, x) >>> Derivative(eq).doit() 2*x + 1 = 2*x + 1 The unevaluated Derivative in the first output there is nonsensical. We also have e.g. >>> P, V, T = symbols('P, V, T') >>> eq = Eqn(P*V, T) >>> eq P*V = T >>> diff(eq, T) Derivative(P*V, T) = 1 >>> diff(eq, T).doit() 0 = 1 Here the way the derivative is evaluated treats the lhs and rhs differently giving an unevaluated derivative on the left. I think the idea is to prevent the left hand side from fully evaluating although it will if you call doit. I expect that a lot of users will find this surprising (I certainly wouldn't want/expect this effect from differentiating the equation). Another quirk is the way that integration is handled: >>> integrate(eq, T) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/Users/enojb/current/sympy/sympy/sympy/integrals/integrals.py", line 1567, in integrate integral = Integral(*args, **kwargs) File "/Users/enojb/current/sympy/sympy/sympy/integrals/integrals.py", line 81, in __new__ return function._eval_Integral(*symbols, **assumptions) File "/Users/enojb/current/sympy/sympy/sympy/core/equation.py", line 491, in _eval_Integral raise ValueError('You must specify `side="lhs"` or `side="rhs"` ' ValueError: You must specify `side="lhs"` or `side="rhs"` when integrating an Equation You have pass a "side" argument to integrate to specify which side to integrate: >>> integrate(eq, T, side='lhs') P*T*V I would rather just pass the lhs to integrate if that's all this is doing: >>> integrate(eq.lhs, T) P*T*V I think that for integration and differentiation we should stick to the general approach implied by apply or do e.g. we use apply to integrate both sides and applylhs to integrate one side. >>> eq.apply(lambda s: Integral(s, (x, 0, 1))) Integral(P*V, (x, 0, 1)) = Integral(T, (x, 0, 1)) If we want a convenience method for integrating both sides like eq.integrate(x, (x, 0, 1)) then that could make sense. I could potentially countenance diff and integrate as functions that make a specific exception to support Eqn so that diff(eq, x) works. I do not think that Derivative(eq) should be allowed though as the unevaluated derivative of an equation is nonsensical as a symbolic expression. I think this is what we want and we should try to get it in for the next release. I have problems with specific parts of it though. As discussed on GitHub Jonathan and I are in disagreement over things like whether cos(eq) should work. We would like to hear what others think about these kinds of details about how working with Eqn should work or what makes more sense as an interface. That's why we have brought the discussion here to the mailing list so if anyone has any thoughts about any of these things then please say so. -- Oscar On Wed, 20 Jan 2021 at 00:58, gu...@uwosh.edu <gu...@uwosh.edu> wrote: > > Aaron, > Thank you for the comments. I need go through them more carefully, but I did > want to direct you to the SymPy pull request for the `Eqn` class that > implements everything in the Binder except for the ability to use `solve` on > an `Eqn`. My preference would be to get the `Eqn` class into SymPy and then > make it work with solve as a separate project/pull. > > As you will see there has been quite a bit of discussion within the pull > request. Hence this request for more input from the community. > > Jonathan > > On Tuesday, January 19, 2021 at 5:34:47 PM UTC-6 asme...@gmail.com wrote: >> >> Is there a pull request with the code associated with this, or is it >> only in the Binder for now? >> > -- > 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+unsubscr...@googlegroups.com. > To view this discussion on the web visit > https://groups.google.com/d/msgid/sympy/3627a868-7cf0-42a8-92b2-b29b723ddca5n%40googlegroups.com. -- 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+unsubscr...@googlegroups.com. To view this discussion on the web visit https://groups.google.com/d/msgid/sympy/CAHVvXxT0yAWhQn47%3DZHxqQ%3Dvt6HLY7sEZ--PpZxOSemJfT9-7g%40mail.gmail.com.