Version 12 of Runge-Kutta-Fehlberg

Updated 2006-05-17 08:04:59

Rung-Kutta-Fehlberg (RKF) method is a numerical method of solving ordinary differential equations derived from the Runge-Kutta method. RKF uses a fourth order and fifth order accurate method to provide 2 estimates of the next step in the solution; the difference in these results is related to the accuracy of the solution and can be used to decrease the integration step length if the error exceeds a specified accuracy. The clever feature of RKF is that it reduces the number of evaluations of the force functions by using an integrator that uses some of the same values in both the 4th order and 5th order methods.

The attached code compares RKF solution with the TCLlib::math calculus RK4 4th order method. The implemetnation is designed to emulate the RK4 code with the addition of the error vector to specify the desired accuracy.

gwm 17-05-05 correction to recommended step length improves speed, and accuracy.

  # rk-f properties, see http://math.fullerton.edu/mathews/n2003/RungeKuttaFehlbergMod.html
  # basicaly replaces 4th order with 4th and fifth order solution and 
  # calculates estimate of time step required for next step of integration.
  # By careful selection of the RK coefficients, fewer evaluations of derivatives etc are required.


  proc rungeKuttaFehlbergStep { t tstep xvec func {errvec ""}} {
        ;# find steps required to reach tolerance errvec
        # divide step tstep into N equally spaced steps (tstep/N) if estimated step length < tstep.
        #
        # Six steps:
        # - k1=tstep*f(t,y)
        # - k2=tstep*f(t+.25.tstep,y+.25*k1)
        # - k3=tstep*f(t+3tstep/8, y+(3.k1+9.k2)/32) => (tt,y+3/8*(k1/4+3k2/4))
        # - k4=tstep*f(t+12tstep/13, y+(1932.k1-7200.k2+7296*k3)/2197)
        # - k5=tstep*f(t+tstep, y+(439.k1/216-8.k2+3680.k3/513-845*k4/4104))
        # - k6=tstep*f(t+tstep/2, y+(-8.k1/27+2.k2-3544.k3/2565+1859.k4/4104-11.k5/40))
        # 4th order sol is y4th=y+25/216*k1+1408/2565.k3+2197/4104*k4-k5/5
        # 5th order sol is y5th=y+16/135*k1+6656/12825.k3+28561/56430*k4-9k5/50+2/55k6
        # and opt step size is s.h where err is the desired accuracy (dimension cancels with y5,y4)
        # s = {err.h/(2*(y5th-y4th)))}^0.25
        # or [s^4*2*(y5th-y4th)]/h=err??
        #

        set funcq [uplevel 1 namespace which -command $func]
        if {$errvec != ""} {
                upvar 1 $errvec errv ;# set "local" address of variable with constant error desired.
        } else {set errv $xvec}
        # - k1=f(t,y)
        set xk1 [$funcq $t $xvec] ;# first step is vector of derivatives f(t,y)
        set tstep2 [expr {$tstep/4.0}]
        set xvec2 {}
        foreach x1 $xvec xv1 $xk1 {
                lappend xvec2 [expr {$x1+$tstep2*$xv1}]
        }
        # - k2=tstep*f(t+.25.tstep,y+.25*k1) tstep2=dt/4
        set xk2 [$funcq [expr {$t+$tstep2}] $xvec2]
        # - k3=tstep*f(t+3tstep/8, y+(3.k1+9.k2)/32) => xk3=tstep*f(t+tstep3,y+3/8*(k1/4+3k2/4))
        # =tstep3*f(t+tstep3,y+(k1/4+3k2/4))
        set tstep3 [expr {3.0*$tstep/8.0}]
        set xvec3 {}
        foreach x1 $xvec xv1 $xk1 xv2 $xk2 {
                lappend xvec3 [expr {$x1+$tstep3*(.25*$xv1+.75*$xv2)}]
        }
        set xk3 [$funcq [expr {$t+$tstep3}] $xvec3]
        # - k4=tstep*f(t+12tstep/13, y+(1932.k1-7200.k2+7296*k3)/2197) =>
        # xk4=tstep4*f(t+tstep4, y+(161.k1-600.k2+608*k3)/169)
        set tstep4 [expr {12.0*$tstep/13.0}]
        set xvec4 {}
        foreach x1 $xvec xv1 $xk1 xv2 $xk2 xv3 $xk3 {
                lappend xvec4 [expr {$x1+$tstep4*(161.0*$xv1-600.0*$xv2+608.0*$xv3)/169.0}]
        }
        set xk4 [$funcq [expr {$t+$tstep4}] $xvec4]

        # k5=tstep*f(t+tstep, y+(439.k1/216-8.k2+3680.k3/513-845.0*k4/4104)) =>
        # xk5=tstep5*f(t+tstep5, y+(439.k1/216-8.k2+3680.k3/513-845*k4/4104))
        set tstep5 $tstep
        set xvec5 {}
        foreach x1 $xvec xv1 $xk1 xv2 $xk2 xv3 $xk3 xv4 $xk4 {
                lappend xvec5 [expr {$x1+$tstep5*(439./216*$xv1-8.*$xv2+3680.0/513*$xv3-845.0/4104*$xv4)}]
        }
        set xk5 [$funcq [expr {$t+$tstep5}] $xvec5]
        # - k6=tstep*f(t+tstep/2, y+(-8.k1/27+2.k2-3544.k3/2565+1859.k4/4104-11.k5/40)) =>
        # xk6=tstep6*f(t+tstep/2, y+(-16.k1/27+2.k2-7088.k3/2565+1859.k4/2052-11.k5/20))
        set tstep6 [expr {$tstep/2.0}]
        set xvec6 {}
        foreach x1 $xvec xv1 $xk1 xv2 $xk2 xv3 $xk3 xv4 $xk4 xv5 $xk5 {
                lappend xvec6 [expr {$x1+$tstep*(-8./27*$xv1+2.*$xv2-3544./2565*$xv3+1859./4104.*$xv4-11./40*$xv5)}]
        }
        set xk6 [$funcq [expr {$t+$tstep6}] $xvec6]
        # 4th order sol is y4th=y+25/216*k1+1408/2565.k3+2197/4104*k4-k5/5
        # 5th order sol is y5th=y+16/135*k1+6656/12825.k3+28561/56430*k4-9k5/50+2/55k6
        # use 5th order result
        set result {}
        set res4 {}
        set stepest $tstep
        foreach x0 $xvec k1 $xk1 k3 $xk3 k4 $xk4 k5 $xk5 k6 $xk6 err $errv { ;# values {k2 $xk2} are not used
                set dx [expr {16.0/135.0*$k1+6656.0/12825.0*$k3+28561.0/56430.0*$k4-0.18*$k5+2.0/55.0*$k6}]
                lappend result [expr {$x0+$dx*$tstep}]
                if {$errvec != ""} {
                        set dx4 [expr {25.0/216.0*$k1+1408.0/2565*$k3+2197/4104.0*$k4-0.2*$k5}]
                        lappend res4 [expr {$x0+$dx4*$tstep}]
                        if {$dx!=$dx4} {
                        #step size recommended is s = {err.h/(2*(y5th-y4th)))}^0.25
                        set spp [expr {$tstep*pow($err*$tstep/(2.0*$tstep*(abs( $dx-$dx4 ))) , 0.25)}]
                        if {$spp <$stepest} {

#puts "Miniisiung $spp, expr $spp*$tstep"

                                set stepest $spp
                        }
                        }
                }
        }
        if {$stepest<$tstep} { ;# adjust time step by redoing N sub-steps
                set nsubsteps 2;#[expr {int(ceil( $tstep/$stepest))}] ;# or 2
                set dtsub [expr {double($tstep)/$nsubsteps}]
                set result $xvec ;# restart half time step
        #         puts "[info level] at $t $tstep rec $stepest dt is [expr {double($tstep)/int(ceil( $tstep/$stepest))}]  [lindex $result 0]  [lindex $result 1]"
                set i 0
                        set tsu $t
                while {$i<$nsubsteps} {
                        set result [rungeKuttaFehlbergStep $tsu $dtsub $result $func errv ]
                #        puts "[info level] at $t dt is $dtsub [lindex $result 0]  [lindex $result 1]"
 # if the estimate is not good enough then we will recurse again in the sub-step
                        set tsu [expr {$tsu+$dtsub} ]
                        incr i
                }
        }
        return $result
  }

  if {1==0} {

This section is the graph drawing widget option found under Another Graph Widget

  }

 proc grapharea {w args} { ;# a graph plotter canvas derived thing.
        set dx 400
        set dy 400
        global $w.pencolour $w.xmin $w.xmax $w.ymin $w.ymax
        set $w.pencolour black
        set  $w.xmin -1;        set $w.xmax 1
        set  $w.ymin -1; set $w.ymax 1
                array set options {{-colour} red}
                # split off the custom options:
                set textArgs [list]
                foreach {opt val} $args {
                        switch -- $opt {
                                {-pencolour} {set options($opt) $val}
                                default {lappend textArgs $opt $val}
                        }
                }
        eval canvas $w $textArgs

        interp hide {} $w
        # Install the alias:
        interp alias {} $w {} graphareaCmd $w
        foreach opt [array names options] {
                $w configure $opt $options($opt)
        }
          return $w ;# the original canvas
 }
  proc graphareaCmd {self cmd args} {
        switch -- $cmd {
                configure {eval graphareaConfigure $self $cmd $args}
                cget {eval graphareaCget $self $args}
                showgrid { ;# draw a grid of lines on the graph
                        set x0 [$self cget xmin]
                        set x1 [$self cget xmax]
                        set y0 [$self cget ymin]
                        set y1 [$self cget ymax]
                        set dx [lindex $args 0]
                        set x [expr {int($x0/$dx)*$dx}] ;# choose lines s.t. zero is a line.
                        while {$x<$x1} {
                                $self create line $x $y0 $x $y1
                                set x [expr {$x+$dx}]
                        }
                        set dy [lindex $args 1]
                        set y [expr {int($y0/$dy)*$dy}]
                        while {$y<$y1} {
                                $self create line $x0 $y $x1 $y
                                set y [expr {$y+$dy}]
                        }
                }
                  create  { ;# replaces the create default of a canvas which works in pixels
                        #- adds text, rectangle, oval, polygon... (eg) positioned at scaled position
                        set args [eval concat $args] ;# this removes a level of curlies if necessary from the list.
                        set penc [$self cget pencolour];# get "local" pen colour name
                        # scale factor for draw space to pixels
                        set x0 [$self cget xmin];                        set x1 [$self cget xmax]
                        set y0 [$self cget ymin];                        set y1 [$self cget ymax]
                        set wid [$self cget -width]
                        set ht [$self cget -height]
                        set idx 1 ;# where to start in args for coordinates
                        while {$idx<[llength $args]} {
                                if {[string is double [lindex $args $idx]]} {
                                        lappend xylist [expr {int($wid*double([lindex $args $idx]-$x0)/($x1-$x0))}]
                                        set args [lreplace $args $idx $idx]
                                        lappend xylist [expr {int($ht*double([lindex $args $idx]-$y1)/($y0-$y1))}]
                                        set args [lreplace $args $idx $idx]
                                } else {
                                        incr idx
                                }
                        }
                        switch [lindex $args 0] {
                                {line}  -
                                {text} {
                                        lappend command [lindex $args 0] $xylist -fill $penc
                                        if {[llength $args]>1} { set command [concat $command [lrange $args 1 end]] }
                                        eval interp invokehidden {{}} $self create $command
                                }
                                {default} {
                                        lappend command [lindex $args 0] $xylist -outline $penc
                                        if {[llength $args]>1} { set command [concat $command [lrange $args 1 end]] }
                                        eval interp invokehidden {{}} $self create $command
                                }
                        }
                }
        }
  }
  proc graphareaConfigure {self cmd args} {
        # 3 scenarios:
        #
        # $args is empty       -> return all options with their values
        # $args is one element -> return current values
        # $args is 2+ elements -> configure the options
        switch [llength $args] {
                0 { ;# return argument values
                        set result [ option get $self -pencolour ""]

                        # default options:
                        lappend result [interp invokehidden {} $self configure ]
                        #        lappend result [uplevel 1  $self cconfigure]
                        return $result
                }
                1 {
                        switch -- $args {
                                {-pencolour} {return [uplevel 1 [list interp invokehidden {} $self configure -pencolour]]}
                                default {return [uplevel 1 interp invokehidden {} $self configure $args]}
                                }
                        }
                default { ;# >1 arg - an option and its value
                                # go through each option:
                                foreach {option value} $args {
                                        switch -- $option {
                                                {-xmin} -
                                                {-xmax} -
                                                {-ymin} -
                                                {-ymax} -
                                                {-pencolour} {
  if {1==0} {

This is the part I dont like - I create a global variable for each added option.

  }
                                        global $self.[string range $option 1 end]
                                        set $self.[string range $option 1 end] $value
                                        }
                                        default {puts " default $option, $value for $self";$self configure $option $value}
                                                ;#$self configure $option $value
                                }
                        }
                        return {}
                }
        }
  }
  proc graphareaCget {self args} {
        # cget defaults done by the canvas cget command
        #puts "In graphareaCget option $self $args"
        if {[info exists ::$self.$args]} {upvar #0 $self.$args val; return $val}
        switch -- $args {
                {-xmin} -
                {-xmax} -
                {-ymin} -
                {-ymax} -
                {-pencolour} {puts "Cget option $args"
                        return [uplevel 1 [list interp invokehidden {} $self cget [string range $args 1 end]]]}
                default {return [uplevel 1 [list interp invokehidden {} $self cget $args]]}
        }
  }

  if {1==0} {

This section exercises the RKF solver

red line is the RKF tme step adjusted solution (there may be more than 1 step between each graph point) green line is the RK4 solution using the base timesteps of the RKF solution

 and compares to a short step RK4 solver (curve in black).

The short step RK4 is assumed to be accurate. The RKF points coincide with the short step RK4 curve. The long step RK4 points do not coincide. The RKF algorithm has automatically adjusted the time steps to (1) fit exactly N steps between the basic steps and (2) keep the estimated error below the requested accuracy.

This technique is useful for dynamics calculations where long time steps are needed for the motion between collisions, and short time steps during a collision. Also for calculating the trajectory of a highly eccentric object (such as a comet) around the sun where shorter steps are useful when the object is near the sun as the acceleration of the object is high. (EG Halley's comet spends 76 years orbiting the sun, for less than 3 months is it visible from earth.)

  }

  package require math::calculus  ;# used for comparison with supplied RK4 algorithm

  proc dampened_oscillator {t xvec} { ;# as used by RungeKuttaStep also used by RKFstep.
        global damp
           set x  [lindex $xvec 0] ;# pos
           set x1 [lindex $xvec 1] ;# velocity
        # dpos/dx=v
        # dv/dx= k.(x-origin)-damp*vel
           return [list $x1 [expr {-$damp*$x1-$x}]]
  }

  set tmax 40.0
  set damp 0.0
        catch {destroy .fib}
  set dr2 [grapharea .fib -width 600 -height 400]
 $dr2 configure -pencolour gray
        $dr2 configure -xmin -1 -xmax $tmax  -ymin -1.2 -ymax 1.2
        pack $dr2
        $dr2 configure -pencolour gray50
        $dr2 showgrid 5 1
        $dr2 configure -pencolour orange
  while {$damp<1} {
    puts "Damp is $damp"
    set xvec   { 1.0 0.0 }  ;# position, velocity
    set xvec4   { 1.0 0.0 }  ;# position, velocity rk4 solver
    set t      0.0
    set errvec {0.001 0.0002}
    set tstep  2.5 ;# if tstep <1.5 then RK4 with constant time step is acceptable
        # try tstep 3 to see rk4 diverging drastically!
        set pos ""
    lappend pos $t [lindex $xvec 0]
        set pos4rk ""; lappend pos4rk $t [lindex $xvec4 0]
    while { $t < $tmax } {

       set res4 [::math::calculus::rungeKuttaStep  $t $tstep $xvec4 dampened_oscillator]
       set result [rungeKuttaFehlbergStep  $t $tstep $xvec dampened_oscillator errvec]
       set t2      [expr {$t+$tstep}]
        lappend pos   $t2 [lindex $result 0]
        lappend pos4rk  $t2 [lindex $res4 0]
       set t      $t2
       set xvec   $result
       set xvec4   $res4
    }
        $dr2 configure -pencolour red
        $dr2 create line $pos
        $dr2  create text 10.0 1.1  -text {{Runge-Kutta-Fehlberg error correcting}}
        $dr2 configure -pencolour green
        $dr2 create line $pos4rk
        $dr2  create text 10.0 0.9  -text { "Runge-Kutta long step"}
    # redo calculation using RK 4th order, but very short time step
    set xvec4   { 1.0 0.0 }  ;# position, velocity rk4 solver
    set t 0
    set tstep [expr {$tstep/10.0}]
        set pos "";lappend pos $t [lindex $xvec4 0]
    while { $t < $tmax } {
       set res4 [::math::calculus::rungeKuttaStep  $t $tstep $xvec4 dampened_oscillator]
       set t2      [expr {$t+$tstep}]
        lappend pos $t2 [lindex $res4 0]
       set t      $t2
       set xvec4   $res4
    }
        $dr2 configure -pencolour black
        $dr2 create line $pos
        $dr2  create text 10.0 1.0  -text {"Runge-Kutta short step"}
    set damp [expr {$damp+0.1}]
    update
  }

An even better test is a particle bouncing between 2 walls - the forces are zero except when the particle hits the wall. The wall is stiff, so the force is very high briefly, requiring very short steps during the collision, but allowing long steps between collisions. The sample below correctly predicts the collisions at 1,3,5,7,9 and 11 seconds.

                puts "Type is $type"
        # hitwal - 2 solid walls with ball bouncing between.
  proc wallhit {t xvec} { ;# as used by RungeKuttaStep also used by RKFstep.
           set x  [lindex $xvec 0] ;# pos
           set x1 [lindex $xvec 1] ;# velocity
        # dpos/dx=v
        # dv/dx= if x>wall k.(x-wall) or if x<-wall f=-k.wall-x
        set wc 1.e10
        set f 0
        if {$x>1.0} { set f [expr {-$wc*($x-1.0)}] 
        } elseif {$x<-1.0} { set f [expr {-$wc*($x+1.0)}] 
        }
        #puts "$t $xvec -> forces $x1 $f"
           return [list $x1 $f]
  }
    set xvec   { 0.0 1.0 }  ;# position, velocity
    set errvec {0.0002 2.e-5}
    set t      0.0
        set tmax 22.2
        catch {destroy .fib}
  set dr2 [grapharea .fib -width 600 -height 400]
  $dr2 configure -pencolour gray
        $dr2 configure -xmin -1 -xmax $tmax  -ymin -1.2 -ymax 1.2
        pack $dr2
        $dr2 configure -pencolour gray50
        $dr2 showgrid 5 1
        $dr2 configure -pencolour orange
        set tstep 0.05 ;# take 10 steps to hit wall
        set pos ""
        while {$t<$tmax} {
       set result [rungeKuttaFehlbergStep  $t $tstep $xvec wallhit  errvec]
       set xvec   $result
                puts "Time $t [lindex $result 0]  [lindex $result 1]"
        lappend pos    $t [lindex $result 0] 
                set t [expr {$t+$tstep}]
                update
        }
     $dr2 create line $pos

See the reference [L1 ] for more details of RKF method.


Category Mathematics - Category Physics