Overview of the Equation class

[1]:
from sympy import *
from sympy_equation import Equation, Eqn, solve
init_printing()

In this overview we are going to explore the capabilities of the Equation class. Note that:

  1. Eqn is an alias for Equation.

  2. the second line of code imports solve from sympy_equation: this function is aware of objects of type Equation. It is just a wrapper to SymPy’s solve.

Defining equations

[2]:
var("a:d")
# first mode: instantiate the Equation class directly
e1 = Eqn(a, b + 1)
# second mode: short syntax
e2 =@ c + 1 = d
display(e1, e2)
$\displaystyle a = b + 1$
$\displaystyle c + 1 = d$

The above cell created two equations:

  • e1 was created by explicitly instanting the Equation class. While it might be longer to type, it is more readable as it clearly indicates what the code is doing.

  • e2 was created with the short syntax =@. This symbol combination was chosen to avoid conflicts with reserved python symbols while minimizing impacts on syntax highlighting and autoformatting. The short syntax is only available on interactive shells (like IPython, Jupyter Notebook).

Mathematical operators

Objects of type Equation supports the following mathematical operations:

[3]:
e1 + e2
[3]:
$\displaystyle a + c + 1 = b + d + 1$
[4]:
e1 - e2
[4]:
$\displaystyle a - c - 1 = b - d + 1$
[5]:
e1 * e2
[5]:
$\displaystyle a \left(c + 1\right) = d \left(b + 1\right)$
[6]:
e1 / e2
[6]:
$\displaystyle \frac{a}{c + 1} = \frac{b + 1}{d}$
[7]:
e1**2
[7]:
$\displaystyle a^{2} = \left(b + 1\right)^{2}$
[8]:
2**e1
[8]:
$\displaystyle 2^{a} = 2^{b + 1}$
[9]:
e1**e2
[9]:
$\displaystyle a^{c + 1} = \left(b + 1\right)^{d}$

Applying functions to Equation

We can apply mathematical functions (or user defined functions) to an Equation (or to a particular side) with the following methods:

  • eq.apply(func, *args, **kwargs): apply to both side of the equation, where *args, **kwargs are additional positional and keyword arguments to the function.

  • eq.applylhs(func, *args, **kwargs): apply to the LHS of the equation.

  • eq.applyrhs(func, *args, **kwargs): apply to the RHS of the equation.

[10]:
e1
[10]:
$\displaystyle a = b + 1$
[11]:
e1.apply(sin)
[11]:
$\displaystyle \sin{\left(a \right)} = \sin{\left(b + 1 \right)}$
[12]:
e1.applylhs(sin)
[12]:
$\displaystyle \sin{\left(a \right)} = b + 1$
[13]:
e1.applyrhs(sin)
[13]:
$\displaystyle a = \sin{\left(b + 1 \right)}$
[14]:
eq3 = Eqn(a, b / c)
eq3
[14]:
$\displaystyle a = \frac{b}{c}$

Compute the first cubic root:

[15]:
eq3.apply(root, 3)
[15]:
$\displaystyle \sqrt[3]{a} = \sqrt[3]{\frac{b}{c}}$

Compute the second cubic root, only on the RHS:

[16]:
eq3.applyrhs(root, 3, 1)
[16]:
$\displaystyle a = \left(-1\right)^{\frac{2}{3}} \sqrt[3]{\frac{b}{c}}$

Compute the third cubic root, only on the LHS:

[17]:
eq3.applylhs(root, 3, 2)
[17]:
$\displaystyle - \sqrt[3]{-1} \sqrt[3]{a} = \frac{b}{c}$

Apply a custom function:

[18]:
def add_square(expr):
    return expr + expr**2

eq3.apply(add_square)
[18]:
$\displaystyle a^{2} + a = \frac{b^{2}}{c^{2}} + \frac{b}{c}$
[19]:
add_square(eq3)
[19]:
$\displaystyle a^{2} + a = \frac{b^{2}}{c^{2}} + \frac{b}{c}$

Expression manipulation

factor(eq), simplify(eq), collect(eq) works on both sides of the Equation. The same is true for eq.factor(), eq.simplify(), eq.collect().

