Python中找到四次多项式方程的最小正实根的最快方法

6
[我想要做的]是找到四次函数 ax^4 + bx^3 + cx^2 + dx + e 的唯一最小正实根。 [现有方法] 我的等式用于碰撞预测,最高次数为四次函数,如 f(x) = ax^4 + bx^3 + cx^2 + dx + e,而 a,b,c,d,e 系数可以是正数、负数或零(实数浮点值)。因此,我的函数 f(x) 可以是四次、三次或二次,具体取决于输入系数 a、b、c、d、e。
目前,我使用 NumPy 查找根,如下所示。
import numpy

root_output = numpy.roots([a, b, c ,d ,e])

NumPy模块中的“root_output”可以是根据输入系数可能的所有实/复根。因此,我必须逐个查看“root_output”,并检查哪个根是最小的正实数值(根>0?)。 [问题] 我的程序需要多次执行numpy.roots([a,b,c,d,e]),但多次执行numpy.roots对于我的项目来说太慢了。当执行numpy.roots时,(a,b,c ,d ,e)的值总是在每次更改。
我尝试在Raspberry Pi2上运行代码。以下是处理时间的示例:
  • 在PC上运行多次numpy.roots: 1.2秒
  • 在Raspberry Pi2上运行多次numpy.roots: 17秒
请指导我如何在最快的解决方案中找到最小的正实根?使用scipy.optimize或实现某种算法以加速查找根或您的任何建议都将很好。
谢谢。 [解决方案]
  • 二次函数只需要实际正根(请注意除零)
def SolvQuadratic(a, b ,c):
    d = (b**2) - (4*a*c)
    if d < 0:
        return []

    if d > 0:
        square_root_d = math.sqrt(d)
        t1 = (-b + square_root_d) / (2 * a)
        t2 = (-b - square_root_d) / (2 * a)
        if t1 > 0:
            if t2 > 0:
                if t1 < t2:
                    return [t1, t2]
                return [t2, t1]
            return [t1]
        elif t2 > 0:
            return [t2]
        else:
            return []
    else:
        t = -b / (2*a)
        if t > 0:
            return [t]
        return []
  • 四次函数 对于四次函数,您可以使用纯Python / Numba版本,如@B.M.的下面的答案所示。 我还添加了另一个来自@B.M代码的Cython版本。 您可以将以下代码用作.pyx文件,然后编译它以获得比纯Python快约2倍的速度(请注意舍入问题)。
import cmath

cdef extern from "complex.h":
    double complex cexp(double complex)

cdef double complex  J=cexp(2j*cmath.pi/3)
cdef double complex  Jc=1/J

cdef Cardano(double a, double b, double c, double d):
    cdef double z0
    cdef double a2, b2
    cdef double p ,q, D
    cdef double complex r
    cdef double complex u, v, w
    cdef double w0, w1, w2
    cdef double complex r1, r2, r3


    z0=b/3/a
    a2,b2 = a*a,b*b
    p=-b2/3/a2 +c/a
    q=(b/27*(2*b2/a2-9*c/a)+d)/a
    D=-4*p*p*p-27*q*q
    r=cmath.sqrt(-D/27+0j)
    u=((-q-r)/2)**0.33333333333333333333333
    v=((-q+r)/2)**0.33333333333333333333333
    w=u*v
    w0=abs(w+p/3)
    w1=abs(w*J+p/3)
    w2=abs(w*Jc+p/3)
    if w0<w1:
      if w2<w0 : v = v*Jc
    elif w2<w1 : v = v*Jc
    else: v = v*J
    r1 = u+v-z0
    r2 = u*J+v*Jc-z0
    r3 = u*Jc+v*J-z0
    return r1, r2, r3

cdef Roots_2(double a, double complex b, double complex c):
    cdef double complex bp
    cdef double complex delta
    cdef double complex r1, r2


    bp=b/2
    delta=bp*bp-a*c
    r1=(-bp-delta**.5)/a
    r2=-r1-b/a
    return r1, r2

