I think the issue is whether we are thinking about this as computer code or a computer tool for doing mathematics.
I believe the mathematical concept of an equation that connects two expressions with an equals sign also implies an assignment. When you write `a = b/c` on a piece of paper the equation implies: `a==b/c` assuming no mistake is made. It implies you can calculate a by taking b and dividing it by c. It implies that you can rearrange it to calculate b or c from the other two variable. It also implies: `da/db = 1/c` and so on. I see little value to the equation class if it simply is the equivalent of a list of two expressions that operations are distributed over. I think SymPy pretty much covers that behavior with vectors or just Python lists. I have also made some more elaborate comments in the draft SymPEP for the equation class <https://github.com/sympy/SymPEPs/pull/1#pullrequestreview-575020237>. But in summary they point out that if we apply operations such as diff() the same to both sides of an equation we arrive at expressions that are not consistent in a general way with the application of derivatives to an equation. An equation class that behaves as a list of two expressions which is displayed with an equals sign between the two expressions is certainly easier to implement. However, IMHO an equation with an equals sign *is* more than that. Jonathan On Thursday, February 4, 2021 at 4:51:31 PM UTC-6 asme...@gmail.com wrote: > On Thu, Feb 4, 2021 at 2:18 PM Matthias Geier <matthia...@gmail.com> > wrote: > > > > Hi all. > > > > First of all, thanks for all the effort all of you are putting into > > SymPy in general and into the upcoming Equation class specifically! > > > > I'm not sure whether I fully understand either side of the contentious > > questions mentioned in this thread, but let me nevertheless add my > > opinion: > > > > I think what Jonathan describes is a mixture of two things: > > > > * an "equation" where operations can be applied on both sides > > * a "definition" or "assignment" where an expression (on the right > > side) gets a new name (on the left side) > > > > I'm quite sure that those should be two separate classes, but I'm not > > sure whether *both* should be part of SymPy. > > > > I think the "Equation" class should never treat the two sides > > differently (unless explicitly requested by "rhs" or "lhs" stuff, and > > probably as argument to subs()?). > > I agree with this. > > Incidentally, there is an Assignment class in SymPy, in the codegen > module. It is designed specifically around generating code, so it > might not apply to the use cases here. > > Aaron Meurer > > > > > However, I can very much appreciate the desire to have a symbol that > > represents an expression, where both are displayed nicely with an > > equals sign in between. > > > > FWIW, I've somewhat recently created an ad-hoc class called > > "NamedExpression" that does some of what Jonathan describes. > > > > It's defined there: > > > https://github.com/AudioSceneDescriptionFormat/splines/blob/master/doc/euclidean/utility.py > > > > And there's an example using it: > > https://splines.readthedocs.io/en/latest/euclidean/hermite-uniform.html > > > > Apart from derivatives, there is also .evaluate_at() which treats the > > two sides of the equation differently. > > > > On top of that, I've also made a "NamedMatrix" class with a few > > additional features specific to matrices. > > For example it allows to invert and transpose both sides (which is > > also shown in the example mentioned above). > > > > As a special feature it has also a .pull_out() method, which allows to > > pull a common factor out of all matrix coefficients. > > > > I'm not suggesting to implement any of this in SymPy, probably it is > > much too specific and uses some assumptions that are not appropriate > > for general use. > > > > Probably something like this could become a separate Python module > > (targeted at teaching) using building blocks from SymPy (like the > > upcoming Equation class)? > > > > cheers, > > Matthias > > > > On Thu, Jan 21, 2021 at 4:29 AM Aaron Meurer <asme...@gmail.com> wrote: > > > > > > On Wed, Jan 20, 2021 at 3:49 PM gu...@uwosh.edu <gu...@uwosh.edu> > wrote: > > > > > > > > First, Oscar thank you for taking the time to summarize for everyone > the key issues we are trying to make decisions on. > > > > > > > > Second, I think I can help people understand a little more about why > these are issues. > > > > > > > > Most of them arise because in code we make a distinction between a > function and a symbol. As a physical scientist I routinely work with > equations where all the symbols can be thought of as functions of all the > other symbols. Consider the ideal gas law: P = nRT/V, where P = pressure, n > = moles, R = gas constant, T = absolute temperature and V = volume. You can > solve this equation for any of the symbols. In the form it is written it > makes perfect sense to say dP/dT = nR/V. If I had solved for V => dV/dT = > nR/P. Measurements in the laboratory show that both relationships are true. > Thus the following does not make sense for this simple example: > > > > >>>diff(P=n*R*T/V,T) > > > > 0 = n*R/V > > > > n*R/V is generally not equal to zero (only two cases: n = 0 and V = > oo; both physically not very important). > > > > > > > > Because of the way SymPy presently treats functions, getting the > behavior that I would expect as a scientists for an equation such as the > ideal gas law cannot be achieved by using functions instead of symbols. > > > > > > This starts to get into the question of what the semantic meaning of > > > Eqn is. Should Derivative treat the variables in Eqn as depending on > > > each other, because the equality implies they do? What about Eqn(f(x), > > > a*x)? Does the presence of f(x) prevent a from depending on x? You > > > could also write Eqn(a, f(x)/x). The first one "looks" like a doesn't > > > depend on x and the second one "looks" like it does. But shouldn't the > > > two Eqns be treated in the same way, since one can be obtained from > > > the other by a simple, reversible transformation? > > > > > > My feeling on this is that Derivative already solves this problem by > > > implicitly assuming that variables are independent of one another, > > > unless explicitly stated otherwise via a functional relationship. I > > > don't think Eqn should change this behavior. As an aside, it may be > > > useful, independently of Eqn, to have a function that can take > > > derivatives under the assumption that certain symbols depend on one > > > another, as a convenience over creating explicit Function objects > > > (idiff() is an example of this sort of thing). > > > > > > But the underlying question about the semantic meaning is, to what > > > degree does Eqn represent an "equality", vs. just being a convenient > > > way to manipulate two expressions in tandem. This question also comes > > > up with some of my suggestions about manipulating an Eqn in my > > > previous post. I think an Eqn *is* more than just a pair of > > > expressions. But we should think about a clear way to define an > > > "equality" so that it is clearer what an Eqn should and shouldn't do. > > > > > > > > > > > The fundamental question, as Oscar points out, is whether we want to > be able to wrap an equation in an operation and have that default to > application of the operation to both sides. We are clear that this should > work for simple binary math operations, but for other things it is less > clear. > > > > > > > > From an ease of use and consistency with how expressions behave, I > am inclined towards being able to wrap equations in operations such as > `cos(eqn)`. However, there are clearly some cases where this can cause > problems. So, two important questions we would like the community to > respond to: > > > > > > This really boils down to how we want to answer the question I posed > above. > > > > > > I think that ideally cos(eq) would work. If it works with operations > > > like + and *, it should work with any mathematical function. I also > > > think Eqn should be an Expr so that it has all the same methods. > > > > > > The problem is how to make this work technically. As Oscar said, you > > > can get most mathematical "functions" in SymPy by hooking into > > > Function. But that doesn't do anything for manipulation or > > > simplification functions, like simplify(). Ideally, any generic > > > function that doesn't know about Eqn would just operate on both sides > > > of the equation, and so long as it is a function that normally takes > > > an expression and returns an expression, it should work. But how do we > > > make this work technically? Does every function need to be decorated > > > with some special logic to pass through equations? The ability to > > > "pass through" or dispatch on arbitrary functions is a larger question > > > that has come up in a lot of other places in SymPy. I don't know if > > > we have a solution for it yet, but it is something we'd like to solve. > > > > > > Aaron Meurer > > > > > > > > > > > How important is the convenience of being able to wrap equations in > operations? > > > > What operations are you aware of where this behavior could cause > problems? > > > > > > > > > > > > On Wednesday, January 20, 2021 at 2:45:44 PM UTC-6 Oscar wrote: > > > >> > > > >> 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+un...@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+un...@googlegroups.com. > > > > To view this discussion on the web visit > https://groups.google.com/d/msgid/sympy/d7f67a32-381b-4ca1-baa0-da6c240c4c98n%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+un...@googlegroups.com. > > > To view this discussion on the web visit > https://groups.google.com/d/msgid/sympy/CAKgW%3D6KSj3NTWDgqr7WjeTFf%3DkHrAtvk7sxkW%2Bzm_RbLO72ZWg%40mail.gmail.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+un...@googlegroups.com. > > To view this discussion on the web visit > https://groups.google.com/d/msgid/sympy/CAFesC-c1SzDeVd%3D5iz%3Dm6dRUWs2Y24%2Bpw-6g6%3DK4KoEG7T5cJg%40mail.gmail.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/979503cf-84e1-4d58-b939-bde8b055c36an%40googlegroups.com.