cpoly Subroutine

public subroutine cpoly(opr, opi, degree, zeror, zeroi, fail)

Finds the zeros of a complex polynomial.

Reference

  • Jenkins & Traub, "Algorithm 419: Zeros of a complex polynomial" Communications of the ACM, Volume 15, Issue 2, Feb. 1972, pp 97-99.
  • Added changes from remark on algorithm 419 by david h. withers cacm (march 1974) vol 17 no 3 p. 157]

Note

the program has been written to reduce the chance of overflow occurring. if it does occur, there is still a possibility that the zerofinder will work provided the overflowed quantity is replaced by a large number.

History

  • Jacob Williams, 9/18/2022 : modern Fortran version of this code.

Arguments

Type IntentOptional Attributes Name
real(kind=wp), intent(in), dimension(degree+1) :: opr

vectors of real parts of the coefficients in order of decreasing powers.

real(kind=wp), intent(in), dimension(degree+1) :: opi

vectors of imaginary parts of the coefficients in order of decreasing powers.

integer, intent(in) :: degree

degree of polynomial

real(kind=wp), intent(out), dimension(degree) :: zeror

real parts of the zeros

real(kind=wp), intent(out), dimension(degree) :: zeroi

imaginary parts of the zeros

logical, intent(out) :: fail

true only if leading coefficient is zero or if cpoly has found fewer than degree zeros.


Source Code

subroutine cpoly(opr,opi,degree,zeror,zeroi,fail)

    implicit none

    integer,intent(in) :: degree !! degree of polynomial
    real(wp), dimension(degree+1), intent(in) :: opr !! vectors of real parts of the coefficients in
                                                     !! order of decreasing powers.
    real(wp), dimension(degree+1), intent(in) :: opi !! vectors of imaginary parts of the coefficients in
                                                     !! order of decreasing powers.
    real(wp), dimension(degree), intent(out) :: zeror !! real parts of the zeros
    real(wp), dimension(degree), intent(out) :: zeroi !! imaginary parts of the zeros
    logical,intent(out) :: fail !! true only if leading coefficient is zero or if cpoly
                                !! has found fewer than `degree` zeros.

    real(wp) :: sr , si , tr , ti , pvr , pvi, xxx , zr , zi , bnd , xx , yy
    real(wp), dimension(degree+1) :: pr , pi , hr, hi , qpr, qpi, qhr , qhi , shr , shi
    logical :: conv
    integer :: cnt1 , cnt2, i , idnn2 , nn

    real(wp), parameter :: base = radix(1.0_wp)
    real(wp), parameter :: eta = eps
    real(wp), parameter :: infin = huge(1.0_wp)
    real(wp), parameter :: smalno = tiny(1.0_wp)
    real(wp), parameter :: are = eta
    real(wp), parameter :: cosr = cos(94.0_wp*deg2rad)  !! -.069756474
    real(wp), parameter :: sinr = sin(86.0_wp*deg2rad)  !! .99756405
    real(wp), parameter :: mre = 2.0_wp*sqrt(2.0_wp)*eta
    real(wp), parameter :: cos45 = cos(45.0_wp*deg2rad) !! .70710678

    if ( opr(1)==0.0_wp .and. opi(1)==0.0_wp ) then
        ! algorithm fails if the leading coefficient is zero.
        fail = .true.
        return
    end if

    xx = cos45
    yy = -xx
    fail = .false.
    nn = degree + 1

    ! remove the zeros at the origin if any.
    do
        if ( opr(nn)/=0.0_wp .or. opi(nn)/=0.0_wp ) then
            exit
        else
            idnn2 = degree - nn + 2
            zeror(idnn2) = 0.0_wp
            zeroi(idnn2) = 0.0_wp
            nn = nn - 1
        endif
    end do

    ! make a copy of the coefficients.
    do i = 1 , nn
        pr(i) = opr(i)
        pi(i) = opi(i)
        shr(i) = cmod(pr(i),pi(i))
    enddo
    ! scale the polynomial.
    bnd = scale(nn,shr,eta,infin,smalno,base)
    if ( bnd/=1.0_wp ) then
        do i = 1 , nn
            pr(i) = bnd*pr(i)
            pi(i) = bnd*pi(i)
        enddo
    endif

    ! start the algorithm for one zero.
    main : do
        if ( nn>2 ) then
            ! calculate bnd, a lower bound on the modulus of the zeros.
            do i = 1 , nn
                shr(i) = cmod(pr(i),pi(i))
            enddo
            bnd = cauchy(nn,shr,shi)
            ! outer loop to control 2 major passes with different sequences
            ! of shifts.
            do cnt1 = 1 , 2
                ! first stage calculation, no shift.
                call noshft(5)
                ! inner loop to select a shift.
                do cnt2 = 1 , 9
                    ! shift is chosen with modulus bnd and amplitude rotated by
                    ! 94 degrees from the previous shift
                    xxx = cosr*xx - sinr*yy
                    yy = sinr*xx + cosr*yy
                    xx = xxx
                    sr = bnd*xx
                    si = bnd*yy
                    ! second stage calculation, fixed shift.
                    call fxshft(10*cnt2,zr,zi,conv)
                    if ( conv ) then
                        ! the second stage jumps directly to the third stage iteration.
                        ! if successful the zero is stored and the polynomial deflated.
                        idnn2 = degree - nn + 2
                        zeror(idnn2) = zr
                        zeroi(idnn2) = zi
                        nn = nn - 1
                        do i = 1 , nn
                            pr(i) = qpr(i)
                            pi(i) = qpi(i)
                        enddo
                        cycle main
                    endif
                    ! if the iteration is unsuccessful another shift is chosen.
                enddo
                ! if 9 shifts fail, the outer loop is repeated with another
                ! sequence of shifts.
            enddo
            ! the zerofinder has failed on two major passes.
            ! return empty handed.
            fail = .true.
            return
        else
            exit
        endif

    end do main

    ! calculate the final zero and return.
    call cdivid(-pr(2),-pi(2),pr(1),pi(1),zeror(degree),zeroi(degree))

