使用SymPy强制对象属性之间的代数关系

时间:2014-03-07 08:27:54

标签: python-2.7 sympy

我有兴趣使用SymPy来增强我的工程模型。我不想定义一组严格的输入和输出,而是希望用户只需提供他们所知道的关于系统的所有信息,然后将该数据应用于计算未知数的代数模型(如果有足够的数据)。

例如,假设我有一些stuff,其中有一些massvolumedensity。我想定义这些参数(density = mass / volume)之间的关系,这样当用户提供了足够的信息(任何2个变量)时,会自动计算第3个变量。最后,如果稍后更新任何值,则应更改其他值之一以保留关系。该系统面临的挑战之一是,当存在多个自变量时,需要有一种方法来指定哪个自变量应该更改以满足要求。

这是我目前使用的一些工作代码:

from sympy import *

class Stuff(object):
    def __init__(self, *args, **kwargs):
        #String of variables
        varString = 'm v rho'

        #Initialize Symbolic variables
        m, v, rho = symbols(varString)

        #Define density equation
        # "rho = m / v" becomes "rho - m / v = 0" which means the eqn. is rho - m / v
        # This is because solve() assumes equation = 0
        density_eq = rho - m / v

        #Store the equation and the variable string for later use
        self._eqn = density_eq
        self._varString = varString

        #Get a list of variable names
        variables = varString.split()

        #Initialize parameters dictionary
        self._params = {name: None for name in variables}

    @property
    def mass(self):
        return self._params['m']

    @mass.setter
    def mass(self, value):
        param = 'm'
        self._params[param] = value
        self.balance(param)

    @property
    def volume(self):
        return self._params['v']

    @volume.setter
    def volume(self, value):
        param = 'v'
        self._params[param] = value
        self.balance(param)

    @property
    def density(self):
        return self._params['rho']

    @density.setter
    def density(self, value):
        param = 'rho'
        self._params[param] = value
        self.balance(param)

    def balance(self, param):

        #Get the list of all variable names
        variables = self._varString.split()

        #Get a copy of the list except for the recently changed parameter
        others = [name for name in variables if name != param]

        #Loop through the less recently changed variables
        for name in others:
            try:
                #Symbolically solve for the current variable
                eq = solve(self._eqn, [symbols(name)])[0]
                #Get a dictionary of variables and values to substitute for a numerical solution (will contain None for unset values)
                indvars = {symbols(n): self._params[n] for n in variables if n is not name}
                #Set the parameter with the new numeric solution
                self._params[name] = eq.evalf(subs=indvars)
            except Exception, e:
                pass


if __name__ == "__main__":

    #Run some examples - need to turn these into actual tests

    stuff = Stuff()
    stuff.density = 0.1
    stuff.mass = 10.0
    print stuff.volume

    stuff = Water()
    stuff.mass = 10.0
    stuff.volume = 100.0
    print stuff.density

    stuff = Water()
    stuff.density = 0.1
    stuff.volume = 100.0
    print stuff.mass

    #increase volume
    print "Setting Volume to 200"
    stuff.volume = 200.0
    print "Mass changes"
    print "Mass {0}".format(stuff.mass)
    print "Density {0}".format(stuff.density)

    #Setting Mass to 15
    print "Setting Mass to 15.0"
    stuff.mass = 15.0
    print "Volume changes"
    print "Volume {0}".format(stuff.volume)
    print "Density {0}".format(stuff.density)

    #Setting Density to 0.5
    print "Setting Density to 0.5"
    stuff.density = 0.5
    print "Mass changes"
    print "Mass {0}".format(stuff.mass)
    print "Volume {0}".format(stuff.volume)

    print "It is impossible to let mass and volume drive density with this setup since either mass or volume will " \
          "always be recalculated first."

我试着尽可能优雅地使用整体方法和课程布局,但我不禁想知道我是不是错了 - 如果我以错误的方式使用SymPy完成这项任务。我有兴趣开发一种复杂的航空航天飞行器模型,它具有数十个/数百个相互关联的特性。在我从这个相当简单的例子中扩展之前,我想找到一种优雅且可扩展的方式来使用SymPy来管理整个车辆的财产关系。

我也关心如何/何时重新平衡方程式。我熟悉PyQt信号和插槽,这是我第一次想到如何将相关事物链接在一起以触发模型更新(当值更新时发出信号,这将由依赖于每个方程组的重新平衡函数接收那个参数?)。是的,我真的不知道使用SymPy做到这一点的最好方法。可能需要一个更大的例子来探索方程组。

以下是关于我在这个项目中的发展方向的一些想法。仅以质量为例,我想将整个车辆质量定义为子系统质量的总和,并将所有子系统质量定义为组件质量的总和。此外,某些子系统和组件之间将存在质量关系。这些关系将驱动模型,直到提供更具体的数据。因此,如果燃料质量与总车辆质量的默认比率为50%,则指定100lb燃料将使车辆的尺寸为200lb。然而,如果我后来指定车辆实际上是210lb,我会希望重新计算关系(让它变得依赖,因为燃料质量和车辆质量最近设定,或者因为我指定它们是独立变量或锁定或什么的)。下一个问题是迭代。当模型中存在循环或冲突关系时,必须迭代模型以希望收敛于解决方案。这通常是上述车辆质量模型的情况。如果车辆变重,需要更多的燃料来满足要求,这会导致车辆变得更重,等等。我不确定如何在这些情况下利用SymPy。

