Version 2 of Polynomial fitting

Updated 2004-04-05 06:47:41

Arjen Markus (3 april 2004) Knowing orthogonal polynomials like the Legendre family and others, and having played a bit with discrete Fourier transforms (see Discrete Fourier Transform) I thought I would try an analoguous technique polynomials.

There is one practical difference with discrete Fourier transforms: it is difficult (but not impossible) to write down closed formulae for the polynomials to be used. I started with:

  • The nodes or auxiliary points are x = k/N (where k = -m, -m+1, ..., m-1, m, N=2m+1,

and similar - but different for even N)

  • The poynomials must be orthogonal in the sense that the sum
       m
      Sum   Pi(x)*Pj(x) = 0,   if i != j
      k=-m
  • The first two are simple:
    P0(x) = 1
    P1(x) = x
  • The third becomes troublesome already because you have to distinguish between even and odd N.

So, I decided to give up on this analytic approach and instead use a practical (numerical) one:

  • I try to fit N data points
  • The nodes for the polynomials are x = k, k=1,N
  • The polynomials that I want to fit with are determined "on the fly", using a simple Gram-Schmidt orthogonalisation process and subsequent normalisation.
  • Due to the orthogonality the fitting of the data becomes almost trivial, just as with the discrete Fourier transform.

(Improvements:

  • Reduce the overhead of double computations - certain intermediate results are computed more than once
  • The toString procedure does not take care of minus signs - I overlooked that possibility and was too lazy to repair that. Probably the best solution is to modify the string afterwards.)

 # discrpol.tcl --
 #    A discrete polynomial transform:
 #    Just like with a discrete Fourier transform a series of data
 #    is fitted with orthogonal polynomials, the orthogonality being
 #    defined via sums over discrete (equidistant) points.
 #    This leads to sums of the type:
 #         N
 #        Sum  k**p
 #        k=1
 #    which can be expressed in a closed form for any integer p,
 #    but the formulae get complicated and there is little system
 #    in them (well, there is a very complicated system in them).
 #
 #    So, we compute the orthogonal polynomials "on the fly"
 #
 #    Note:
 #    The polynomials are represented as a tagged list of
 #    coefficients. The independent coordinates are taken to be
 #    from 1 to N (the number of data)

 namespace eval ::dpt {
    # Make it exist
 }

 # EvalPolynomial --
 #    Evalute a polynomial at a given ordinate
 #
 # Arguments:
 #    coeffs      The coefficients of the polynomial
 #    x           The ordinate
 # Result:
 #    Value of the polynomial at x
 #
 proc ::dpt::EvalPolynomial { coeffs x } {
    set result 0.0
    set px     1.0
    foreach c $coeffs {
       set result [expr {$result+$c*$px}]
       set px     [expr {$px*$x}]
    }
    return $result
 }

 # auxiliaryPoints --
 #    Return a list of auxiliary points (for evaluting the polynomials)
 #
 # Arguments:
 #    nodata      Number of data
 # Result:
 #    List of ordinates to evaluate the polynomials at
 # Note:
 #    This procedure defines in fact the details of the polynomial
 #    fitting.
 #
 proc ::dpt::auxiliaryPoints { nodata } {
    set xlist  {}
    for { set x 1 } { $x <= $nodata } { incr x } {
       lappend xlist $x
    }
    return $xlist
 }

 # valuesPolynomial --
 #    Evalute a polynomial for a list of ordinates
 #
 # Arguments:
 #    polyn       The polynomial
 #    xlist       List of ordinates
 # Result:
 #    Values of the polynomial at each ordinate as a list
 #
 proc ::dpt::valuesPolynomial { polyn xlist } {
    set result {}
    set coeffs [lrange $polyn 2 end]
    foreach x $xlist {
       lappend result [EvalPolynomial $coeffs $x]
    }
    return $result
 }

 # discreteNorm --
 #    Determine the (discrete) norm of a given polynomial
 #
 # Arguments:
 #    poly      The coefficients of the polynomial
 # Result:
 #    Norm of the polynomial (sqrt(sum(Poly(x)**2))
 #
 proc ::dpt::discreteNorm { poly } {
    set nodata [lindex $poly 1]
    set xlist  [auxiliaryPoints $nodata]

    set values [valuesPolynomial $poly $xlist]
    set result [inproduct $values $values]
    expr {sqrt($result)}
 }

 # inproduct --
 #    Determine the inproduct of two data vectors
 #
 # Arguments:
 #    data1     The values in the first vector
 #    data2     The values in the second vector
 # Result:
 #    Inproduct
 #
 proc ::dpt::inproduct { data1 data2 } {
    set result 0.0

    foreach d1 $data1 d2 $data2 {
       set result [expr {$result+$d1*$d2}]
    }
    return $result
 }

 # scalePolynomial --
 #    Scale a polynomial
 #
 # Arguments:
 #    poly      The polynomial to be scaled
 #    factor    The factor to be applied
 # Result:
 #    New polynomial
 #
 proc ::dpt::scalePolynomial { poly factor } {

    set newpoly [lrange $poly 0 1]
    foreach c [lrange $poly 2 end] {
       lappend newpoly [expr {$c*$factor}]
    }
    return $newpoly
 }

 # addPolynomials --
 #    Add two polynomials
 #
 # Arguments:
 #    poly1     The first polynomial
 #    poly2     The second polynomial
 # Result:
 #    New polynomial
 # Note:
 #    The two polynomials need not be of the same degree!
 #
 proc ::dpt::addPolynomials { poly1 poly2 } {

    set newpoly [lrange $poly1 0 1]
    foreach c1 [lrange $poly1 2 end] c2 [lrange $poly2 2 end] {
       if { $c1 == {} } { set c1 0.0 }
       if { $c2 == {} } { set c2 0.0 }
       lappend newpoly [expr {$c1+$c2}]
    }
    return $newpoly
 }

 # axpyPolynomials --
 #    Add two polynomials and scale the first at the same time
 #
 # Arguments:
 #    factor    Factor for scaling the first
 #    poly1     The first polynomial
 #    poly2     The second polynomial
 # Result:
 #    New polynomial
 # Note:
 #    The two polynomials need not be of the same degree!
 #
 proc ::dpt::axpyPolynomials { factor poly1 poly2 } {

    set newpoly [lrange $poly1 0 1]
    foreach c1 [lrange $poly1 2 end] c2 [lrange $poly2 2 end] {
       if { $c1 == {} } { set c1 0.0 }
       if { $c2 == {} } { set c2 0.0 }
       lappend newpoly [expr {$factor*$c1+$c2}]
    }
    return $newpoly
 }

 # toString --
 #    Turn a polynomial into a readable string
 #
 # Arguments:
 #    poly     The polynomial to be converted
 # Result:
 #    String of the form 1.0 + 1.2*x + 3.0*x^2 ...
 #
 # Note:
 #    Not quite right yet - I forgot about minus signs ...
 #
 proc ::dpt::toString { poly } {

    set string ""
    set power  -1
    foreach c [lrange $poly 2 end] {
       incr power
       if { $c == 0 } { continue }

       #
       # Trick: leave out 1.00000
       #
       set cs "$c"
       if { $cs != 1.0 || $power == 0 } {
          append string "$cs"
          if { $power != 0 } {
             append string "*"
          }
       }
       if { $power > 0 } {
          append string "x"
       }
       if { $power > 1 } {
          append string "^$power"
       }
       append string " + "
    }

    return [string range $string 0 end-3]
 }

 # orthogonalPolynomials --
 #    Build a list of N discretely orthogonal polynomials
 #
 # Arguments:
 #    nodata      Number of data
 #    maxdegree   Maximum degree
 # Result:
 #    List of polynomials that are "discretely" orthogonal
 # Note:
 #    This procedure uses an ordinary Gram-Schmidt algorithm
 #    to achieve the orthogonal (orthonormal) polynomials.
 #    It may not be ideal, but it is fairly straightforward.
 #    Improvements in terms of computational speed:
 #    Some things are calculated more than once, for instance
 #    the values of the polynomial at the auxiliary point.
 #
 proc ::dpt::orthogonalPolynomials { nodata maxdegree } {

    set result   {}
    set v_points {}

    #
    # The first polynomial is simple: P(x) = c, where c = 1/sqrt(nodata)
    #
    set polyn [list DISCRETE-POLYNOMIAL $nodata 1.0]
    set norm  [expr {1.0/[discreteNorm $polyn]}]

    lappend result [scalePolynomial $polyn $norm] ;# To get a norm 1

    #
    # The auxiliary points for which to evalue the polynomials
    #
    set xlist [auxiliaryPoints $nodata]

    lappend v_points [valuesPolynomial [lindex $result 0] $xlist]

    #
    # From there we build up our polynomials
    #
    for { set degree 1 } { $degree <= $maxdegree } { incr degree } {
       set polyn    [linsert $polyn 1 0.0]
       set newpolyn $polyn

       foreach orthop $result orthov $v_points {
          set coeff   [inproduct $orthov [valuesPolynomial $newpolyn $xlist]]
          set newpolyn [axpyPolynomials [expr {-$coeff}] $orthop $newpolyn]
       }

       #
       # Normalise
       #
       set newpolyn [scalePolynomial $newpolyn [expr {1.0/[discreteNorm $newpolyn]}]]
       lappend result   $newpolyn
       lappend v_points [valuesPolynomial $newpolyn $xlist]
    }
    return $result
 }

 # fittingPolynomial --
 #    Compute the best fitting (approximating) polynomial of given degree
 #
 # Arguments:
 #    data        The data to be fitted
 #    maxdegree   The maximum degree for the polynomial
 # Result:
 #    New polynomial that fits the data
 # Note:
 #    The procedure is fairly straightforward:
 #    - Compute the orthonormal polynomials first
 #    - Determine the coefficients for each
 #    - Sum the result
 #
 proc ::dpt::fittingPolynomial { data maxdegree } {
    set nodata    [llength $data]
    set orthopols [orthogonalPolynomials $nodata $maxdegree]

    #
    # The auxiliary points for which to evalue the polynomials
    #
    set xlist [auxiliaryPoints $nodata]

    set result [list DISCRETE-POLYNOMIAL 0.0]
    foreach p $orthopols {
       set coeff  [inproduct [valuesPolynomial $p $xlist] $data]
       set result [axpyPolynomials $coeff $p $result]
    }
    return $result
 }

 # testOrthonormality --
 #    Compute how well the list of polynomials fits orthonormality
 #
 # Arguments:
 #    nodata      The number of data in the fits
 #    maxdegree   The maximum degree for the polynomial
 # Result:
 #    None
 # Side effects:
 #    Printed report
 #
 proc ::dpt::testOrthonormality { nodata maxdegree } {
    set orthopols [orthogonalPolynomials $nodata $maxdegree]

    puts "Number of data points: $nodata"
    puts "Discrete norm of the polynomials (should be 1)"
    set degree 0
    foreach p $orthopols {
       puts "degree $degree: norm = [discreteNorm $p]"
       incr degree
    }

    puts "Non-orthogonality between polynomials (inproduct should be zero)"
    set degree 0
    set xlist  [auxiliaryPoints $nodata]
    foreach p $orthopols {
       set degree2 [expr {$degree+1}]
       foreach q [lrange $orthopols $degree2 end] {
          set inp [inproduct [valuesPolynomial $p $xlist] [valuesPolynomial $q $xlist]]
          puts "degrees $degree and $degree2: inproduct = $inp"
          incr degree2
       }
       incr degree
    }
 }

 # a few tests ---
 #

 set polyn [list DISCRETE-POLYNOMIAL 10 1.0 0.0 2.0 1.0]
 puts [::dpt::toString $polyn]

 set polyn1 [list DISCRETE-POLYNOMIAL 10 1.0 2.0]
 set polyn2 [list DISCRETE-POLYNOMIAL 10 0.0 0.0 2.0 1.0]
 puts [::dpt::toString [::dpt::addPolynomials $polyn1 $polyn2]]

 set orthopols [::dpt::orthogonalPolynomials 10 3]
 foreach p $orthopols {
    puts [::dpt::toString $p]
 }

 puts "Fit trivial data:"
 puts "-1+x expected"
 set data   [list 0 1 2 3 4 5 6 7 8 9 10]
 set result [::dpt::fittingPolynomial $data 3]
 puts [::dpt::toString $result]

 puts "1-2*x+x^2 expected"
 set data   [list 0 1 4 9 16 25 36 49 64 81 100]
 set result [::dpt::fittingPolynomial $data 3]
 puts [::dpt::toString $result]

 puts "Test for quality of orthogonal polynomials"
 ::dpt::testOrthonormality 10 4
 ::dpt::testOrthonormality 100 10

