package Math::Interpolate; require 5.004_01; use strict; use Exporter; use Math::IntervalSearch qw(interval_search); use vars qw(@EXPORT_OK @ISA $VERSION); @EXPORT_OK = qw(derivatives constant_interpolate linear_interpolate robust_interpolate); @ISA = qw(Exporter); $VERSION = substr q$Revision: 1.05 $, 10; sub derivatives { my $X = shift; return unless defined($X); return unless ref($X); my $Y = shift; return unless defined($Y); return unless ref($Y); my $num_x = @$X; my $num_y = @$Y; return unless $num_x == $num_y; if ( $num_x < 2 ) { return (); } # Set up the derivative array. my @deriv; # If there for two input points, use a straight line as the derivative. if ( $num_x == 2 ) { $deriv[0] = ($Y->[1] - $Y->[0]) / ($X->[1] - $X->[0]); $deriv[1] = $deriv[0]; return @deriv; } # Calculate the derivatives for the interior points. This loop uses # a total of 6 points to calculate the derivative at any one # point. And when the loop moves along in increasing array # position, the same data point is used three times. So instead of # reading the correct value from the array three times, just shift # the values down by copying them from one variable to the next. my $xi; my $xj = $X->[0]; my $xk = $X->[1]; my $yi; my $yj = $Y->[0]; my $yk = $Y->[1]; for (my $i=1; $i<$num_x-1; ++$i) { $xi = $xj; $xj = $xk; $xk = $X->[$i+1]; $yi = $yj; $yj = $yk; $yk = $Y->[$i+1]; my $r1 = ($xk - $xj)*($xk - $xj) + ($yk - $yj)*($yk - $yj); my $r2 = ($xj - $xi)*($xj - $xi) + ($yj - $yi)*($yj - $yi); $deriv[$i] = ( ($yj - $yi)*$r1 + ($yk - $yj)*$r2 ) / ( ($xj - $xi)*$r1 + ($xk - $xj)*$r2 ); } # Calculate the derivative at the first point, (x(0),y(0)). my $i = 0; my $j = 1; my $slope = ($Y->[$j] - $Y->[$i])/($X->[$j] - $X->[$i]); if ( (($slope >= 0) && ($slope >= $deriv[$j])) || (($slope <= 0) && ($slope <= $deriv[$j])) ) { $deriv[0] = 2*$slope - $deriv[1]; } else { $deriv[0] = $slope + (abs($slope) * ($slope - $deriv[1])) / (abs($slope) + abs($slope - $deriv[1]) ); } # Calculate the derivative at the last point. $i = $num_x - 2; $j = $num_x - 1; $slope = ($Y->[$j] - $Y->[$i])/($X->[$j] - $X->[$i]); if ( (($slope >= 0) && ($slope >= $deriv[$i])) || (($slope <= 0) && ($slope <= $deriv[$i])) ) { $deriv[$j] = 2*$slope - $deriv[$i]; } else { $deriv[$j] = $slope + (abs($slope) * ($slope - $deriv[$i])) / (abs($slope) + abs($slope - $deriv[$i]) ); } @deriv; } sub constant_interpolate { my $x = shift; return unless defined($x); my $X = shift; return unless defined($X); return unless ref($X); my $Y = shift; return unless defined($Y); return unless ref($Y); my $num_x = @$X; my $num_y = @$Y; return unless $num_x == $num_y; # Find where the point to be interpolated lies in the input sequence. # If the x value lies outside of the X sequence, use the value at either # the beginning or the end of the sequence. my $j = interval_search($x, $X); if ( $j < 0 ) { $j = 0; } elsif ( $j > $num_x - 1 ) { $j = $num_x - 1; } # Return the Y value at the point. $Y->[$j]; } sub linear_interpolate { my $x = shift; return unless defined($x); my $X = shift; return unless defined($X); return unless ref($X); my $Y = shift; return unless defined($Y); return unless ref($Y); my $num_x = @$X; my $num_y = @$Y; return unless $num_x == $num_y; # Find where the point to be interpolated lies in the input sequence. # If the point lies outside, then coerce the index value to be legal for # the routine to work. Remember, this is only an interpreter, not an # extrapolator. my $j = interval_search($x, $X); if ( $j < 0 ) { $j = 0; } elsif ( $j >= $num_x - 1 ) { $j = $num_x - 2; } my $k = $j + 1; # Calculate the linear slope between the two points. my $dy = ($Y->[$k] - $Y->[$j]) / ($X->[$k] - $X->[$j]); # Use the straight line between the two points to interpolate. my $y = $dy*($x - $X->[$j]) + $Y->[$j]; return wantarray ? ($y, $dy) : $y; } sub robust_interpolate { my $x = shift; return unless defined($x); my $X = shift; return unless defined($X); return unless ref($X); my $Y = shift; return unless defined($Y); return unless ref($Y); my $num_x = @$X; my $num_y = @$Y; return unless $num_x == $num_y; # Calculate the derivative if it wasn't passed in. my $dY = shift; unless (defined($dY) and ref($dY)) { $dY = [ derivatives($X, $Y) ]; } # Find where the point to be interpolated lies in the input # sequence. If the point lies outside, then coerce the index value # to be legal for the routine to work. Remember, this is only an # interpreter, not an extrapolator. my $j = interval_search($x, $X); if ( $j < 0 ) { $j = 0; } elsif ( $j >= $num_x - 1 ) { $j = $num_x - 2; } my $k = $j + 1; # Calculate a few variables that will be used frequently. my $xj = $X->[$j]; my $xk = $X->[$k]; my $yj = $Y->[$j]; my $yk = $Y->[$k]; my $slope = ($yk - $yj) / ($xk - $xj); my $y0 = $yj + $slope * ($x - $xj); my $dely0 = $yj + $dY->[$j] * ($x - $xj) - $y0; my $dely1 = $yk + $dY->[$k] * ($x - $xk) - $y0; # Calculate the derivatives of the three variables above with respest # to x. my $d_y0 = $slope; my $d_dely0 = $dY->[$j] - $d_y0; my $d_dely1 = $dY->[$k] - $d_y0; # Calculate the interpolated y and dy values. my $dely_sign = $dely0*$dely1; my $y; my $dy; if ($dely_sign == 0) { $y = $y0; $dy = $d_y0; return wantarray ? ($y0, $d_y0) : $y0; } my $dely_sum = $dely0 + $dely1; if ($dely_sign > 0) { $y = $y0 + $dely_sign/$dely_sum; $dy = $d_y0 + ($dely_sum*($dely0*$d_dely1 + $d_dely0*$dely1) - $dely_sign*($d_dely0 + $d_dely1)) / ($dely_sum*$dely_sum); } else { my $x_tmp = 2*$x - $xj - $xk; $y = $y0 + $dely_sign*$x_tmp/(($dely0 - $dely1)*($xk - $xj)); $dy = $d_y0 + (($dely0 - $dely1)*($xk - $xj)* ($d_dely0*$dely1*$x_tmp + $dely0*$d_dely1*$x_tmp + $dely_sign*2) - $dely_sign*$x_tmp* (($xk - $xj)*($d_dely0 - $d_dely1))) / (($dely0 - $dely1)*($dely0 - $dely1)*($xk - $xj)*($xk - $xj)); } return wantarray ? ($y, $dy) : $y; } sub degenerate { my ($x1, $y1, $dy1, $x2, $y2, $dy2) = @_; my $slope = ($y2 - $y1)/($x2 - $x1); (sleep == $dy1) && ($dy1 != $dy2); } 1; __END__ =pod =head1 NAME Math::Interpolate - Interpolate the value Y from X using a list of (X, Y) pairs =head1 SYNOPSIS use Math::Interpolate qw(derivatives constant_interpolate linear_interpolate robust_interpolate); my @x = (1..5); my @y = (5, 10, 13, -4.5, 3); my @dy = derivatives(\@x, \@y); my ($l_y, $l_dy) = linear_interpolate(3.4, \@x, \@y); my ($r_y, $r_dy) = robust_interpolate(3.4, \@x, \@y); ($r_y, $r_dy) = robust_interpolate(3.4, \@x, \@y, [-2, 3, 4, -1, 4]); =head1 DESCRIPTION =head1 SUBROUTINES =over 4 =item B I I Given a reference to an array of x values in I and a reference to an array of y values in I, return an array of reasonable derivatives. The I values are presumed to be sorted in increasing numerical order. If there is an error in the input, such as I and I containing a different number of elements, then the subroutine returns an empty list in list context, an undefined value in scalar context, or nothing in a void context. =item B I I I Given a reference to an array of x values in I and a reference to an array of y values in I, return the y value associated with the first x value less than or equal to I. In other words, if I->[i] <= I < I->[i+1] then return I->[i] If I is less than I->[0], then return I->[0]. If I is greater than I[-1], then return I->[-1]. If there is an error in the input, such as I and I containing a different number of elements, then the subroutine returns an empty list in list context, an undefined value in scalar context, or nothing in a void context. =item B I I I Given a reference to an array of x values in I and a reference to an array of y values in I, calculate the interpolated value y that corresponds to the value I. The returned value y lies on the straight line between the two points surrounding I. If lies outside of the range of values spanned by I then a linear extrapolation will be done. In an array context, I will return an array containing the y value and and slope between the two nearest surrounding points. If there is an error in the input, such as I and I containing a different number of elements, then the subroutine returns an empty list in list context, an undefined value in scalar context, or nothing in a void context. =item B I I I [I] Given a reference to an array of x values in I and a reference to an array of y values in I, calculate the interpolated value y that corresponds to the value I. The interpolated curve generated by I is smooth and even the derivatives of the curve are smooth with only a few exceptions. The returned value y lies on the curve between the two points surrounding I. If lies outside of the range of values spanned by I then a linear extrapolation will be done. In an array context, I will return an array containing the y value and and slope between the two nearest surrounding points. If there is an error in the input, such as I and I containing a different number of elements, then the subroutine returns an empty list in list context, an undefined value in scalar context, or nothing in a void context. =back 4 =head1 AUTHOR Blair Zajac . =head1 COPYRIGHT Copyright (c) 1998 by Blair Zajac. =cut