contains

subroutine noshft(l1)

    ! computes the derivative polynomial as the initial h
    ! polynomial and computes l1 no-shift h polynomials.

    implicit none

    integer,intent(in) :: l1

    integer :: i , j , jj , n , nm1
    real(wp) :: xni , t1 , t2

    n = nn - 1
    nm1 = n - 1
    do i = 1 , n
       xni = nn - i
       hr(i) = xni*pr(i)/real(n,wp)
       hi(i) = xni*pi(i)/real(n,wp)
    enddo
    do jj = 1 , l1
       if ( cmod(hr(n),hi(n))<=eta*10.0_wp*cmod(pr(n),pi(n)) ) then
          ! if the constant term is essentially zero, shift h coefficients.
          do i = 1 , nm1
             j = nn - i
             hr(j) = hr(j-1)
             hi(j) = hi(j-1)
          enddo
          hr(1) = 0.0_wp
          hi(1) = 0.0_wp
       else
          call cdivid(-pr(nn),-pi(nn),hr(n),hi(n),tr,ti)
          do i = 1 , nm1
             j = nn - i
             t1 = hr(j-1)
             t2 = hi(j-1)
             hr(j) = tr*t1 - ti*t2 + pr(j)
             hi(j) = tr*t2 + ti*t1 + pi(j)
          enddo
          hr(1) = pr(1)
          hi(1) = pi(1)
       endif
    enddo
end subroutine noshft

subroutine fxshft(l2,zr,zi,conv)

    ! computes l2 fixed-shift h polynomials and tests for
    ! convergence.
    ! initiates a variable-shift iteration and returns with the
    ! approximate zero if successful.

    implicit none

    integer,intent(in) :: l2 !! limit of fixed shift steps
    real(wp) :: zr , zi !! approximate zero if conv is .true.
    logical :: conv !! logical indicating convergence of stage 3 iteration

    integer :: i , j , n
    real(wp) :: otr , oti , svsr , svsi
    logical :: test , pasd , bool

    n = nn - 1
    ! evaluate p at s.
    call polyev(nn,sr,si,pr,pi,qpr,qpi,pvr,pvi)
    test = .true.
    pasd = .false.
    ! calculate first t = -p(s)/h(s).
    call calct(bool)
    ! main loop for one second stage step.
    do j = 1 , l2
       otr = tr
       oti = ti
        ! compute next h polynomial and new t.
       call nexth(bool)
       call calct(bool)
       zr = sr + tr
       zi = si + ti
        ! test for convergence unless stage 3 has failed once or this
        ! is the last h polynomial .
       if ( .not.(bool .or. .not.test .or. j==l2) ) then
          if ( cmod(tr-otr,ti-oti)>=0.5_wp*cmod(zr,zi) ) then
             pasd = .false.
          elseif ( .not.pasd ) then
             pasd = .true.
          else
            ! the weak convergence test has been passed twice, start the
            ! third stage iteration, after saving the current h polynomial
            ! and shift.
             do i = 1 , n
                shr(i) = hr(i)
                shi(i) = hi(i)
             enddo
             svsr = sr
             svsi = si
             call vrshft(10,zr,zi,conv)
             if ( conv ) return
            ! the iteration failed to converge. turn off testing and restore
            ! h,s,pv and t.
             test = .false.
             do i = 1 , n
                hr(i) = shr(i)
                hi(i) = shi(i)
             enddo
             sr = svsr
             si = svsi
             call polyev(nn,sr,si,pr,pi,qpr,qpi,pvr,pvi)
             call calct(bool)
          endif
       endif
    enddo
    ! attempt an iteration with final h polynomial from second stage.
    call vrshft(10,zr,zi,conv)