The results:

 1.0 + 2.0*x^2 + x^3
 1.0 + 2.0*x + 2.0*x^2 + x^3
 0.316227766017
 -0.605530070819 + 0.110096376513*x
 0.957427107756 + -0.478713553878*x + 0.0435194139889*x^2
 -1.54380482359 + 1.36927211043*x + -0.296885542998*x^2 + 0.017993063212*x^3
 Fit trivial data:
 -1+x expected
 -1.0 + 1.0*x + 3.90981074709e-014*x^2 + -2.11315780976e-015*x^3
 1-2*x+x^2 expected
 1.0 + -2.0*x + 1.0*x^2 + -3.79690387208e-015*x^3
 Test for quality of orthogonal polynomials
 Number of data points: 10
 Discrete norm of the polynomials (should be 1)
 degree 0: norm = 1.0
 degree 1: norm = 1.0
 degree 2: norm = 1.0
 degree 3: norm = 1.0
 degree 4: norm = 1.0
 Non-orthogonality between polynomials (inproduct should be zero)
 degrees 0 and 1: inproduct = -5.55111512313e-017
 degrees 0 and 2: inproduct = -1.36002320517e-015
 degrees 0 and 3: inproduct = 1.20181642416e-014
 degrees 0 and 4: inproduct = -2.02504679692e-013
 degrees 1 and 2: inproduct = -2.10942374679e-015
 degrees 1 and 3: inproduct = 1.93178806285e-014
 degrees 1 and 4: inproduct = -2.31370478332e-013
 degrees 2 and 3: inproduct = 9.90874049478e-015
 degrees 2 and 4: inproduct = -8.87345752432e-014
 degrees 3 and 4: inproduct = -2.43693953905e-014
 Number of data points: 100
 Discrete norm of the polynomials (should be 1)
 degree 0: norm = 1.0
 degree 1: norm = 1.0
 degree 2: norm = 1.0
 degree 3: norm = 1.0
 degree 4: norm = 1.0
 degree 5: norm = 1.0
 degree 6: norm = 1.0
 degree 7: norm = 1.0
 degree 8: norm = 1.0
 degree 9: norm = 1.00000000001
 degree 10: norm = 1.00000000001
 Non-orthogonality between polynomials (inproduct should be zero)
 degrees 0 and 1: inproduct = 1.2490009027e-016
 degrees 0 and 2: inproduct = 1.21430643318e-016
 degrees 0 and 3: inproduct = -6.71337985203e-015
 degrees 0 and 4: inproduct = -1.0762224445e-014
 degrees 0 and 5: inproduct = 7.17013254325e-013
 degrees 0 and 6: inproduct = -9.62304888552e-012
 degrees 0 and 7: inproduct = 9.26707738325e-011
 degrees 0 and 8: inproduct = -7.20321007702e-010
 degrees 0 and 9: inproduct = 4.39475776257e-009
 degrees 0 and 10: inproduct = -1.81058440948e-008
 degrees 1 and 2: inproduct = -2.77555756156e-017
 degrees 1 and 3: inproduct = -4.71150896075e-015
 degrees 1 and 4: inproduct = -1.44675937896e-014
 degrees 1 and 5: inproduct = 4.85986251242e-013
 degrees 1 and 6: inproduct = -6.81009415526e-012
 degrees 1 and 7: inproduct = 7.36949667957e-011
 degrees 1 and 8: inproduct = -6.32568483705e-010
 degrees 1 and 9: inproduct = 4.16724826868e-009
 degrees 1 and 10: inproduct = -1.84985766982e-008
 degrees 2 and 3: inproduct = 9.36750677027e-016
 ...
 degrees 8 and 9: inproduct = -3.20738435811e-011
 degrees 8 and 10: inproduct = 8.70094343797e-010
 degrees 9 and 10: inproduct = -3.22448748258e-010

[ Category Mathematics

Category Numerical Analysis

]