管理任意数量变量的 Pythonic 方式,用于方程求解。

Pythonic way to manage arbitrary amount of variables, used for equation solving.

如果没有直接的例子,这有点难以解释。因此,让我们以非常简单的 ideal-gas law 为例。对于正常情况下的理想气体,以下等式成立:

PV = RT

这意味着如果我们知道 4 个变量中的 3 个(压力、体积、特定气体常数和温度),我们就可以求解另一个。

如何将其放入对象中?我想要一个对象,我可以在其中插入 3 个变量,然后计算第 4 个。请问是否可以通过属性实现?

我目前最好的猜测是像这样插入它:

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

虽然这很麻烦,而且容易出错(我必须添加检查以确保恰好有一个参数设置为 "None")。

有没有更好、更简洁的方法?

我看到这种情况 "problem" 以各种不同的方式经常发生,尤其是一旦变量的数量增加(增加密度、雷诺数、混合粘度),不同的 if-语句增长很快。 (即,如果我有 8 个变量并且任何 5 个使系统唯一,我将需要 8 nCr 5 = 56 if 语句)。

一种解决方案是使用字典来存储变量名称及其值。这使您可以随时轻松添加其他变量。此外,您可以通过计算字典中 "None" 项的数量来检查是否只有一个变量具有值 "None"。

我的方法相当简单:

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

您同样定义 V、R 和 T 的属性。

此方法允许您设置对象的属性:

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])

此外,您需要为带有公式的属性定义getter。像这样:

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

或者计算setFlowParams中的所有值。

使用 sympykwargs 检查用户提供的信息的基本解决方案:

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

如果您想在系统中存储和操作不同的值,则可以将其转换为 class 方法,利用 class 属性而不是关键字参数。

上面也可以改写为:

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)

使用sympy,您可以为每个方程创建一个class。用 ω, π = sp.symbols('ω π') 等创建等式的符号,等式本身,然后使用函数 f() 完成其余的工作:

import sympy as sp    

# Create all symbols.
P, V, n, R, T = sp.symbols('P V 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.')

试试这个...:

vals = {P: 2, n: 3, R: 1, T:4}

r = f(V, values_dct=vals, eq_lst=[IDEAL_GAS_EQUATION, ])
print(r)   # Prints 6

如果您没有通过 values_dct 提供足够的参数,您将得到类似 3*T/2 的结果,检查其 type() 您会得到 <class 'sympy.core.mul.Mul'>

如果您确实提供了所有参数,您得到的结果是 6 并且它的类型是 <class 'sympy.core.numbers.Integer'>,因此您可以引发异常或任何您需要的。您也可以使用 int() 将其转换为 int (如果您使用 3*T/2 而不是 6 会引发错误,因此您也可以那样测试它)。

或者,您可以简单地检查 values_dct 中的 None 值是否大于 1。


要组合多个方程式,例如 PV=nRTP=2m,您可以像以前的符号一样创建额外的符号 m 并将 2m 分配给新方程式name MY_EQ_2,然后将其插入函数的 eq_lst 中:

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

数值方法

您可能想要在没有 sympy 的情况下执行此操作,例如,使用数字 root finding 进行练习。这种方法的美妙之处在于它适用于范围极广的方程式,即使是有同情心的人也会遇到麻烦。我认识的每个人都在大学学士数学课程中教过这个*,不幸的是,没有多少人可以在实践中应用它。

因此,首先我们获取 rootfinder,您可以在维基百科和整个网络上找到代码示例,这是众所周知的东西。许多数学包都内置了这些,例如 scipy.optimize 以获得好的根查找器。我将使用正割法来简化实现(在这种情况下我真的不需要迭代,但如果您碰巧想使用其他一些公式,我还是会使用通用版本)。

"""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()

求根的好处是,即使您的公式不是那么简单,它仍然可以工作,因此只需编写公式就可以完成任意数量的公式。这通常是一项非常有用的技能。 SYMPY 很好,但符号数学并不总是很容易解决

根求解器很容易扩展到向量和多方程的情况,甚至是矩阵求解。为优化而构建的现成 scipy 函数默认情况下已完成此操作。

这里还有一些资源:

* 大多数至少引入了 Newton–Raphson 方法