end subroutine fxshft

subroutine vrshft(l3,zr,zi,conv)

    ! carries out the third stage iteration.

    implicit none

    integer,intent(in) :: l3 !! limit of steps in stage 3.
    real(wp) :: zr , zi !! on entry contains the initial iterate, if the
                        !! iteration converges it contains the final iterate
                        !! on exit.
    logical :: conv !! .true. if iteration converges

    real(wp) :: mp , ms , omp , relstp , r1 , r2 , tp
    logical :: b , bool
    integer :: i , j

    conv = .false.
    b = .false.
    sr = zr
    si = zi

    ! main loop for stage three
    do i = 1 , l3
        ! evaluate p at s and test for convergence.
       call polyev(nn,sr,si,pr,pi,qpr,qpi,pvr,pvi)
       mp = cmod(pvr,pvi)
       ms = cmod(sr,si)
       if ( mp>20.0_wp*errev(nn,qpr,qpi,ms,mp,are,mre) ) then
          if ( i==1 ) then
             omp = mp
          elseif ( b .or. mp<omp .or. relstp>=0.05_wp ) then
            ! exit if polynomial value increases significantly.
             if ( mp*0.1_wp>omp ) return
             omp = mp
          else
            ! iteration has stalled. probably a cluster of zeros. do 5 fixed
            ! shift steps into the cluster to force one zero to dominate.
             tp = relstp
             b = .true.
             if ( relstp<eta ) tp = eta
             r1 = sqrt(tp)
             r2 = sr*(1.0_wp+r1) - si*r1
             si = sr*r1 + si*(1.0_wp+r1)
             sr = r2
             call polyev(nn,sr,si,pr,pi,qpr,qpi,pvr,pvi)
             do j = 1 , 5
                call calct(bool)
                call nexth(bool)
             enddo
             omp = infin
          endif
            ! calculate next iterate.
          call calct(bool)
          call nexth(bool)
          call calct(bool)
          if ( .not.(bool) ) then
             relstp = cmod(tr,ti)/cmod(sr,si)
             sr = sr + tr
             si = si + ti
          endif
       else
          ! polynomial value is smaller in value than a bound on the error
          ! in evaluating p, terminate the iteration.
          conv = .true.
          zr = sr
          zi = si
          return
       endif
    enddo

end subroutine vrshft

subroutine calct(bool)

    ! computes `t = -p(s)/h(s)`.

    implicit none

    logical,intent(out) :: bool !! logical, set true if `h(s)` is essentially zero.

    real(wp) :: hvr , hvi
    integer :: n

    n = nn - 1
    ! evaluate h(s).
    call polyev(n,sr,si,hr,hi,qhr,qhi,hvr,hvi)
    bool = cmod(hvr,hvi)<=are*10.0_wp*cmod(hr(n),hi(n))
    if ( bool ) then
       tr = 0.0_wp
       ti = 0.0_wp
    else
        call cdivid(-pvr,-pvi,hvr,hvi,tr,ti)
    end if

end subroutine calct

subroutine nexth(bool)

    ! calculates the next shifted h polynomial.

    implicit none

    logical,intent(in) :: bool !! logical, if .true. `h(s)` is essentially zero

    real(wp) :: t1 , t2
    integer :: j , n , nm1

    n = nn - 1
    nm1 = n - 1
    if ( bool ) then
       ! if h(s) is zero replace h with qh.
       do j = 2 , n
          hr(j) = qhr(j-1)
          hi(j) = qhi(j-1)
       enddo
       hr(1) = 0.0_wp
       hi(1) = 0.0_wp
    else
        do j = 2 , n
            t1 = qhr(j-1)
            t2 = qhi(j-1)
            hr(j) = tr*t1 - ti*t2 + qpr(j)
            hi(j) = tr*t2 + ti*t1 + qpi(j)
        enddo
        hr(1) = qpr(1)
        hi(1) = qpi(1)
    end if

end subroutine nexth

subroutine polyev(nn,sr,si,pr,pi,qr,qi,pvr,pvi)

    ! evaluates a polynomial  p  at  s  by the horner recurrence
    ! placing the partial sums in q and the computed value in pv.

    implicit none

    integer,intent(in) :: nn
    real(wp) :: pr(nn) , pi(nn) , qr(nn) , qi(nn) , sr , si , pvr , pvi

    real(wp) :: t
    integer :: i

    qr(1) = pr(1)
    qi(1) = pi(1)
    pvr = qr(1)
    pvi = qi(1)
    do i = 2 , nn
       t = pvr*sr - pvi*si + pr(i)
       pvi = pvr*si + pvi*sr + pi(i)
       pvr = t
       qr(i) = pvr
       qi(i) = pvi
    enddo

