#use strict; ########### #use Carp; =head1 NAME Numerical methods for the PG language =head1 SYNPOSIS =head1 DESCRIPTION =cut =head2 Interpolation methods =head3 Plotting a list of points (piecewise linear interpolation) Usage: plot_list([x0,y0,x1,y1,...]); plot_list([(x0,y0),(x1,y1),...]); plot_list(\x_y_array); plot_list([x0,x1,x2...], [y0,y1,y2,...]); plot_list(\@xarray,\@yarray); =cut BEGIN { be_strict(); } sub _PGnumericalmacros_init { } sub plot_list { my($xref,$yref) = @_; unless( defined($xref) && ref($xref) =~/ARRAY/ ) { die "Error in plot_list:X values must be given as an array reference. Remember to use ~~\@array to reference an array in the PG language."; } if (defined($yref) && ! ( ref($yref) =~/ARRAY/ ) ) { die "Error in plot_list:Y values must be given as an array reference. Remember to use ~~\@array to reference an array in the PG language."; } my (@x_vals, @y_vals); unless( defined($yref) ) { #with only one entry we assume (x0,y0,x1,y1..); if ( @$xref % 2 ==1) { die "ERROR in plot_list -- single array of input has odd number of elements"; } my @in = @$xref; while (@in) { push(@x_vals, shift(@in)); push(@y_vals, shift(@in)); } $xref = \@x_vals; $yref = \@y_vals; } my $fun =sub { my $x = shift; my $y; my( $x0,$x1,$y0,$y1); my @x_values = @$xref; my @y_values = @$yref; while (( @x_values and $x > $x_values[0]) or ( @x_values > 0 and $x >= $x_values[0] ) ) { $x0 = shift(@x_values); $y0 = shift(@y_values); } # now that we have the left hand of the input #check first that x isn't out of range to the left or right if (@x_values && defined($x0) ) { $x1= shift(@x_values); $y1=shift(@y_values); $y = $y0 + ($y1-$y0)*($x-$x0)/($x1-$x0); } $y; }; $fun; } =head3 Horner polynomial/ Newton polynomial Usege: $fn = horner([x0,x1,x2],[q0,q1,q2]); Produces the newton polynomial &$fn(x) = q0 + q1*(x-x0) +q2*(x-x1)*(x-x0); Generates a subroutine which evaluates a polynomial passing through the points C<(x0,q0), (x1,q1), ... > using Horner's method. =cut sub horner { my ($xref,$qref) = @_; # get the coefficients my $fn = sub { my $x = shift; my @xvals = @$xref; my @qvals = @$qref; my $y = pop(@qvals); pop(@xvals); while (@qvals) { $y = $y * ($x - pop(@xvals) ) + pop(@qvals); } $y; }; $fn; } =head3 Hermite polynomials =pod Usage: $poly = hermit([x0,x1...],[y0,y1...],[yp0,yp1,...]); Produces a reference to polynomial function with the specified values and first derivatives at (x0,x1,...). &$poly(34) gives a number Generates a subroutine which evaluates a polynomial passing through the specified points with the specified derivatives: (x0,y0,yp0) ... The polynomial will be of high degree and may wobble unexpectedly. Use the Hermite splines described below and in Hermite.pm for most graphing purposes. =cut sub hermite { my($x_ref,$y_ref,$yp_ref) = @_; my (@zvals,@qvals); my $n = $#{$x_ref}; my $i =0; foreach $i (0..$n ) { $zvals[2*$i] = $$x_ref[$i]; $zvals[2*$i+1] = $$x_ref[$i]; $qvals[2*$i][0] = $$y_ref[$i]; $qvals[2*$i+1][0] = $$y_ref[$i]; $qvals[2*$i+1][1] = $$yp_ref[$i]; $qvals[2*$i][1] = ( $qvals[2*$i][0] - $qvals[2*$i-1][0] ) / ( $zvals[2*$i]- $zvals[2*$i-1] ) unless $i ==0; } my $j; foreach $i ( 2..(2*$n+1) ) { foreach $j (2 .. $i) { $qvals[$i][$j] = ($qvals[$i][$j-1] - $qvals[$i-1][$j-1]) / ($zvals[$i] - $zvals[$i-$j]); } } my @output; foreach $i (0..2*$n+1) { push(@output,$qvals[$i][$i]); } horner(\@zvals,\@output); } =head3 Hermite splines Usage: $spline = hermit_spline([x0,x1...],[y0,y1...],[yp0,yp1,...]); Produces a reference to a piecewise cubic hermit spline with the specified values and first derivatives at (x0,x1,...). &$spline(45) evaluates to a number. Generates a subroutine which evaluates a piecewise cubic polynomial passing through the specified points with the specified derivatives: (x0,y0,yp0) ... An object oriented version of this is defined in Hermite.pm =cut sub hermite_spline { my ($xref, $yref, $ypref) = @_; my @xvals = @$xref; my @yvals = @$yref; my @ypvals = @$ypref; my $x0 = shift @xvals; my $y0 = shift @yvals; my $yp0 = shift @ypvals; my ($x1,$y1,$yp1); my @polys; #calculate a hermite polynomial evaluator for each region while (@xvals) { $x1 = shift @xvals; $y1 = shift @yvals; $yp1 = shift @ypvals; push @polys, hermite([$x0,$x1],[$y0,$y1],[$yp0,$yp1]); $x0 = $x1; $y0 = $y1; $yp0 = $yp1; } my $hermite_spline_sub = sub { my $x = shift; my $y; my $fun; my @xvals = @$xref; my @fns = @polys; return $y=&{$fns[0]} ($x) if $x == $xvals[0]; #handle left most endpoint while (@xvals && $x > $xvals[0]) { # find the function for this range of x shift(@xvals); $fun = shift(@fns); } # now that we have the left hand of the input #check first that x isn't out of range to the left or right if (@xvals && defined($fun) ) { $y =&$fun($x); } $y; }; $hermite_spline_sub; } =head3 Cubic spline approximation Usage: $fun_ref = cubic_spline(~~@x_values, ~~@y_values); Where the x and y value arrays come from the function to be approximated. The function reference will take a single value x and produce value y. $y = &$fun_ref($x); You can also generate javaScript which defines a cubic spline: $function_string = javaScript_cubic_spline(~~@_x_values, ~~@y_values, name => 'myfunction1', llimit => -3, rlimit => 3, ); The string contains and can be placed in the header of the HTML output using HEADER_TEXT($function_string); =cut sub cubic_spline { my($t_ref, $y_ref) = @_; my @spline_coeff = create_cubic_spline($t_ref, $y_ref); sub { my $x = shift; eval_cubic_spline($x,@spline_coeff); } } sub eval_cubic_spline { my ($x, $t_ref,$a_ref,$b_ref,$c_ref,$d_ref ) = @_; # The knot points given by $t_ref should be in order. my $i=0; my $out =0; while (defined($t_ref->[$i+1] ) && $x > $t_ref->[$i+1] ) { $i++; } unless (defined($t_ref->[$i]) && ( $t_ref->[$i] <= $x ) && ($x <= $t_ref->[$i+1] ) ) { $out = undef; # input value is not in the range defined by the function. } else { $out = ( $t_ref->[$i+1] - $x )* ( ($d_ref->[$i]) +($a_ref->[$i])*( $t_ref->[$i+1] - $x )**2 ) + ( $x - $t_ref->[$i] ) * ( ($b_ref->[$i])*( $x - $t_ref->[$i] )**2 + ($c_ref->[$i]) ) } $out; } ########################################################################## # Cubic spline algorithm adapted from p319 of Kincaid and Cheney's Numerical Analysis ########################################################################## sub create_cubic_spline { my($t_ref, $y_ref) = @_; # The knot points are given by $t_ref (in order) and the function values by $y_ref my $num =$#{$t_ref}; # number of knots my $i; my (@h,@b,@u,@v,@z); foreach $i (0..$num-1) { $h[$i] = $t_ref->[$i+1] - $t_ref->[$i]; $b[$i] = 6*( $y_ref->[$i+1] - $y_ref->[$i] )/$h[$i]; } $u[1] = 2*($h[0] +$h[1] ); $v[1] = $b[1] - $b[0]; foreach $i (2..$num-1) { $u[$i] = 2*( $h[$i] + $h[$i-1] ) - ($h[$i-1])**2/$u[$i-1]; $v[$i] = $b[$i] - $b[$i-1] - $h[$i-1]*$v[$i-1]/$u[$i-1] } $z[$num] = 0; for ($i = $num-1; $i>0; $i--) { $z[$i] = ( $v[$i] - $h[$i]*$z[$i+1] )/$u[$i]; } $z[0] = 0; my ($a_ref, $b_ref, $c_ref, $d_ref); foreach $i (0..$num-1) { $a_ref->[$i] = $z[$i]/(6*$h[$i]); $b_ref->[$i] = $z[$i+1]/(6*$h[$i]); $c_ref->[$i] = ( ($y_ref->[$i+1])/$h[$i] - $z[$i+1]*$h[$i]/6 ); $d_ref->[$i] = ( ($y_ref->[$i])/$h[$i] - $z[$i]*$h[$i]/6 ); } ($t_ref, $a_ref, $b_ref, $c_ref, $d_ref); } sub javaScript_cubic_spline { my $x_ref = shift; my $y_ref = shift; my %options = @_; assign_option_aliases(\%options, ); set_default_options(\%options, name => 'func', llimit => $x_ref->[0], rlimit => $x_ref->[$#$x_ref], ); my ($t_ref, $a_ref, $b_ref, $c_ref, $d_ref) = create_cubic_spline ($x_ref, $y_ref); my $str_t_array = "t = new Array(" . join(",",@$t_ref) . ");"; my $str_a_array = "a = new Array(" . join(",",@$a_ref) . ");"; my $str_b_array = "b = new Array(" . join(",",@$b_ref) . ");"; my $str_c_array = "c = new Array(" . join(",",@$c_ref) . ");"; my $str_d_array = "d = new Array(" . join(",",@$d_ref) . ");"; my $output_str = < END_OF_JAVA_TEXT $output_str; } =head2 Numerical Integration methods =head3 Integration by Left Hand Sum =pod Usage: lefthandsum(function_reference, start, end, steps=>30 ); Implements the Left Hand sum using 30 intervals between 'start' and 'end'. The first three arguments are required. The final argument (number of steps) is optional and defaults to 30. =cut sub lefthandsum { my $fn_ref = shift; my $x0 = shift; my $x1 = shift; my %options = @_; assign_option_aliases(\%options, intervals => 'steps', ); set_default_options(\%options, steps => 30, ); my $steps = $options{steps}; my $delta = ($x1-$x0)/$steps; my $i; my $sum=0; foreach $i (0..($steps-1) ) { $sum =$sum + &$fn_ref( $x0 + ($i)*$delta ); } $sum*$delta; } =head3 Integration by Right Hand Sum =pod Usage: righthandsum(function_reference, start, end, steps=>30 ); Implements the right hand sum using 30 intervals between 'start' and 'end'. The first three arguments are required. The final argument (number of steps) is optional and defaults to 30. =cut sub righthandsum { my $fn_ref = shift; my $x0 = shift; my $x1 = shift; my %options = @_; assign_option_aliases(\%options, intervals => 'steps', ); set_default_options(\%options, steps => 30, ); my $steps = $options{steps}; my $delta = ($x1-$x0)/$steps; my $i; my $sum=0; foreach $i (1..($steps) ) { $sum =$sum + &$fn_ref( $x0 + ($i)*$delta ); } $sum*$delta; } =head3 Integration by Midpoint rule =pod Usage: midpoint(function_reference, start, end, steps=>30 ); Implements the Midpoint rule using 30 intervals between 'start' and 'end'. The first three arguments are required. The final argument (number of steps) is optional and defaults to 30. =cut sub midpoint { my $fn_ref = shift; my $x0 = shift; my $x1 = shift; my %options = @_; assign_option_aliases(\%options, intervals => 'steps', ); set_default_options(\%options, steps => 30, ); my $steps = $options{steps}; my $delta = ($x1-$x0)/$steps; my $i; my $sum=0; foreach $i (0..($steps-1) ) { $sum =$sum + &$fn_ref( $x0 + ($i+1/2)*$delta ); } $sum*$delta; } =head3 Integration by Simpson's rule =pod Usage: simpson(function_reference, start, end, steps=>30 ); Implements Simpson's rule using 30 intervals between 'start' and 'end'. The first three arguments are required. The final argument (number of steps) is optional and defaults to 30, but must be even. =cut sub simpson { my $fn_ref = shift; my $x0 = shift; my $x1 = shift; my %options = @_; assign_option_aliases(\%options, intervals => 'steps', ); set_default_options(\%options, steps => 30, ); my $steps = $options{steps}; unless( $steps % 2 == 0 ) { die "Error: Simpson's rule requires an even number of steps."; } my $delta = ($x1-$x0)/$steps; my $i; my $sum=0; for ($i=1;$i<$steps;$i=$i+2) { # look this up - loop by two. $sum =$sum + 4*&$fn_ref( $x0 + $i*$delta ); } for ($i=2;$i<$steps-1;$i=$i+2) { # ditto $sum = $sum + 2*&$fn_ref( $x0 + $i*$delta); } $sum = $sum + &$fn_ref($x0) + &$fn_ref($x1); $sum*$delta/3; } =head3 Integration by trapezoid rule =pod Usage: trapezoid(function_reference, start, end, steps=>30 ); Implements the trapezoid rule using 30 intervals between 'start' and 'end'. The first three arguments are required. The final argument (number of steps) is optional and defaults to 30. =cut sub trapezoid { my $fn_ref = shift; my $x0 = shift; my $x1 = shift; my %options = @_; assign_option_aliases(\%options, intervals => 'steps', ); set_default_options(\%options, steps => 30, ); my $steps = $options{steps}; my $delta =($x1-$x0)/$steps; my $i; my $sum=0; foreach $i (1..($steps-1) ) { $sum =$sum + &$fn_ref( $x0 + $i*$delta ); } $sum = $sum + 0.5*( &$fn_ref($x0) + &$fn_ref($x1) ); $sum*$delta; } =head3 Romberg method of integration =pod Usage: romberg(function_reference, x0, x1, level); Implements the Romberg integration routine through 'level' recursive steps. Level defaults to 6. =cut sub romberg { my $fn_ref = shift; my $x0 = shift; my $x1 = shift; my %options = @_; set_default_options(\%options, level => 6, ); my $level = $options{level}; romberg_iter($fn_ref, $x0,$x1, $level,$level); } sub romberg_iter { my $fn_ref = shift; my $x0 = shift; my $x1 = shift; my $j = shift; my $k = shift; my $out; if ($k == 1 ) { $out = trapezoid($fn_ref, $x0,$x1,steps => 2**($j-1) ); } else { $out = ( 4**($k-1) * romberg_iter($fn_ref, $x0,$x1,$j,$k-1) - romberg_iter($fn_ref, $x0,$x1,$j-1,$k-1) ) / ( 4**($k-1) -1) ; } $out; } =head3 Inverse Romberg =pod Usage: inv_romberg(function_reference, a, value); Finds b such that the integral of the function from a to b is equal to value. Assumes that the function is continuous and doesn't take on the zero value. Uses Newton's method of approximating roots of equations, and Romberg to evaluate definite integrals. =cut sub inv_romberg { my $fn_ref = shift; my $a = shift; my $value = shift; my $b = $a; my $delta = 1; my $i=0; my $funct; my $deriv; while (abs($delta) > 0.000001 && $i < 5000) { $funct = romberg($fn_ref,$a,$b)-$value; $deriv = &$fn_ref ( $b ); if ($deriv == 0) { warn 'Values of the function are too close to 0.'; return; } $delta = $funct/$deriv; $b = $b - $delta; $i++; } if ($i == 5000) { warn 'Newtons method does not converge.'; return; } $b; } ######################################################### =pod rungeKutta4 Finds integral curve of a vector field using the 4th order Runge Kutta method. Useage: rungeKutta4( &vectorField(t,x),%options); Returns: \@array of points [t,y}) Default %options: 'initial_t' => 1, 'initial_y' => 1, 'dt' => .01, 'num_of_points' => 10, #number of reported points 'interior_points' => 5, # number of 'interior' steps between reported points 'debug' =cut sub rungeKutta4 { my $rf_fun = shift; my %options = @_; set_default_options( \%options, 'initial_t' => 1, 'initial_y' => 1, 'dt' => .01, 'num_of_points' => 10, #number of reported points 'interior_points' => 5, # number of 'interior' steps between reported points 'debug' => 1, # remind programmers to always pass the debug parameter ); my $t = $options{initial_t}; my $y = $options{initial_y}; my $num = $options{'num_of_points'}; # number of points my $num2 = $options{'interior_points'}; # number of steps between points. my $dt = $options{'dt'}; my $errors = undef; my $rf_rhs = sub { my @in = @_; my ( $out, $err) = &$rf_fun(@in); $errors .= " $err at ( ".join(" , ", @in) . " )
\n" if defined($err); $out = 'NaN' if defined($err) and not is_a_number($out); $out; }; my @output = ([$t, $y]); my ($i, $j, $K1,$K2,$K3,$K4); for ($j=0; $j<$num; $j++) { for ($i=0; $i<$num2; $i++) { $K1 = $dt*&$rf_rhs($t, $y); $K2 = $dt*&$rf_rhs($t+$dt/2,$y+$K1/2); $K3 = $dt*&$rf_rhs($t+$dt/2, $y+$K2/2); $K4 = $dt*&$rf_rhs($t+$dt, $y+$K3); $y = $y + ($K1 + 2*$K2 + 2*$K3 + $K4)/6; $t = $t + $dt; } push(@output, [$t, $y]); } if (defined $errors) { return $errors; } else { return \@output; } } 1;