def SolveQuartic(double a, double b, double c, double d, double e):
    "Ferrarai's Method"
    "resolution of P=ax^4+bx^3+cx^2+dx+e=0, coeffs reals"
    "First shift : x= z-b/4/a  =>  P=z^4+pz^2+qz+r"
    cdef double z0
    cdef double a2, b2, c2, d2
    cdef double p, q, r
    cdef double A, B, C, D
    cdef double complex y0, y1, y2
    cdef double complex a0, b0
    cdef double complex r0, r1, r2, r3


    z0=b/4.0/a
    a2,b2,c2,d2 = a*a,b*b,c*c,d*d
    p = -3.0*b2/(8*a2)+c/a
    q = b*b2/8.0/a/a2 - 1.0/2*b*c/a2 + d/a
    r = -3.0/256*b2*b2/a2/a2 + c*b2/a2/a/16 - b*d/a2/4+e/a
    "Second find y so P2=Ay^3+By^2+Cy+D=0"
    A=8.0
    B=-4*p
    C=-8*r
    D=4*r*p-q*q
    y0,y1,y2=Cardano(A,B,C,D)
    if abs(y1.imag)<abs(y0.imag): y0=y1
    if abs(y2.imag)<abs(y0.imag): y0=y2
    a0=(-p+2*y0)**.5
    if a0==0 : b0=y0**2-r
    else : b0=-q/2/a0
    r0,r1=Roots_2(1,a0,y0+b0)
    r2,r3=Roots_2(1,-a0,y0-b0)
    return (r0-z0,r1-z0,r2-z0,r3-z0)

[Ferrari方法的问题] 当四次方程的系数为[0.00614656,-0.0933333333333,0.527664995846,-1.31617928376,1.21906444869]时,使用numpy.roots和Ferrari方法得到的输出完全不同(numpy.roots是正确的输出)。

import numpy as np
import cmath


J=cmath.exp(2j*cmath.pi/3)
Jc=1/J

def ferrari(a,b,c,d,e):
    "Ferrarai's Method"
    "resolution of P=ax^4+bx^3+cx^2+dx+e=0, coeffs reals"
    "First shift : x= z-b/4/a  =>  P=z^4+pz^2+qz+r"
    z0=b/4/a
    a2,b2,c2,d2 = a*a,b*b,c*c,d*d
    p = -3*b2/(8*a2)+c/a
    q = b*b2/8/a/a2 - 1/2*b*c/a2 + d/a
    r = -3/256*b2*b2/a2/a2 +c*b2/a2/a/16-b*d/a2/4+e/a
    "Second find y so P2=Ay^3+By^2+Cy+D=0"
    A=8
    B=-4*p
    C=-8*r
    D=4*r*p-q*q
    y0,y1,y2=Cardano(A,B,C,D)
    if abs(y1.imag)<abs(y0.imag): y0=y1
    if abs(y2.imag)<abs(y0.imag): y0=y2
    a0=(-p+2*y0)**.5
    if a0==0 : b0=y0**2-r
    else : b0=-q/2/a0
    r0,r1=Roots_2(1,a0,y0+b0)
    r2,r3=Roots_2(1,-a0,y0-b0)
    return (r0-z0,r1-z0,r2-z0,r3-z0)

#~ @jit(nopython=True)
def Cardano(a,b,c,d):
    z0=b/3/a
    a2,b2 = a*a,b*b
    p=-b2/3/a2 +c/a
    q=(b/27*(2*b2/a2-9*c/a)+d)/a
    D=-4*p*p*p-27*q*q
    r=cmath.sqrt(-D/27+0j)
    u=((-q-r)/2)**0.33333333333333333333333
    v=((-q+r)/2)**0.33333333333333333333333
    w=u*v
    w0=abs(w+p/3)
    w1=abs(w*J+p/3)
    w2=abs(w*Jc+p/3)
    if w0<w1:
      if w2<w0 : v*=Jc
    elif w2<w1 : v*=Jc
    else: v*=J
    return u+v-z0, u*J+v*Jc-z0, u*Jc+v*J-z0

