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.

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:

   1. How important is the convenience of being able to wrap equations in 
   operations?
   2. 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+unsubscr...@googlegroups.com.
To view this discussion on the web visit 
https://groups.google.com/d/msgid/sympy/d7f67a32-381b-4ca1-baa0-da6c240c4c98n%40googlegroups.com.

Reply via email to