如何序列化一个包含菲涅耳积分的mpmath修饰的SymPy函数?



我正在使用SymPy生成一些需要评估菲涅耳积分的函数,并希望将它们保存到磁盘上。当我使用默认的["scipy", "numpy"]模块sm.lambdify表达式时,序列化它们与dill.dump()dill.load()一起工作。然而,它们包含输入的平方根,它可以是负的(尽管输出总是实数),并且默认模块无法处理复数。

当我在sm.lambdify中使用"mpmath"模块时,可以为正输入和负输入评估结果函数。但是,我在用dillcloudpickle序列化这些版本的函数时遇到了麻烦。

下面是我尝试过的简单函数的例子,以及我实际使用的类似的东西。

  1. 简单函数(无菲涅耳积分):
import sympy as sm
import dill
import cloudpickle
a,b = sm.symbols('a b')
f = sm.integrate(a*b**2, (b,0,1))
F = sm.lambdify((a,b), f, "mpmath")
# Saving with dill fails with below error:
F_dump = dill.dumps(F)
# PicklingError: Can't pickle : it's not the same object as mpmath.ctx_iv.ivmpf
# Setting recurse=True results in successful save and load:
F_dump = dill.dumps(F, recurse=True)
F_load = dill.loads(F_dump)
# Cloudpickle also successfully saves and loads by default:
F_dump = cloudpickle.dumps(F)
F_load = cloudpickle.loads(F_dump)
  1. 菲涅耳积分函数:
import sympy as sm
import dill
import cloudpickle
a,b = sm.symbols('a b')
f_fresnel = sm.integrate(sm.cos(a*b**2), (b,0,1))
F_fresnel = sm.lambdify((a,b), f_fresnel, "mpmath")
# Fails with same error as before:
F_fresnel_dump = dill.dumps(F_fresnel)
# PicklingError: Can't pickle : it's not the same object as mpmath.ctx_iv.ivmpf
# With recurse=True, fails after a while with below error:
F_fresnel_dump = dill.dumps(F_fresnel, recurse=True)
# RecursionError: maximum recursion depth exceeded in comparison
# Cloudpickle appears to save successfully:
F_dump = cloudpickle.dumps(F_fresnel)
# But fails with below error when trying to load:
F_load = cloudpickle.loads(F_dump)
# TypeError: mpq.__new__() missing 1 required positional argument: 'p'

我不知道我正在使用的特定类型的函数与问题有多大关系,但考虑到上面两个例子,它似乎至少有一些关系。有人能告诉我如何才能使这项工作,我不确定我是否应该看lambdify阶段,序列化模块,或其他东西完全?

从注释来看,您似乎有一个可能的解决方案,即pickle表达式而不是函数。我只是想说清楚sympy的lambdify函数实际上只是创建了一个动态生成的Python函数,你可以获得该函数的代码:

In [2]: import sympy as sm
...: 
...: a,b = sm.symbols('a b')
...: f_fresnel = sm.integrate(sm.cos(a*b**2), (b,0,1))
...: F_fresnel = sm.lambdify((a,b), f_fresnel, "mpmath")
In [3]: import inspect
In [4]: print(inspect.getsource(F_fresnel))
def _lambdifygenerated(a, b):
return (mpf(1)/mpf(8))*sqrt(2)*sqrt(pi)*fresnelc(sqrt(2)*sqrt(a)/sqrt(pi))*gamma(mpf(1)/mpf(4))/(sqrt(a)*gamma(mpf(5)/mpf(4)))

如果你想在以后的脚本中重用它,你可以直接将这个函数粘贴到你的代码中。它需要像from mpmath import sqrt, gamma, mpf, ...这样的导入。这样以后的脚本不需要导入sympy,不需要使用pickle等。这种方法将更快、更健壮。

这种方法的缺点是,如果你动态地生成这些函数中的许多,那么复制代码可能容易出错,但你也可以自动化:

In [24]: code = inspect.getsource(sm.lambdify((a, b), f_fresnel, use_imps=True))
In [25]: code = code.replace('_lambdifygenerated', 'F_fresnel')
In [26]: with open('generated.py', 'a') as fout: # using append mode
...:     fout.write('n')
...:     fout.write(code)
...: 
In [27]: cat generated.py
def F_fresnel(a, b):
return (mpf(1)/mpf(8))*sqrt(2)*sqrt(pi)*fresnelc(sqrt(2)*sqrt(a)/sqrt(pi))*gamma(mpf(1)/mpf(4))/(sqrt(a)*gamma(mpf(5)/mpf(4)))

显然,在磁盘上重写代码是有风险的,所以如果遵循这种方法,请确保您非常小心。另一个缺点是,如果您试图pickle一个复杂的数据结构,其中包含F_fresnel函数作为该数据结构的一部分,则此方法将不起作用。

至于为什么酸洗失败,问题是为什么酸洗函数失败。调用lambdify之后,除了它是动态生成的函数之外,您得到的是一个普通函数。我认为在这种情况下,pickle将会"腌制"。通过嵌入代码来导入函数。例如:

In [28]: import pickle
In [29]: pickle.dumps(sm.simplify)
Out[29]: b'x80x04x95(x00x00x00x00x00x00x00x8cx17sympy.simplify.simplifyx94x8cx08simplifyx94x93x94.'

我不知道怎么"阅读"。但我可以很清楚地看到这基本上只是存储了Python代码

的等效物
from sympy.simplify.simplify import simplify

如果您pickle包含在确定安装的模块中的函数,则可以工作,因为这意味着pickle.loads可以导入模块并从那里获取函数。不幸的是,这种方法不适用于像F_fresnel这样动态生成的函数,因为它们不是在可以导入它们的模块中定义的(除非像我上面展示的那样复制代码)。我不知道dillcloudpickle如何处理这个与pickle相比,但我很清楚为什么pickle不能处理这个:

In [30]: pickle.dumps(F_fresnel)
---------------------------------------------------------------------------
PicklingError                             Traceback (most recent call last)
Cell In[30], line 1
----> 1 pickle.dumps(F_fresnel)
PicklingError: Can't pickle <function _lambdifygenerated at 0x7f30a5db0f40>: attribute lookup _lambdifygenerated on __main__ failed
In [31]: _lambdifygenerated = F_fresnel
In [32]: pickle.dumps(F_fresnel)
Out[32]: b'x80x04x95#x00x00x00x00x00x00x00x8cx08__main__x94x8cx12_lambdifygeneratedx94x93x94.'

所以pickle想要存储代码from __main__ import _lambdifygenerated,因为_lambdifygeneratedlambdify创建的代码中函数的名称,而不管您给它分配了什么名称,例如F_fresnel。这失败了,因为函数是在__main__模块中定义的,即主脚本,那里没有称为_lambdifygenerated的对象,除非我们将其分配给该名称。但在实践中,它会以许多其他方式失败,因为如果您试图在不同的脚本(不同的__main__)中加载该pickle,那么它将没有要导入的对象。

另一种解决方案是将lambdify的参数封装在可pickle的东西中,然后pickle:

class PickleLambdify:
def __init__(self, args, expression, **kwargs):
self.args = args
self.expression = expression
self.kwargs = kwargs
def regenerate(self):
return lambdify(self.args, self.expression, **self.kwargs)

那么你可以这样做:

In [35]: F_fresnel_pickle = PickleLambdify((a,b), f_fresnel, modules="mpmath")
In [36]: pickle.dumps(F_fresnel_pickle)
Out[36]: b'x80x04x95x0ex02x00x00x00x00x00x00x8cx08__main__x94x8cx0ePickleLambdifyx94x93x94)x81x94}x94(x8cx04argsx94x8cx11sympy.core.symbolx94x8cx06Symbolx94x93x94x8cx01ax94x85x94x81x94hx08x8cx01bx94x85x94x81x94x86x94x8cnexpressionx94x8cx0esympy.core.mulx94x8cx03Mulx94x93x94(x8cx12sympy.core.numbersx94x8cx08Rationalx94x93x94Kx01Kx08x86x94x81x94x8cx10sympy.core.powerx94x8cx03Powx94x93x94hx14x8cx07Integerx94x93x94Kx02x85x94x81x94hx14x8cx04Halfx94x93x94)x81x94x86x94x81x94hx1bhx14x8cx02Pix94x93x94)x81x94h"x86x94x81x94hx1bhx0bhx16JxffxffxffxffKx02x86x94x81x94x86x94x81x94hx1bx8c'sympy.functions.special.gamma_functionsx94x8cx05gammax94x93x94hx16Kx05Kx04x86x94x81x94x85x94x81x94hx14x8cx0bNegativeOnex94x93x94)x81x94x86x94x81x94x8c'sympy.functions.special.error_functionsx94x8cx08fresnelcx94x93x94hx13h$hx1bh'hx16JxffxffxffxffKx02x86x94x81x94x86x94x81x94hx1bhx0bh"x86x94x81x94x87x94x81x94x85x94x81x94h0hx16Kx01Kx04x86x94x81x94x85x94x81x94tx94x81x94x8cx06kwargsx94}x94x8cx07modulesx94x8cx06mpmathx94sub.'
In [37]: pickle.loads(pickle.dumps(F_fresnel_pickle))
Out[37]: <__main__.PickleLambdify at 0x7f30a5dbc290>
In [39]: pickle.loads(pickle.dumps(F_fresnel_pickle)).regenerate()
Out[39]: <function _lambdifygenerated(a, b)>

通过这种方式,你可以透明地使用pickle与你的函数作为一个更大的数据结构的一部分,但显然在运行时取消pickle需要导入sympy,创建符号表达式,然后再次调用lambdify,这基本上是相同的,如果你只是pickle表达式在第一个地方发生的事情。

相关内容

  • 没有找到相关文章

最新更新