end subroutine polyev

real(wp) function errev(nn,qr,qi,ms,mp,are,mre)

    ! bounds the error in evaluating the polynomial
    ! by the horner recurrence.

    implicit none

    integer,intent(in) :: nn
    real(wp) :: qr(nn), qi(nn) !! the partial sums
    real(wp) :: ms !! modulus of the point
    real(wp) :: mp !! modulus of polynomial value
    real(wp) :: are, mre !! error bounds on complex addition and multiplication

    integer :: i
    real(wp) :: e

    e = cmod(qr(1),qi(1))*mre/(are+mre)
    do i = 1 , nn
       e = e*ms + cmod(qr(i),qi(i))
    enddo
    errev = e*(are+mre) - mp*mre

end function errev

real(wp) function cauchy(nn,pt,q)

    ! cauchy computes a lower bound on the moduli of
    ! the zeros of a polynomial

    implicit none

    integer,intent(in) :: nn
    real(wp) :: q(nn)
    real(wp) :: pt(nn) !! the modulus of the coefficients.

    integer :: i , n
    real(wp) :: x , xm , f , dx , df

    pt(nn) = -pt(nn)
    ! compute upper estimate of bound.
    n = nn - 1
    x = exp((log(-pt(nn))-log(pt(1)))/real(n,wp))
    if ( pt(n)/=0.0_wp ) then
       ! if newton step at the origin is better, use it.
       xm = -pt(nn)/pt(n)
       if ( xm<x ) x = xm
    endif

    do
        ! chop the interval (0,x) unitl f <= 0.
        xm = x*0.1_wp
        f = pt(1)
        do i = 2 , nn
           f = f*xm + pt(i)
        enddo
        if ( f<=0.0_wp ) then
           dx = x
           do
               ! newton iteration until x converges to two decimal places.
               if ( abs(dx/x)<=0.005_wp ) then
                  cauchy = x
                  exit
               end if
               q(1) = pt(1)
               do i = 2 , nn
                  q(i) = q(i-1)*x + pt(i)
               enddo
               f = q(nn)
               df = q(1)
               do i = 2 , n
                  df = df*x + q(i)
               enddo
               dx = f/df
               x = x - dx
           end do
           exit
        else
           x = xm
        endif
    end do

end function cauchy

real(wp) function scale(nn,pt,eta,infin,smalno,base)

    ! returns a scale factor to multiply the coefficients of the
    ! polynomial. the scaling is done to avoid overflow and to avoid
    ! undetected underflow interfering with the convergence
    ! criterion.  the factor is a power of the base.

    implicit none

    integer :: nn
    real(wp) :: pt(nn)  !! modulus of coefficients of p
    real(wp) :: eta , infin , smalno , base !! constants describing the
                                            !! floating point arithmetic.

    real(wp) :: hi , lo , max , min , x , sc
    integer :: i , l

    ! find largest and smallest moduli of coefficients.
    hi = sqrt(infin)
    lo = smalno/eta
    max = 0.0_wp
    min = infin
    do i = 1 , nn
       x = pt(i)
       if ( x>max ) max = x
       if ( x/=0.0_wp .and. x<min ) min = x
    enddo
    ! scale only if there are very large or very small components.
    scale = 1.0_wp
    if ( min>=lo .and. max<=hi ) return
    x = lo/min
    if ( x>1.0_wp ) then
       sc = x
       if ( infin/sc>max ) sc = 1.0_wp
    else
       sc = 1.0_wp/(sqrt(max)*sqrt(min))
    endif
    l = log(sc)/log(base) + 0.5_wp
    scale = base**l

end function scale

subroutine cdivid(ar,ai,br,bi,cr,ci)

    ! complex division c = a/b, avoiding overflow.

    implicit none

    real(wp) :: ar , ai , br , bi , cr , ci , r , d

    if ( br==0.0_wp .and. bi==0.0_wp ) then
       ! division by zero, c = infinity.
       cr = infin
       ci = infin
    elseif ( abs(br)>=abs(bi) ) then
       r = bi/br
       d = br + r*bi
       cr = (ar+ai*r)/d
       ci = (ai-ar*r)/d
    else
        r = br/bi
        d = bi + r*br
        cr = (ar*r+ai)/d
        ci = (ai*r-ar)/d
    end if

end subroutine cdivid

real(wp) function cmod(r,i)

    implicit none

    real(wp) :: r , i , ar , ai

    ar = abs(r)
    ai = abs(i)
    if ( ar<ai ) then
       cmod = ai*sqrt(1.0_wp+(ar/ai)**2)
    elseif ( ar<=ai ) then
       cmod = ar*sqrt(2.0_wp)
    else
       cmod = ar*sqrt(1.0_wp+(ai/ar)**2)
    end if

end function cmod

end subroutine cpoly