有什么建议吗?

PS Good explanation of the challenges associated with space launch vehicle design.

修改:根据goncalopp的建议改变代码结构......

class Balanced(object):
    def __init__(self, variables, equationStr):
        self._variables = variables
        self._equation = sympify(equationStr)

        #Initialize parameters dictionary
        self._params = {name: None for name in self._variables}

        def var_getter(varname, self):
            return self._params[varname]

        def var_setter(varname, self, value):
            self._params[varname] = value
            self.balance(varname)

        for varname in self._variables:
            setattr(Balanced, varname, property(fget=partial(var_getter, varname),
                                                fset=partial(var_setter, varname)))

    def balance(self, recentlyChanged):

        #Get a copy of the list except for the recently changed parameter
        others = [name for name in self._variables if name != recentlyChanged]

        #Loop through the less recently changed variables
        for name in others:
            try:
                eq = solve(self._equation, [symbols(name)])[0]
                indvars = {symbols(n): self._params[n] for n in self._variables if n != name}
                self._params[name] = eq.evalf(subs=indvars)
            except Exception, e:
                pass


class HasMass(Balanced):
    def __init__(self):
        super(HasMass, self).__init__(variables=['mass', 'volume', 'density'],
                                      equationStr='density - mass / volume')


class Prop(HasMass):
    def __init__(self):
        super(Prop, self).__init__()

if __name__ == "__main__":

    prop = Prop()
    prop.density = 0.1
    prop.mass = 10.0
    print prop.volume

    prop = Prop()
    prop.mass = 10.0
    prop.volume = 100.0
    print prop.density

    prop = Prop()
    prop.density = 0.1
    prop.volume = 100.0
    print prop.mass

这让我想做的是使用多重继承或装饰器自动为物体分配物理相互关联的属性。所以我可以有另一个名为“Cylindrical”的类来定义半径,直径和长度属性,然后我就可以使用class DowelRod(HasMass, Cylindrical)。这里真正棘手的部分是我想要定义圆柱体积(体积=长度* pi *半径^ 2)并让该体积与质量平衡方程中定义的体积相互作用...这样,质量可能会响应长度等的变化不仅多重继承很棘手,而且自动组合关系会更糟。这将很快变得棘手。我还不知道如何处理方程组,并且很明显有很多参数关系,参数锁定或指定独立/因变量是必要的。

3 个答案:

答案 0 :(得分:1)

虽然我没有这种模型的经验,也没有SymPy的经验,但这里有一些提示:

@property
def mass(self):
    return self._params['m']

@mass.setter
def mass(self, value):
    param = 'm'
    self._params[param] = value
    self.balance(param)

@property
def volume(self):
    return self._params['v']

@volume.setter
def volume(self, value):
    param = 'v'
    self._params[param] = value
    self.balance(param)

正如您所注意到的,您为每个变量重复了很多代码。这是不必要的,因为你会有很多变量,这最终会导致代码维护的噩梦。你的变量整齐排列在varString = 'm v rho'我的建议是更进一步,并定义一个字典:

my_vars= {"m":"mass", "v":"volume", "rho":"density"}

然后add the properties and setters dynamically to the class(而非显式):

from functools import partial

def var_getter(varname, self):
    return self._params[varname]

def var_setter(varname, self, value):
    self._params[varname] = value
    self.balance(varname)

for k,v in my_vars.items():
    setattr(Stuff, k, property(fget=partial(var_getter, v), fset=partial(var_setter, v)))

这样,你只需要编写一次getter和setter。

如果你想拥有几个不同的吸气剂,你仍然可以使用这种技术。存储每个变量的getter或每个getter的变量 - 以更方便的为准。


一旦方程变得复杂,另一件可能有用的事情就是你可以将方程式保存为源中的字符串:

density_eq = sympy.sympify(   "rho - m / v"   )

使用这两个“技巧”,您甚至可能希望将变量和方程式保留在外部文本文件或CSV中。

答案 1 :(得分:0)

将问题视为解决约束问题,除了SymPy之外,您可能还需要查看python-constraint(http://labix.org/python-constraint)。

答案 2 :(得分:0)

刚才意识到python-constraint软件包并不适用于此,因为域需要是有限的。如果域名是有限的,这里举例说明:

import constraint as cst

p = cst.Problem()
p.addVariables(['rho','m', 'v'], range(100))
p.addConstraint(lambda rho,m,v: rho * v == m, ['rho', 'm', 'v'])

p.addConstraint(lambda rho: rho == 2, ['rho'])     # setting inputs; for illustration only
p.addConstraint(lambda m: m == 10, ['m'])        
print p.getSolutions()
  
    
      

[{' m':10,' rho':2,' v':5}]

    
  

但是,由于此处需要真实域名,因此该套餐不适用。