[20]:
var("x, y")
eq4 = Eqn(2**(x**2 + 2*x + 1), x * y**2 + 2*y*x)
eq4
[20]:
$\displaystyle 2^{x^{2} + 2 x + 1} = x y^{2} + 2 x y$
[21]:
eq4.factor()
[21]:
$\displaystyle 2^{x^{2} + 2 x + 1} = x y \left(y + 2\right)$
[22]:
eq4.factor(deep=True)
[22]:
$\displaystyle 2^{\left(x + 1\right)^{2}} = x y \left(y + 2\right)$
[23]:
simplify(eq4)
[23]:
$\displaystyle 2^{x \left(x + 2\right) + 1} = x y \left(y + 2\right)$
[24]:
eq4.collect(x)
[24]:
$\displaystyle 2^{x^{2} + 2 x + 1} = x \left(y^{2} + 2 y\right)$

In addition to the apply(), applylhs(), applyrhs() methods, it is also possible to work directly on a specified side of the equation and perform a complex sequence of manipulations by chaining different methods of the Expr class. This can be done with the following machinery:

  • do: work on both sides of the Equation.

  • dolhs: work on the LHS.

  • dorhs: work on the RHS.

These are not attributes, in the sense that they don’t return anything useful to the user. They are just machinery that allows to work on a particular side. For example:

[25]:
eq5 = Eqn(sin(x), series(sin(x), x, 0))
eq5
[25]:
$\displaystyle \sin{\left(x \right)} = x - \frac{x^{3}}{6} + \frac{x^{5}}{120} + O\left(x^{6}\right)$
[26]:
eq5 = eq5.dorhs.removeO().factor()
eq5
[26]:
$\displaystyle \sin{\left(x \right)} = \frac{x \left(x^{4} - 20 x^{2} + 120\right)}{120}$
[27]:
plot(*eq5.args, (x, -pi, pi), legend=True)
../_images/tutorials_tut-0-overview_38_0.png
[27]:
<sympy.plotting.backends.matplotlibbackend.matplotlib.MatplotlibBackend at 0x755696cda450>

Substitution and numerical evaluation

Equation.subs() is just a wrapper to SymPy’s Basic.subs() in order to make it aware of objects of type Equation. The following modes of operation are supported:

  • eq.subs(old, new)

  • eq.subs([(old1, new1), (old2, new2), ...])

  • eq.subs({old1: new1, old2: new2, ...})

  • eq.subs(other_eq): here, other_eq.lhs is the old value to replace, while other_eq.rhs is the new value to use.

  • eq.subs(eq2, eq3): substitute multiple equations. Here, the order of the arguments is important.

[28]:
var("p V n R T")
ideal_gas_law = Eqn(p * V, n * R * T)
pressure = ideal_gas_law / V
pressure
[28]:
$\displaystyle p = \frac{R T n}{V}$
[29]:
var("L atm mol K") # units
d = {R: 0.08206*L*atm/mol/K, T: 273*K, V: 24*L, n: 1*mol}
pressure.subs(d)
[29]:
$\displaystyle p = 0.9334325 atm$
[30]:
pressure.evalf(subs=d, n=3)
[30]:
$\displaystyle p = 0.933 atm$

An example showing the substitution of an equation into another:

[31]:
var("a:d")
eq_a = Eqn(a * (b + c), b / d)
eq_b = Eqn(b + c, 3 * d)
display(eq_a, eq_b)
$\displaystyle a \left(b + c\right) = \frac{b}{d}$
$\displaystyle b + c = 3 d$
[32]:
eq_a.subs(eq_b)
[32]:
$\displaystyle 3 a d = \frac{b}{d}$

Differentiation and Integration

diff(eq, *args, **kwargs) and integrate(eq, *args, **kwargs) works on both sides of the Equation. The same is true for eq.diff(*args, **kwargs) and eq.integrate(*args, **kwargs).

Use eq.applylhs or eq.applyrhs (or eq.dolhs, eq.dorhs) to differentiate only one side of the equation.

[33]:
e = Eqn(a * x**2 * y + b * x * y**2, x*y - c)
e
[33]:
$\displaystyle a x^{2} y + b x y^{2} = - c + x y$
[34]:
e.diff(y)
[34]:
$\displaystyle a x^{2} + 2 b x y = x$
[35]:
e.diff(x, 2)
[35]:
$\displaystyle 2 a y = 0$
[36]:
e.integrate((y, 1, 2))
[36]:
$\displaystyle \frac{3 a x^{2}}{2} + \frac{7 b x}{3} = - c + \frac{3 x}{2}$
[37]:
e.applylhs(diff, x)
[37]:
$\displaystyle 2 a x y + b y^{2} = - c + x y$
[38]:
e.dorhs.diff(y)
[38]:
$\displaystyle a x^{2} y + b x y^{2} = x$
[39]:
e.dorhs.integrate(x)
[39]:
$\displaystyle a x^{2} y + b x y^{2} = - c x + \frac{x^{2} y}{2}$

