#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 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;
}
#########################################################
1;