有没有一种方法可以找到比使用Sympy的nroot()更快的四阶多项式的根?

时间:2019-12-11 04:55:56

标签: python numpy sympy numerical-methods

我正在尝试求解具有复杂系数的四阶多项式,即

-0.678916793992528*w^4 + 9207096.65180878*i*w^3 
+ 1.47445911372677e+15*w^2 - 1.54212540689566e+21*i*w 
+ 2.70530138119032e+26

此代码的最终目标是至少使用不同的系数来求解该多项式至少100,000次,因此我希望该代码快速高效。我一直在使用sympy.nroots()获取根,但根据%timeit,每个循环大约需要9.6毫秒,与numpy.roots()每个循环需要60 µs相比,这是非常缓慢的。但是,我不能使用numpy.roots(),因为它不能很好地处理复数系数,并且一直错误地解决了该多项式的根。每个循环使用sympy.solve()的速度甚至更慢,仅为122 ms。

我想尝试加快这一过程的一件事是,我真的只需要根的虚部,特别是最负的虚部,但是我不确定是否可以将其用于此代码的运行时间更快。

我的问题是,还有其他功能可以用于查找根的速度更快吗?还是有其他我可以编写的寻根方法会更快?最后,有一种方法只能解决复杂的根,这会更快吗?

1 个答案:

答案 0 :(得分:3)

在双精度浮点数上,您无法获得比np.root更好的结果。评估接近于根的多项式涉及许多灾难性的抵消。

使用numpy例程对示例进行示例,得出的根为

def print_Carr(z):
    for zz in z: print(">>> % 22.17e %+.17ej"%(zz.real, zz.imag))

p=np.array([-0.678916793992528, 9207096.65180878j, 1.47445911372677e+15, -1.54212540689566e+21j, 2.70530138119032e+26])
z=np.roots(p); print_Carr(z)
>>>  4.60399640251209885e+07 +6.25409784852022864e+06j
>>> -4.60399640251209214e+07 +6.25409784852025378e+06j
>>>  6.97016694994478896e-13 +1.20627308238215139e+06j
>>>  5.23825344503222243e-11 -1.53018048966713541e+05j

对于多项式评估,这些值相当大。这些根的评估值是

print_Carr(np.polyval(p,z))
>>> -3.48222204464332800e+15 +2.82412997568102400e+15j
>>>  5.73769835033395200e+15 -1.64254152287846400e+15j
>>> -4.12316860416000000e+11 +1.37984933104284096e+09j
>>>  6.87194767360000000e+10 -1.04451799855962357e+11j

这对于残差而言似乎很糟糕,但是根的尾数的最后一位的变化会导致值的绝对变化很大。请记住,确切的根(对于给定的系数)在浮点数之间。这些变化对多项式值的影响可以通过用绝对值替换系数和根来估算,因为mu*|p|(|z|)是浮点计算误差的估算。

print_Carr(np.polyval(abs(p),abs(z)) *2**-52)
>>>  1.63036010254646300e+15 +0.00000000000000000e+00j
>>>  1.63036010254645625e+15 +0.00000000000000000e+00j
>>>  9.53421868314746094e+11 +0.00000000000000000e+00j
>>>  1.20139515277909210e+11 +0.00000000000000000e+00j

残差几乎在这些范围之内。

更改根近似值或多项式系数的最后一个尾数位可以产生影响,可以通过根位置的导数来估算

print_Carr(abs(np.polyval(np.polyder(p),z))*(2**-52*abs(z)))
>>>  1.38853576300226150e+15 +0.00000000000000000e+00j
>>>  1.38853576300225050e+15 +0.00000000000000000e+00j
>>>  5.30242273857438416e+11 +0.00000000000000000e+00j
>>>  6.77504690635207825e+10 +0.00000000000000000e+00j

再次证明,任何超过最后两个尾数位的变化都会大大增加残差。

要在实施np.roots时消除“伴随矩阵的特征值”的不精确性,请通过牛顿法的一个步骤应用“根抛光”并重新计算残差,

z = z - np.polyval(p,z)/np.polyval(np.polyder(p),z); print_Carr(z)
>>>  4.60399640251209661e+07 +6.25409784852025565e+06j
>>> -4.60399640251209661e+07 +6.25409784852025472e+06j
>>>  1.00974195868289511e-28 +1.20627308238215116e+06j
>>>  0.00000000000000000e+00 -1.53018048966713570e+05j
print_Carr(np.polyval(p,z))
>>>  6.74825261547520000e+13 -7.41139556597760000e+13j
>>>  1.55993212190720000e+13 -1.15513145425920000e+14j
>>>  2.74877906944000000e+11 +1.99893600285358499e-07j
>>>  0.00000000000000000e+00 +0.00000000000000000e+00j

实际上,残差减少了一个或两个小数位,这表明使用此浮点数据类型几乎可以达到最好的效果。

因此,针对您任务的新建议是使用numpy.roots和一个牛顿步骤进行根抛光。


最后与另一个多精度结果进行比较

from mpmath import mp
mp.dps = 20; mp.pretty = True;
mp.polyroots(p, maxsteps=20, extraprec=30) # prec=bits, dps=digits, 10bits=3digits
>>> [(0.0 - 153018.04896671356797j),
>>>  (0.0 + 1206273.0823821511478j),
>>>  (-46039964.025120967306 + 6254097.8485202553318j),
>>>  ( 46039964.025120967306 + 6254097.8485202553318j)]

当对实部和虚部进行相同位置计数时,roots + Newton结果在15个前导数字中是正确的。