Package: pyequations
PyEquationsTable of ContentsOverviewInstallationExampleAdditional FeaturesMulti-Level InheritanceNo-Solution HandlingRecursive BranchingExact Solution FormNo Need for Initial ValuesAutomatic Branch Deletion for Invalid ValuesAPIAdditional NotesOverviewPyEquations is a Python package for solving a bunch of equations with a bunch of unknowns.
PyEquations maximally solves with the given equations, functions, and variables. It internally uses SymPy
for symbolic math and supports units on all variables.The key advantage of this package is that you don't need to know what specific equations to use to solve for a certain value.
You can simply dump all the equations and the known values. PyEquations will figure out the rest.InstallationpipinstallpyequationsExampleSuppose we on top of a 100-meter tall building, and we throw a ball up with an initial velocity of 3 meters/second.
After how long does the ball hit the ground and with what velocity?This is a simple problem if one has studied physics. Everything can be solved for using the kinematic equations (of constant acceleration):$$ v_f = v_0 + at $$$$ x_f = x_0 + v_0t + \frac{1}{2}at^2 $$$$ v_f^2 = v_0^2 + 2a(x_f - x_0) $$$$ x_f = x_0 + \frac{1}{2}(v_0 + v_f)t $$Let's demonstrate how to solve this problem with PyEquations.# Import PyEquartions dependenciesfrompyequations.inheritablesimportPyEquationsfrompyequations.decoratorsimporteq# Create a class that inherits from PyEquationsclassKinematic(PyEquations):def__init__(self):# Initialize the PyEquations class with the variables# Also supports a list of variable names if descriptions are not neededsuper().__init__({'x_0':'Initial position','x_f':'Final position','v_0':'Initial velocity','v_f':'Final velocity','a':'Acceleration','t':'Time'})# Define the equations as methods with the @eq decorator# Reference variables as class attributes# Every @eq method must return two-element tuple, with the comma representing an '='@eqdefcalc_v_f(self):# v_f = v_0 + atreturnself.v_f,self.v_0+self.a*self.t@eqdefcalc_x_f(self):# x_f = x_0 + v_0t + 0.5at^2returnself.x_f,self.x_0+self.v_0*self.t+0.5*self.a*self.t**2@eqdefcalc_v_f_2(self):# v_f^2 = v_0^2 + 2a(x_f - x_0)returnself.v_f**2,self.v_0**2+2*self.a*(self.x_f-self.x_0)@eqdefcalc_x_f_2(self):# x_f = x_0 + 0.5(v_0 + v_f)treturnself.x_f,self.x_0+0.5*(self.v_0+self.v_f)*self.tNow, we can use the class to solve for a given set of known values.# Import SymPy units if you want to use unitsfromsympy.physics.unitsimportmeter,second# Create an instance of the classk=Kinematic()# Set the known valuesk.x_0=100*meterk.v_0=3*meter/secondk.a=-9.8*meter/second**2k.x_f=0*meter# Call the solverk.solve()# Print the resultsprint(k.vars)The output of this code is:[{'x_0':100*meter,'x_f':0,'v_0':3*meter/second,'v_f':-44.3734154646676*meter/second,'a':-9.8*meter/second**2,'t':4.83402198619057*second},{'x_0':100*meter,'x_f':0,'v_0':3*meter/second,'v_f':44.3734154646676*meter/second,'a':-9.8*meter/second**2,'t':-4.22177708823138*second}]And just like that, PyEquations figured out the rest of the unknowns for us.But, if you look carefully at the output, you'll notice that there are two solution branches.
PyEquations figured out that there are two possible solutions with the given equations and known values.
One of them has negative time, which we do not want for out ball-off-building example.Let's fix this problem while also showcasing another feature of PyEquations.# Import the func decoratorfrompyequations.decoratorsimportfuncclassKinematic(PyEquations):# ... rest of the code# Add a function decorated with @func# This function is called during solving# It can access and set variables, and even delete solution branches@funcdefparse(self):ifself.t<0:self.del_branch()# ... rest of the codeThe above code adds a parsing function that deletes the solution branch if the time is negative.The output of the updated code is:[{'x_0':100*meter,'x_f':0,'v_0':3*meter/second,'v_f':-44.3734154646676*meter/second,'a':-9.8*meter/second**2,'t':4.83402198619057*second}]Now, we have only one solution, which is the correct one.If we want to access the solution values, we can do so as follows:print(k.get_var_vals('t'))print(k.get_var_vals('v_f'))The output of this code is:[4.83402198619057*second][-44.3734154646676*meter/second]Invokingget_var_valswith a variable name returns a list of all the values of that variable in the solution branches.However, in a case like this where there is only one solution branch,
we can jut directly use the class attributeprint(k.t)print(k.v_f)The output of this code is:4.83402198619057*second
-44.3734154646676*meter/secondThe reason we can directly access the attributes is due to how PyEquations handles multiple solutions.
It uses a technique very similar to context switching in order to handle multiple solution branches.
This means that the class attributes are always set to the values of the current context.Furthermore, if you want to know how many solution branches there are, you can use thenum_branchesattribute.print(k.num_branches)The output of this code is:1Additional FeaturesMulti-Level InheritancePyEquations supports multi-level inheritance. This is useful for creating a hierarchy of equations.
A sample use case is having a parent class called Silicon that contains all the equations for silicon.
A child class called SiliconDiode can inherit from Silicon and add the equations for a diode while maintaining the equations for silicon.Here is a basic example of the syntax and how it works:classBase(PyEquations):def__init__(self):super().__init__(['x','y'])@eqdefeq1(self):returnself.x+self.y,6@eqdefeq2(self):returnself.x-self.y,2classChild(Base):def__init__(self):super().__init__()self.add_variables(['z'])@eqdefeq3(self):returnself.z,self.x*self.yclassGrandchild(Child):def__init__(self):super().__init__()self.add_variables(['a'])@eqdefeq4(self):returnself.a,self.z**2g=Grandchild()g.solve()print(g.vars)The output of this code is:[{'a':64,'x':4,'y':2,'z':8}]Note how PyEquations was able to solve for all the variables in the hierarchy.
Furthermore, all variables are alphabetically sorted in the output.No-Solution HandlingLet's say you input the following system into PyEquations:$$ y = x + 1 $$$$ y = x + 2 $$Or rather, if you enter any system with no solution, PyEquations will
raise aRunTimeErrorwhile noting that no consistent solutions exist..Recursive BranchingPyEquations systems of equations can branch infinitely.
If multiple variables have multiple solutions, all valid solutions will be accounted for.
This includes imaginary solutions. If these are unwanted, a@funcdecorated method can be used to delete branches
with imaginary solutions, similar to the example above.
For example, the output of PyEquations solving this system:$$ x^3 + 2x^2 + 4x + 2 = 0$$is:[{'x':-2/3+(-1/2-sqrt(3)*I/2)*(1/27+sqrt(57)/9)**(1/3)-8/(9*(-1/2-sqrt(3)*I/2)*(1/27+sqrt(57)/9)**(1/3))},{'x':-2/3-8/(9*(-1/2+sqrt(3)*I/2)*(1/27+sqrt(57)/9)**(1/3))+(-1/2+sqrt(3)*I/2)*(1/27+sqrt(57)/9)**(1/3)},{'x':-8/(9*(1/27+sqrt(57)/9)**(1/3))-2/3+(1/27+sqrt(57)/9)**(1/3)}]Wow, that's a mess. Note that you can also use thevars_decimalattribute to get the decimal values of the variables:[{'x':-0.680551540264324-1.63317024091524*I},{'x':-0.680551540264324+1.63317024091524*I},{'x':-0.638896919471353}]Exact Solution FormAs demonstrated above, PyEquations can solve systems of equations and return the exact solution form.No Need for Initial ValuesAlso as demonstrated in the Recursive Branching section, PyEquations does not require initial values to solve a system.Automatic Branch Deletion for Invalid ValuesLet's say you were working with this system$$ x^2 = 16 $$$$ x + y = 10 $$$$ x - y = -2 $$Looking at the first equation, PyEquations would branch into two branches, one with $x = 4$ and one with $x = -4$.
However, the second and third equations together are only valid for $x = 4$ and $y = 6$.PyEquations will automatically delete the branch with $x = -4$ because it is invalid in this system.
If neither $x = 4$ nor $x = -4$ were valid, PyEquations would raise aRuntimeErrornoting that no consistent solutions exist.API@eqFunction decorator used for notifying PyEquations that a method is an equationThe method must return a tuple of the form (expression, expression), with the comma symbolizing an equality@funcFunction decorator used for notifying PyEquations that a method is a functionThe method will be run during solvingCan access/modify variables and delete branchessolve() -> NoneUsed for running the solvervarsProperty used for getting the solution in exact formThe solution is returned as a list of dictionariesvars_decimalProperty used for getting the solution in decimal formThe solution is returned as a list of dictionariesnum_branchesProperty used for getting the number of solution branchesReturns an intlockedProperty used for getting the locked status of the objectReturns a boolLocks from changing or adding variablesAutomatically locked after branchingadd_variables(var_descriptions: dict[str, str] | list[str]) -> NoneUsed for adding variables to the systemMust be called before branchingcontext_switch(self, target_branch: int) -> NoneUsed for switching to a different branchMostly used internally, but can be used to switch contexts manually to easily access variablesrotate_context(self) -> NoneMostly used internally, but can be used to rotate contexts manually to easily access variablesdelete_branch(self, branch: int) -> Noneused for deleting a branchUsed in@funcdecorated methodsvar_description(self, name: str) -> strUsed for getting the description of a variableget_var_vals(self, name: str) -> listUsed for getting the values of a variable spanning all branchesget_var_vals_decimal(self, name: str) -> listUsed for getting the decimal values of a variable spanning all branchessolved(*variables) -> boolUsed for checking if all the provided variables has been solvedExample usage:solved(self.x, self.y, self.z)would returnTrueif all three variables have been solved in the
current branchAdditional NotesSystems can only be solved onceA given object that inherits from PyEquations can only be solved onceThis is to avoid manually setting values resulting in invalid statesDoing so will result in aRuntimeErrorIf you want to solve with different values, create a new object or re-run with different valuesVariables can only be added/set before the system branchesDoing so will result in aRuntimeErrorThis is to avoid ambiguity in setting when there could be potentially multiple branches to set@eqand@funcdecorated methods must be present at instantiationThese functions are gathered and processed at instantiation, inefficient (and unnecessary) to check for them later