Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Equations SymPEP #1

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
301 changes: 301 additions & 0 deletions Equations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
SymPEP XXXX Equation class
=========================================

**Author** Jonathan Gutow, <[email protected]>

**Status** Draft

**Type** Standards Track

**Created** 2021-01-25

**Resolution** url to discussion (required for Accepted | Rejected | Withdrawn)


## Abstract

Fundamentally this is an object with two SymPy expressions connected by an equals sign.
Potentially this could be extended to a general relational expression, but that is not
part of this proposal. This class facilitates manipulating mathematical expressions
in a manner as similar to what is done on paper as possible. For operations/methods where
it makes sense they would be applied to both sides of the equation. As much as possible,
operations on the equations should be initiated using syntax mimicking standard
mathematical notation. This class also aims to provide output in standard mathematical
notation when used within tools such as Jupyter, that support typeset LaTex.

## Motivation and Scope
The original impetus for development of the `Equation` class came from discussions J. Gutow
had with Physical Science Educators using SymPy in their classes. They wanted this on-paper-like
behavior to facilitate demonstrating algebraic manipulations in physical science classes.

There are a number of specific motivations for the development of the `Equation` class:
1. Make interactive manipulation of equations as similar to doing the work on paper as
possible and make the results easier to read by having results that look like
`x = 2*y + 1` rather than `2*y + 1`, with what it is equal to on a separate line.
1. Have equations on which manual algebra can be done by having SymPy perform the actual
symbolic manipulation. This allows people to perform messy manipulations with fewer errors, much
as calculators, spreadsheets and other numerical computation tools facilitate arithmetic.
1. From an educational and new user perspective this increases the ease of use of SymPy
by decreasing the cognitive load of using SymPy. This would be acheived by maintaining as
close to standard mathematical notation for operations as possible (see
[Detailed Description](#detailed-description)). Making SymPy easier to begin using will
increase its adoption and encourage people to learn to use symbolic math tools.
1. The `Equality` class does not reliably support general SymPy operations on both sides
of the equality without collapsing to a Boolean value of `True` or `False`. Parts of SymPy
depend on this Boolean behavior; thus this proposal for a separate `Equation` class rather
than an extension of `Equality`.
1. Currently cycling between expressions and the results of the `solve` operation is quite
messy. This proposal includes extending the `solve` operation so that if passed an equation
or set of equations it would return the solutions as a set of equations.
1. `.subs()` could also be made more natural to use if it could take a list of equations.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be confused with xreplace to be changed to unpack equation objects.
I think that for xreplace, the only xreplace to be allowed with equation is substituting the whole equation object with any other specified target, if equation matches some of the subtree.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point I was only envisioning that .subs(Eqn(a,b)) would be equivalent to .subs({a:b}). A wholesale update to .subs() should probably be a separate SymPEP or at least issue. Getting even more generic syntax such as .subs(a=b) being equivalent to .subs({a:b}) will require more involved workarounds (preparsing?) as Sagemath uses. That is definitely beyond the scope of this proposal.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is possible to make Eqn recognized as some rule table (like how sympy's own Tuple or Dict are recognized as rule. But at least I'm sure that they shoul)
For making subs(a=b) work, I think that we can lookup for unknown keyword argument passed to **kwargs by kwargs.items, but the possible problem is conflicting with the options for subs, If the symbol is named like simulatneous.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For making subs(a=b) work, I think that we can lookup for unknown keyword argument passed to **kwargs by kwargs.items, but the possible problem is conflicting with the options for subs, If the symbol is named like simulatneous.

This is why I think .subs(a=b) would need more sophisticated preparsing to make it work.

1. As an equation with an `=` connecting the two sides can be thought of as a specific
example of a general relation (`=`, `>`, `<`, etc...) as much as possible the `Equation`
class should draw upon the underlying logic in the `relational` class.

## Usage and Impact

The class would have the name `Equation` and `Eqn` would be a synonym. The expectation is
that this would be used primarily interactively for symbolic algebra. Some examples
(__Note__ that for illustration purposes results of operations are written with the two
sides connected by `=`, however this will only be the case in environments with Latex
output):
```
>>> a, b, c, x = var('a b c x')
>>> Equation(a,b/c)
a = b/c
>>> t=Eqn(a,b/c)
>>> t
a = b/c
>>> t*c
a*c = b
>>> c*t
a*c = b
>>> exp(t)
exp(a) = exp(b/c)
>>> exp(log(t))
a = b/c

Simplification and Expansion
>>> f = Eqn(x**2 - 1, c)
>>> f
x**2 - 1 = c
>>> f/(x+1)
(x**2 - 1)/(x + 1) = c/(x + 1)
>>> (f/(x+1)).simplify()
x - 1 = c/(x + 1)
>>> simplify(f/(x+1))
x - 1 = c/(x + 1)
>>> (f/(x+1)).expand()
x**2/(x + 1) - 1/(x + 1) = c/(x + 1)
>>> expand(f/(x+1))
x**2/(x + 1) - 1/(x + 1) = c/(x + 1)
>>> factor(f)
(x - 1)*(x + 1) = c
>>> f.factor()
(x - 1)*(x + 1) = c
>>> f2 = f+a*x**2+b*x +c
>>> f2
a*x**2 + b*x + c + x**2 - 1 = a*x**2 + b*x + 2*c
>>> collect(f2,x)
b*x + c + x**2*(a + 1) - 1 = a*x**2 + b*x + 2*c

Apply operation to only one side
>>> poly = Eqn(a*x**2 + b*x + c*x**2, a*x**3 + b*x**3 + c*x)
>>> poly.applyrhs(factor,x)
a*x**2 + b*x + c*x**2 = x*(c + x**2*(a + b))
>>> poly.applylhs(factor)
x*(a*x + b + c*x) = a*x**3 + b*x**3 + c*x
>>> poly.applylhs(collect,x)
b*x + x**2*(a + c) = a*x**3 + b*x**3 + c*x

``.apply...`` also works with user defined python functions
>>> def addsquare(eqn):
... return eqn+eqn**2
...
>>> t.apply(addsquare)
a**2 + a = b**2/c**2 + b/c
>>> t.applyrhs(addsquare)
a = b**2/c**2 + b/c
>>> t.apply(addsquare, side = 'rhs')
a = b**2/c**2 + b/c
>>> t.applylhs(addsquare)
a**2 + a = b/c
>>> addsquare(t)
a**2 + a = b**2/c**2 + b/c

Inaddition to ``.apply...`` there is also the less general ``.do``,
``.dolhs``, ``.dorhs``, which only works for operations defined on the
``Expr`` class (e.g.``.collect(), .factor(), .expand()``, etc...).
>>> poly.dolhs.collect(x)
b*x + x**2*(a + c) = a*x**3 + b*x**3 + c*x
>>> poly.dorhs.collect(x)
a*x**2 + b*x + c*x**2 = c*x + x**3*(a + b)
>>> poly.do.collect(x)
b*x + x**2*(a + c) = c*x + x**3*(a + b)
>>> poly.dorhs.factor()
a*x**2 + b*x + c*x**2 = x*(a*x**2 + b*x**2 + c)

``poly.do.exp()`` or other sympy math functions will raise an error.

Rearranging an equation (simple example made complicated as illustration)
>>> p, V, n, R, T = var('p V n R T')
>>> eq1=Eqn(p*V,n*R*T)
>>> eq1
V*p = R*T*n
>>> eq2 =eq1/V
>>> eq2
p = R*T*n/V
>>> eq3 = eq2/R/T
>>> eq3
p/(R*T) = n/V
>>> eq4 = eq3*R/p
>>> eq4
1/T = R*n/(V*p)
>>> 1/eq4
T = V*p/(R*n)
>>> eq5 = 1/eq4 - T
>>> eq5
0 = -T + V*p/(R*n)

Substitution (#'s and units)
>>> L, atm, mol, K = var('L atm mol K', positive=True, real=True) # units
>>> eq2.subs({R:0.08206*L*atm/mol/K,T:273*K,n:1.00*mol,V:24.0*L})
p = 0.9334325*atm
>>> eq2.subs({R:0.08206*L*atm/mol/K,T:273*K,n:1.00*mol,V:24.0*L}).evalf(4)
p = 0.9334*atm

Combining equations (Math with equations: lhs with lhs and rhs with rhs)
>>> q = Eqn(a*c, b/c**2)
>>> q
a*c = b/c**2
>>> t
a = b/c
>>> q+t
a*c + a = b/c + b/c**2
>>> q/t
c = 1/c
>>> t**q
a**(a*c) = (b/c)**(b/c**2)

``solve()``
>>> t=Eqn(a,b/c)
>>> t
a = b/c
>>> solve(t,c)
c = b/a
```

## Backwards compatibility

No backwards compatiblity breaks are necessary. However, it has been suggested that the
`Equality` class might eventually be replaced and then `Eq` and `Equality` could become
synomyms for `Equation`.

## Detailed Description

_Binary Operations (+,-, *, /, %, **)_
* If combining an equation with a SymPy expression the expression will be applied to both
sides of the equation as specified by the binary operator.
```
>>> a, b, c = Symbol('a b c')
>>> eq1 = Equation(a, b/c)
>>> eq1
a = b/c
>>> eq1*c
a*c = b
```
* If combining two equations the lhs will combine with the lhs and the rhs with the rhs.
```
>>> eq2 = Equation(b, c**2)
>>> eq2
b = c**2
>>> eq1 + eq2
a + b = b/c + c**2
```
_Functions (sin, cos, exp, etc...)_
* Functions will apply to both sides. Functions that take more than one parameter will
only accept an equation as one of the parameters. It will be up to the user to make
sure the equation is used appropriately.
```
>>> cos(eq1)
cos(a) = cos(b/c)
```
* Keywords and additional parameters will be passed through to the function.

_Simplification_
* Behavior should mimic the behavior for expressions and default to applying to both sides.

_Differentiation and Integration_
* No special support although they could be done on the separate sides of the equation
or utilizing the [_Applications of methods_](#Applications-of-methods)

_Solve_
* Solve should accept equations and lists of equations and return equations or lists of
equations as solutions. To avoid breaking past behavior this should only occur when solve
is used on equations or lists of equations.

#### _Applications of methods_
* Support `.method()` calls for any method that applies to expressions. Apply the method
to both sides of the equation.
* Support `.apply()`, `.applylhs()`, `.applyrhs()`, `.do.`, `.dolhs.`,`.dorhs.`.
* Methods that can be applied to a SymPy expression as a Python method should also work
on an Equation:
```
>>> collect(a*x + b*x**2 + c*x, x)
(a + c)*x + b*x**2
>>> collect(Equation(a*x + b*x**2 + c*x,a*x**2 + b*x + c*x, x)
(a + c)*x + b*x**2 = a*x**2 + (b + c)*x
```
_Miscellaneous_
* `.as_Boolean()` returns the equation cast as an `Equality`.
* `.lhs` returns the expression on the left hand side.
* `.rhs` returns the expression on the right hand side.
* `.swap` returns the equation with the lhs and rhs swapped.
* `.check()` shortcut for `.as_Boolean().simplify()`.

## Related Work

Similar functionality is found in [SageMath](https://www.sagemath.org/), [Maple](https://maplesoft.com),
and other symbolic math packages. The SageMath implementation has essentially no limitations
on the lhs and rhs making it possible to set objects that are incompatible equal to each other.
The proposed implementation here currently requires lhs and rhs to be valid sympy expressions.
A python module that implements much of the proposed capabilities is available via pip:
`pip install -U Algebra_with_SymPy`. This module requires SymPy.

## Implementation
1. All the proposed behaviors except for those associated with `solve` have been implemented in
[PR 21333](https://github.com/sympy/sympy/pull/21333). A smaller subset that begins building
`Equation` as a specific case of `relational` has been implemented in [PR 21325](https://github.com/sympy/sympy/pull/21325)
2. The `solve` features will be implemented after step 1 is completed.
3. Acceptance of lists of equations by `.subs()` will be implemented in parallel with or after the
solve features.
4. A later potential extension might be to allow the user to set parameters to control whether human readable,code or
both forms of the expression are produced as output.

## Alternatives

An alternative would be to implement this as part of the work being done on polyadic predicate
[PR 20656](https://github.com/sympy/sympy/pull/20656). This option was discussed in both PR 19749
and PR 20656. The concensus was that until the predicate work reached the stage of being able to
replace the assumptions module it would be better to keep the two pieces separate. At that point
if combination still appeared straightforward the `Equation` class could become a subclass of the general
class of relations that the predicate work is moving towards.

## Discussions

In decending order of volume discussions within the SymPy community have occured on:
* [PR 19749](https://github.com/sympy/sympy/pull/19479)
* [PR 20656](https://github.com/sympy/sympy/pull/20656)
* [SymPy Google Group](https://groups.google.com/g/sympy/c/rSi_I42i35I)
* Non-public discussions by J. Gutow with Physical Science Educators using SymPy in their classes.
These discussions were the original impetus for development of this class to facilitate demonstrating
algebraic manipulations in physical science classes.
* [PR 21325](https://github.com/sympy/sympy/pull/21325)
* [PR 21333](https://github.com/sympy/sympy/pull/21333)
* [Issue 4986](https://github.com/sympy/sympy/issues/4986)

## References

## Copyright

This document has been placed in the public domain.