Attributes and Methods

  • eq.swap: swap the LHS with RHS.

  • eq.cross_multiply()

  • eq.as_expr(): convert the Equation to a symbolic expression by writing LHS - RHS.

  • eq.as_Boolean(): convert the Equation to a SymPy’s Equality (which does not support mathematical operations).

  • eq.check(): verify if the LHS is mathematically equivalent to the RHS.

[40]:
var("a:d")
e = Eqn(a/b, c/d)
e
[40]:
$\displaystyle \frac{a}{b} = \frac{c}{d}$
[41]:
e.swap
[41]:
$\displaystyle \frac{c}{d} = \frac{a}{b}$
[42]:
e.cross_multiply()
[42]:
$\displaystyle a d = b c$
[43]:
e.as_expr()
[43]:
$\displaystyle \frac{a}{b} - \frac{c}{d}$
[44]:
equality = e.as_Boolean()
print(type(equality))
equality
<class 'sympy.core.relational.Equality'>
[44]:
$\displaystyle \frac{a}{b} = \frac{c}{d}$

Note that there is nothing stopping us from writing “wrong” equations. For example:

[45]:
f = Eqn(0, 1)
f
[45]:
$\displaystyle 0 = 1$

To verify if the two sides are mathematically equivalent let’s execute:

[46]:
f.check()
[46]:
$\displaystyle \text{False}$

If the equivalence can’t be determited, an object of type Equality will be returned:

[47]:
res = e.check()
print(type(res))
res
<class 'sympy.core.relational.Equality'>
[47]:
$\displaystyle \frac{a}{b} = \frac{c}{d}$

Solving equations

If one or more objects of type Equation are provided to solve(), then the output will contains object of the same type:

[48]:
e = Eqn(a*x**2, -b*c - c)
e
[48]:
$\displaystyle a x^{2} = - b c - c$
[49]:
solve(e, x)
[49]:
$\displaystyle \left[ x = - \sqrt{- \frac{c \left(b + 1\right)}{a}}, \ x = \sqrt{- \frac{c \left(b + 1\right)}{a}}\right]$

Differently, if equations are converted to expressions, than solve() will behave exactly like SymPy’s solve():

[50]:
solve(e.as_expr(), x)
[50]:
$\displaystyle \left[ - \sqrt{- \frac{c \left(b + 1\right)}{a}}, \ \sqrt{- \frac{c \left(b + 1\right)}{a}}\right]$

Solving multiple equations:

[51]:
e1 = Eqn(x**2 * y, -2 + x)
e2 = Eqn(y**2 * x, -3 * y)
solve([e1, e2], [x, y])
[51]:
$\displaystyle \left[ \left[ x = \frac{1}{2}, \ y = -6\right], \ \left[ x = 2, \ y = 0\right]\right]$
[52]:
solve([e.as_expr() for e in [e1, e2]], [x, y])
[52]:
$\displaystyle \left[ \left( \frac{1}{2}, \ -6\right), \ \left( 2, \ 0\right)\right]$
[53]:
solve([e.as_expr() for e in [e1, e2]], [x, y], dict=True)
[53]:
$\displaystyle \left[ \left\{ x : \frac{1}{2}, \ y : -6\right\}, \ \left\{ x : 2, \ y : 0\right\}\right]$

Configuration options

equation_config is an object containing a few properties to customize the behaviour of the module:

[54]:
from sympy_equation import equation_config

Arguably the most useful options are :

  • equation_config.show_label

  • equation_config.integers_as_exact

Hide/Show the equation label

By default, the equation’s label will be hidden:

[55]:
equation_config.show_label
[55]:
False
[56]:
var("a:d")
e = Eqn(a / b, c / d)
e
[56]:
$\displaystyle \frac{a}{b} = \frac{c}{d}$

When it’s True, a label with the name of the equation in the python environment will be shown on the screen.

[57]:
equation_config.show_label = True
e
[57]:
$\displaystyle \frac{a}{b} = \frac{c}{d}\qquad (e)$

Integer as exact

When it’s True and we are running in an IPython/Jupyter environment, it preparses the content of a code line in order to convert integer numbers to sympy’s Integer. In doing so, we can write 2/3, which will be converted to Integer(2)/Integer(3), which then SymPy converts to Rational(2, 3). If False, no preparsing is done, and Python evaluates 2/3 to 0.6666667, which will then be converted by SymPy to a Float.

