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.

Reply via email to