#~ @jit(nopython=True)
def Roots_2(a,b,c):
    bp=b/2
    delta=bp*bp-a*c
    r1=(-bp-delta**.5)/a
    r2=-r1-b/a
    return r1,r2

coef = [0.00614656, -0.0933333333333, 0.527664995846, -1.31617928376, 1.21906444869]
print("Coefficient A, B, C, D, E", coef) 
print("") 
print("numpy roots: ", np.roots(coef)) 
print("") 
print("ferrari python ", ferrari(*coef))

你能否澄清一下你的意思:“许多numpy.roots/一个循环对我的项目来说太慢了”?这不是很清楚。 - MaxU - stand with Ukraine
我实际上是指numpy.roots太慢了,我需要知道更快的找到根的方法。因为numpy.roots会找到所有可能的实数/复数根。但我只想知道唯一一个正实数根的输出。所以我认为可能有人知道比numpy.roots更快地找到它的方法。 - Utthawut
很遗憾,我无法为您提供关于scipy / numpy的帮助,但我可以建议您缓存结果 - 即确保您永远不会使用相同的(a,b,c,d,e)集合执行numpy.roots()。 不过,我不知道 - 也许您已经这样做了... - MaxU - stand with Ukraine
除此之外,您可能还想在这里提问:http://math.stackexchange.com/ - MaxU - stand with Ukraine
谢谢@MaxU。 (a,b,c,d,e)总是会改变。 它很少有可能是相同的值。 但我会将您的条件添加到我的主题中,让另一个人了解我的问题范围更多。我是这个社区的新手。 我该如何将此主题标记为[link]math.stackexchange.com,还是我应该创建新的主题到[link]math.stackexchange.com?谢谢 - Utthawut
我认为你应该在那里开一个新话题,但我不会在那里询问关于Python/Scipy/Numpy解决方案的问题。你可以在那里询问数学或算法解决方案... - MaxU - stand with Ukraine
4个回答

6
另一个答案:
使用分析方法(FerrariCardan)进行操作,并通过即时编译(Numba)加速代码:
首先看看改进情况:
In [2]: P=poly1d([1,2,3,4],True)

In [3]: roots(P)
Out[3]: array([ 4.,  3.,  2.,  1.])

In [4]: %timeit roots(P)
1000 loops, best of 3: 465 µs per loop

In [5]: ferrari(*P.coeffs)
Out[5]: ((1+0j), (2-0j), (3+0j), (4-0j))

In [5]: %timeit ferrari(*P.coeffs) #pure python without jit
10000 loops, best of 3: 116 µs per loop    
In [6]: %timeit ferrari(*P.coeffs)  # with numba.jit
100000 loops, best of 3: 13 µs per loop

然后是丑陋的代码:

对于4阶命令:

@jit(nopython=True)
def ferrari(a,b,c,d,e):
    "resolution of P=ax^4+bx^3+cx^2+dx+e=0"
    "CN all coeffs real."
    "First shift : x= z-b/4/a  =>  P=z^4+pz^2+qz+r"
    z0=b/4/a
    a2,b2,c2,d2 = a*a,b*b,c*c,d*d 
    p = -3*b2/(8*a2)+c/a
    q = b*b2/8/a/a2 - 1/2*b*c/a2 + d/a
    r = -3/256*b2*b2/a2/a2 +c*b2/a2/a/16-b*d/a2/4+e/a
    "Second find X so P2=AX^3+BX^2+C^X+D=0"
    A=8
    B=-4*p
    C=-8*r
    D=4*r*p-q*q
    y0,y1,y2=cardan(A,B,C,D)
    if abs(y1.imag)<abs(y0.imag): y0=y1 
    if abs(y2.imag)<abs(y0.imag): y0=y2 
    a0=(-p+2*y0.real)**.5
    if a0==0 : b0=y0**2-r
    else : b0=-q/2/a0
    r0,r1=roots2(1,a0,y0+b0)
    r2,r3=roots2(1,-a0,y0-b0)
    return (r0-z0,r1-z0,r2-z0,r3-z0) 

对于3阶订单:

J=exp(2j*pi/3)
Jc=1/J

@jit(nopython=True) 
def cardan(a,b,c,d):
    u=empty(2,complex128)
    z0=b/3/a
    a2,b2 = a*a,b*b    
    p=-b2/3/a2 +c/a
    q=(b/27*(2*b2/a2-9*c/a)+d)/a
    D=-4*p*p*p-27*q*q
    r=sqrt(-D/27+0j)        
    u=((-q-r)/2)**0.33333333333333333333333
    v=((-q+r)/2)**0.33333333333333333333333
    w=u*v
    w0=abs(w+p/3)
    w1=abs(w*J+p/3)
    w2=abs(w*Jc+p/3)
    if w0<w1: 
        if w2<w0 : v*=Jc
    elif w2<w1 : v*=Jc
    else: v*=J        
    return u+v-z0, u*J+v*Jc-z0,u*Jc+v*J-z0

对于第2个订单:

@jit(nopython=True)
def roots2(a,b,c):
    bp=b/2    
    delta=bp*bp-a*c
    u1=(-bp-delta**.5)/a
    u2=-u1-b/a
    return u1,u2  

可能需要进一步测试,但效率高。


我的四次方程式是通过两个球的非线性(加速)运动获得的,我将制作碰撞预测方程式。因此,它将生成ax^4 + bx^3 + cx^2 + dx + e的系数。在找到根的过程中,我必须决定哪个根是最小的正实根,这将是两个球碰撞可能发生的时间。 - Utthawut
亲爱的@B.M.,在测试您的Python实现法拉利时,我发现当系数为[0.00614656,-0.0933333333333,0.527664995846,-1.31617928376,1.21906444869]时存在一些错误。从numpy.roots输出的结果与法拉利方法完全不同,如下面代码后面的注释所示。 - Utthawut
导入numpy库 导入cmath库J = cmath.exp(2j*cmath.pi/3) Jc = 1/Jdef ferrari(a, b, c, d, e): "Ferrari方法" "解方程P=ax^4+bx^3+cx^2+dx+e=0,系数为实数" "首先进行平移:x= z-b/4/a => P=z^4+pz^2+qz+r" z0 = b/4/a a2, b2, c2, d2 = aa, bb, cc, dd p = -3b2/(8a2) + c/a q = bb2/8/a/a2 - 1/2bc/a2 + d/a r = -3/256b2b2/a2/a2 + cb2/a2/a/16 - bd/a2/4 + e/a "其次找到y,使得P2=Ay^3+By^2+Cy+D=0" A = 8 B = -4p C = -8r D = 4rp - qq y0, y1, y2 = Cardano(A, B, C, D) if abs(y1.imag) < abs(y0.imag): y0 = y1 if abs(y2.imag) < abs(y0.imag): y0 = y2 - Utthawut
请问您能帮我找一下问题吗? - Utthawut
1
嗨,我从我的电脑上更新了程序,现在结果是一样的。不知道错误出在哪里。 - B. M.
显示剩余11条评论

4
numpy解决方案可以无需循环实现:
p=array([a,b,c,d,e])
r=roots(p)
r[(r.imag==0) & (r.real>=0) ].real.min()

scipy.optimize方法可能会较慢,除非你不需要精度:

In [586]: %timeit r=roots(p);r[(r.imag==0) & (r.real>=0) ].real.min()
1000 loops, best of 3: 334 µs per loop

In [587]: %timeit newton(poly1d(p),10,tol=1e-8)
1000 loops, best of 3: 555 µs per loop

In [588]: %timeit newton(poly1d(p),10,tol=1)
10000 loops, best of 3: 177 µs per loop

你需要找到最小的x,满足x除以a等于向下取整(x/b),其中a和b是给定的整数.

编辑

对于2x因子,在计算时需自行处理根:

In [638]: b=zeros((4,4),float);b[1:,:-1]=eye(3)

In [639]: c=b.copy();c[0]=-(p/p[0])[1:];eig(c)[0]
Out[639]: 
array([-7.40849430+0.j        ,  5.77969794+0.j        ,
       -0.18560182+3.48995646j, -0.18560182-3.48995646j])

In [640]: roots(p)
Out[640]: 
array([-7.40849430+0.j        ,  5.77969794+0.j        ,
       -0.18560182+3.48995646j, -0.18560182-3.48995646j])

In [641]: %timeit roots(p)
1000 loops, best of 3: 365 µs per loop

In [642]: %timeit c=b.copy();c[0]=-(p/p[0])[1:];eig(c)
10000 loops, best of 3: 181 µs per loop

谢谢您。但是 CPU 密集型操作在 np.roots 函数内部。 - Utthawut
当运行您的2倍因子代码时,我也遇到了错误。from numpy import zeros, eye, copy, roots; from numpy.linalg import eig; example_equation = "3x^4 + 6x^3 - 123x^2 -126x +1080 = 0"; p = [3, 6, -123, -126, 1080]; roots(p); #array([-6., -4., 5., 3.]); b = zeros((4, 4), float); b[1:,:-1] = eye(3); c = b.copy(); c[0] = -(p/p[0])[1:]; **Traceback (most recent call last): File "", line 1, in c[0] = -(p/p[0])[1:] TypeError: unsupported operand type(s) for /: 'list' and 'int'** - Utthawut
抱歉:之前我为了方便使用NumPy而对p进行了p=array(p)操作。在我看来,这并没有对数值求根的性能产生太大的提升。希望重构初始问题。例如,可以使用一些二次方程更容易解决。将其公开(在另一篇文章中?)。 - B. M.
到目前为止,您的手动计算(伴随矩阵)可以将我的程序在四次函数中加速高达15%。 对于二次方程(ax ^ 2 + bx + c),您有什么加速的想法吗? 如果我使用伴随矩阵来加速二次函数,您能给我一个例子吗? 谢谢。 - Utthawut
我正在通过解析方法、费拉利和卡尔达诺公式(http://stackoverflow.com/questions/35820676/fail-to-implement-cardano-method-cube-root-of-a-complex-number)来寻找一个解决方案。我认为使用numba可以获得10倍或更多的优势。对于第二个问题,你可能在学校里学过这种方法 ;)。 - B. M.
显示剩余2条评论

3

如果多项式系数事先已知,您可以通过将计算向量化来加速在 roots 中的操作(需要Numpy >= 1.10):

import numpy as np

def roots_vec(p):
    p = np.atleast_1d(p)
    n = p.shape[-1]
    A = np.zeros(p.shape[:1] + (n-1, n-1), float)
    A[...,1:,:-1] = np.eye(n-2)
    A[...,0,:] = -p[...,1:]/p[...,None,0]
    return np.linalg.eigvals(A)

def roots_loop(p):
    r = []
    for pp in p:
        r.append(np.roots(pp))
    return r

p = np.random.rand(2000, 4)  # 2000 polynomials of 4th order

assert np.allclose(roots_vec(p), roots_loop(p))

In [35]: %timeit roots_vec(p)
100 loops, best of 3: 4.49 ms per loop

In [36]: %timeit roots_loop(p)
10 loops, best of 3: 81.9 ms per loop

它可能不如解析解+Numba,但可以允许更高阶的操作。


我的程序是基于多项式系数的(在找到当前根后,我将知道下一个系数),我有点担心。无论如何,谢谢你的回答。 - Utthawut

0
在SymPy中,real_roots函数将返回多项式的排序根,您可以循环遍历它们并在第一个正数上中断:
for do in range(100):
  p = Poly.from_list([randint(-100,100) for i in range(5)], x)
  for i in real_roots(p):
    if i.is_positive:
      print(i)
      break
  else:
   print('no positive root', p)

因此,针对此问题的函数可能是:

def small_pos_root(a,b,c,d,e):
    from sympy.abc import x
    from sympy import Poly, real_roots
    for i in real_roots(Poly.from_list([a,b,c,d,e], x)):
        if i.is_positive: return i.n()

如果没有正实数根,它将返回None,否则它将返回第一个正根的数值。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接