[58]:
equation_config.integers_as_exact
[58]:
False
[59]:
e = Eqn(a + 1 / 2, b + 3 / 4)
e
[59]:
$\displaystyle a + 0.5 = b + 0.75\qquad (e)$
[60]:
equation_config.integers_as_exact = True
[61]:
e = Eqn(a + 1 / 2, b + 3 / 4)
e
[61]:
$\displaystyle a + \frac{1}{2} = b + \frac{3}{4}\qquad (e)$

It is reccommended to set this options to True only when executing purely symbolic computations, not when using other numerical libraries, such as Numpy, because it will create hard to debug situations. Consider executing this: np.cos(np.pi / 4). If integers_as_exact = True, this will raise an error because 4 is first replaced with sympy’s Integer(4), then np.pi / Integer(4) becomes a symbolic expression and np.cos is unable to evaluate it.

[62]:
import numpy as np
np.cos(np.pi / 4)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
AttributeError: 'Float' object has no attribute 'cos'

The above exception was the direct cause of the following exception:

TypeError                                 Traceback (most recent call last)
Cell In[62], line 3
      1 import numpy as np
----> 3 np .cos (np .pi /Integer (4 ))

TypeError: loop of ufunc does not support argument 0 of type Float which has no callable cos method

Example 1

Given the equations of the volume and mass of a cylindrical pressure vessel, rewrite the mass in terms of the volume and the ratio \(L/D\).

Parameters:

  • V: volume;

  • M: mass;

  • D: diameter;

  • p: pressure inside the vessel;

  • sigma: stress level on the thin walls;

  • rho: density of the material.

[63]:
equation_config.show_label = False
var("D, L, V, M, rho, p, sigma")
Veq = Eqn(V, pi * D**3 / 6 + pi * D**2 * L / 4)
Meq = Eqn(M, pi * D**2 * rho * p / (2 * sigma) * (L + D / 2))
display(Veq, Meq)
$\displaystyle V = \frac{\pi D^{3}}{6} + \frac{\pi D^{2} L}{4}$
$\displaystyle M = \frac{\pi D^{2} p \rho \left(\frac{D}{2} + L\right)}{2 \sigma}$
[64]:
Veq = (Veq / D**3).expand()
Veq
[64]:
$\displaystyle \frac{V}{D^{3}} = \frac{\pi}{6} + \frac{\pi L}{4 D}$
[65]:
Veq = Veq.collect(pi)
Veq
[65]:
$\displaystyle \frac{V}{D^{3}} = \pi \left(\frac{1}{6} + \frac{L}{4 D}\right)$
[66]:
Meq = (Meq / D**3).expand()
Meq
[66]:
$\displaystyle \frac{M}{D^{3}} = \frac{\pi p \rho}{4 \sigma} + \frac{\pi L p \rho}{2 D \sigma}$
[67]:
Meq = Meq.collect(pi * rho * p / sigma)
Meq
[67]:
$\displaystyle \frac{M}{D^{3}} = \frac{\pi p \rho \left(\frac{1}{4} + \frac{L}{2 D}\right)}{\sigma}$
[68]:
# isolate D**3
Veq = 1 / Veq * V
Veq
[68]:
$\displaystyle D^{3} = \frac{V}{\pi \left(\frac{1}{6} + \frac{L}{4 D}\right)}$
[69]:
eq = Meq.subs(Veq)
eq
[69]:
$\displaystyle \frac{\pi M \left(\frac{1}{6} + \frac{L}{4 D}\right)}{V} = \frac{\pi p \rho \left(\frac{1}{4} + \frac{L}{2 D}\right)}{\sigma}$
[70]:
eq = eq.cross_multiply()
eq
[70]:
$\displaystyle \pi M \sigma \left(\frac{1}{6} + \frac{L}{4 D}\right) = \pi V p \rho \left(\frac{1}{4} + \frac{L}{2 D}\right)$
[71]:
eq = eq / (pi * sigma)
eq
[71]:
$\displaystyle M \left(\frac{1}{6} + \frac{L}{4 D}\right) = \frac{V p \rho \left(\frac{1}{4} + \frac{L}{2 D}\right)}{\sigma}$
[72]:
res = eq / eq.lhs * M
res
[72]:
$\displaystyle M = \frac{V p \rho \left(\frac{1}{4} + \frac{L}{2 D}\right)}{\sigma \left(\frac{1}{6} + \frac{L}{4 D}\right)}$
[ ]: