The python way of controlling an arbitrary number of variables used to solve equations.

This is a little difficult to explain without a direct example. Therefore, let us consider a very simplified ideal gas law . For an ideal gas under normal conditions, the following equality holds:

PV = RT 

This means that if we know 3 of 4 variables (pressure, volume, specific gas constant and temperature), we can decide for another.

How would I put this inside an object? I want to have an object where I can just insert 3 of the variables, and then it calculates the 4th. I wonder if this can be achieved using properties?

My best guess is to insert it like:

 class gasProperties(object): __init__(self, P=None, V=None, R=None, T=None) self.setFlowParams(P, V, R, T) def setFlowParams(self, P=None, V=None, R=None, T=None) if P is None: self._P = R*T/V self._V = V self._R = R self._T = T elif V is None: self._V = R*T/P self._P = P self._R = R self._T = T #etc 

Although this is rather cumbersome and error prone (I have to add checks to make sure that one of the parameters is set to "No").

Is there a better, cleaner way?

I see that this β€œproblem” occurs quite often in different ways, and especially after the number of variables grows (adding density, number of reynolds, viscosity to the mix), the number of different if-statements grows rapidly. (IE, if I have 8 variables, and any 5 makes the system unique, I will need 8 instructions nCr 5 = 56 if).

+7
python
source share
6 answers

Using sympy , you can create a class for each of your equations. Create equation symbols with Ο‰, Ο€ = sp.symbols('Ο‰ Ο€') , etc., by the equation itself, and then use the f() function to do the rest:

 import sympy as sp # Create all symbols. P, V, n, R, T = sp.symbols('PV n R T') # Create all equations IDEAL_GAS_EQUATION = P*V - n*R*T def f(x, values_dct, eq_lst): """ Solves equations in eq_lst for x, substitutes values from values_dct, and returns value of x. :param x: Sympy symbol :param values_dct: Dict with sympy symbols as keys, and numbers as values. """ lst = [] lst += eq_lst for i, j in values_dct.items(): lst.append(sp.Eq(i, j)) try: return sp.solve(lst)[0][x] except IndexError: print('This equation has no solutions.') 

To try this ...:

 vals = {P: 2, n: 3, R: 1, T:4} r = f(V, values_dct=vals, eq_lst=[IDEAL_GAS_EQUATION, ]) print(r) # Prints 6 

If you do not provide enough parameters via values_dct , you will get a result like 3*T/2 , checking its type() , you will get <class 'sympy.core.mul.Mul'> .

If you provide all the parameters that you get as a result of 6 , and its type is <class 'sympy.core.numbers.Integer'> , you can throw exceptions or whatever you need. You can also convert it to int using int() (this will throw an error if instead of 6 you have 3*T/2 so you can check it the same way).

Alternatively, you can simply check if the None values ​​in values_dct greater than 1.


To combine several equations, for example PV=nRT and P=2m , you can create an additional symbol m , like the previous symbols, and assign 2m new equation name MY_EQ_2 , and then insert it into the eq_lst function:

 m = sp.symbols('m') MY_EQ_2 = P - 2 * m vals = {n: 3, R: 1, T:4} r = f(V, values_dct=vals, eq_lst=[IDEAL_GAS_EQUATION, MY_EQ_2]) print(r) # Prints 6/m 
+4
source share

Basic solution using sympy and kwargs to verify the information provided by the user:

 from sympy.solvers import solve from sympy import Symbol def solve_gas_properties(**kwargs): properties = [] missing = None for letter in 'PVRT': if letter in kwargs: properties.append(kwargs[letter]) elif missing is not None: raise ValueError("Expected 3 out of 4 arguments.") else: missing = Symbol(letter) properties.append(missing) if missing is None: raise ValueError("Expected 3 out of 4 arguments.") P, V, R, T = properties return solve(P * V - R * T, missing) print solve_gas_properties(P=3, V=2, R=1) # returns [6], the solution for T 

This can then be converted into a class method using class properties instead of keyword arguments if you want to store and manage various values ​​in the system.

The above can also be rewritten as:

 def gas_properties(**kwargs): missing = [Symbol(letter) for letter in 'PVRT' if letter not in kwargs] if len(missing) != 1: raise ValueError("Expected 3 out of 4 arguments.") missing = missing[0] P, V, R, T = [kwargs.get(letter, missing) for letter in 'PVRT'] return solve(P * V - R * T, missing) 
+3
source share

One solution might be to use a dictionary to store variable names and their values. This makes it easy to add other variables at any time. In addition, you can verify that only one variable is set to No by counting the number of No items in the dictionary.

+1
source share

My approach will be pretty simple:

 class GasProperties(object): def __init__(self, P=None, V=None, R=None, T=None): self.setFlowParams(P, V, R, T) def setFlowParams(self, P=None, V=None, R=None, T=None): if sum(1 for arg in (P, V, R, T) if arg is None) != 1: raise ValueError("Expected 3 out of 4 arguments.") self._P = P self._V = V self._R = R self._T = T @property def P(self): return self._P is self._P is not None else self._R*self._T/self._V 

Similarly defined properties for V, R and T.

+1
source share

This approach allows you to customize the attributes of an object:

 def setFlowParams(self, P=None, V=None, R=None, T=None): params = self.setFlowParams.func_code.co_varnames[1:5] if sum([locals()[param] is None for param in params]) > 1: raise ValueError("3 arguments required") for param in params: setattr(self, '_'+param, locals()[param]) 

In addition, you need to define getters for attributes with formulas. Like this:

 @property def P(self): if self._P is None: self._P = self._R*self._T/self._V return self._P 

Or calculate all the values ​​in setFlowParams.

0
source share

Numerical methods

You might want to do this without sympy , for example, and execute, for example, using root find . The beauty of this method lies in the fact that it works in an extremely wide range of equations, which even those with symptoms may encounter. Everyone I know taught this at the university in a bachelor's degree in mathematics *, unfortunately, many of them cannot put it into practice.

So, first we get rootfinder, you can find code examples on Wikipedia and on the network as a whole, this is pretty well known. Many math packages have built-in ones, for example scipy.optimize for good root crawlers. I am going to use the secant method to simplify the implementation (in this case, I really do not need iterations, but in any case use the general versions if you want to use some other formulas).

 """Equation solving with numeric root finding using vanilla python 2.7""" def secant_rootfind(f, a, incr=0.1, accuracy=1e-15): """ secant root finding method """ b=a+incr; while abs(f(b)) > accuracy : a, b = ( b, b - f(b) * (b - a)/(f(b) - f(a)) ) class gasProperties(object): def __init__(self, P=None,V=None,n=None,T=None): self.vars = [P, V, n, 8.314, T] unknowns = 0 for i,v in enumerate(self.vars): if v is None : self._unknown_=i unknowns += 1 if unknowns > 1: raise ValueError("too many unknowns") def equation(self, a): self.vars[self._unknown_] = a P, V, n, R, T = self.vars return P*V - n*R*T # = 0 def __str__(self): return str(( "P = %f\nV = %f\nn = %f\n"+ "R = %f\nT = %f ")%tuple(self.vars)) def solve(self): secant_rootfind(self.equation, 0.2) print str(self) if __name__=="__main__": # run tests gasProperties(P=1013.25, V=1., T=273.15).solve() print "--- test2---" gasProperties( V=1,n = 0.446175, T=273.15).solve() 

The advantage of finding root is that even if your formula is not so simple, it will still work, so any number of formulas can be done without code, except for writing the wording. This is usually a very useful skill. SYMPY is good, but symbolic math is not always easily solvable

Correal solver easily extends in the case of vectors and multi equations, even when solving matrices. Ready made scipy functions created for optimization have already done this by default.

Here are some more resources:

* most have been introduced at least for the Newton-Raphson method

0
source share

All Articles