

use strict;
use Carp;

=head1 NAME

	Hermite.pm

=head1 SYNPOSIS

      Usage:
                $obj = new Hermit(\@x_values, \y_valuses \@yp_values);

		#get and set methods
                $ra_x_values = $obj -> ra_x(\@x_values);
		$ra_y_values = $obj -> ra_y;
		$ra_yp_values = $obj -> ra_yp;

		$obj -> initialize;           # calculates the approximation

		#get methods
		$rf_function                  = $obj -> rf_f;
		$rf_function_derivative       = $obj -> rf_fp;
		$rf_function_2nd_derivative   = $obj -> rf_fpp;

		$rh_critical_points           =$obj -> rh_critical_points
		$rh_inflection_points         =$obj -> rh_inflection_points




=head1 DESCRIPTION

This module defines an object containing a Hermite spline approximation to a function.
  The approximation
consists of a piecewise cubic polynomial which agrees with the original
function and its first derivative at
the node points.

This is useful for creating on the fly graphics.  Care must be taken to use a small
number of points spaced reasonably far apart, preferably
points with alternating slopes, since this will minimize
the number of extraneous critical points
introduced.  Too many points will introduce many small variations and
a large number of extraneous critical points.

There are even more extraneous inflections points.
This parameter is probably not very useful.   A different
approximation scheme needs to be used.

=cut


# an object that contains a cubic hermite spline

package Hermite;


sub new {
    my $class = shift;
    my ($ra_x,$ra_y,$ra_yp) = @_;
    my $self = {
	         ra_x				=>  [],
		 ra_y				=>  [],
		 ra_yp				=>  [],
		 rf_f				=>  0,   # refers to a subroutine for the function
		 rf_fp				=>  0,   # refers to a subroutine for the derivative of the function
		 rf_fpp				=>  0,   # refers to a subroutine for the second derivative of the function
		 rh_critical_points		=>  {},
		 rh_inflection_points		=>  {},
		 rh_maximum_points		=>  {},
		 rh_minimum_points		=>  {},
    };
    bless $self, $class;
    $self->define($ra_x,$ra_y,$ra_yp) if ref($ra_x) eq 'ARRAY' and ref($ra_y) eq 'ARRAY' and ref($ra_yp) eq 'ARRAY';
    $self;
}

sub define{
    my $self   = shift;
    my $ra_x  = shift;
    my $ra_y  = shift;
    my $ra_yp = shift;
    $self->ra_x($ra_x);
    $self->ra_y($ra_y);
    $self->ra_yp($ra_yp);

    $self -> initialize();
    ($self->{x_ref},$self->{y_ref}, $self->{yp_ref} )
}

sub initialize {
    my $self = shift;
    # define the functions rf_f

    $self -> {rf_f} = hermite_spline($self -> {ra_x}, $self -> {ra_y}, $self -> {ra_yp} );
#     # define the function  rf_fp
     $self -> {rf_fp} = hermite_spline_p($self -> {ra_x}, $self -> {ra_y}, $self -> {ra_yp} );
#     # define the function  rf_fp
     $self -> {rf_fpp} = hermite_spline_pp($self -> {ra_x}, $self -> {ra_y}, $self -> {ra_yp} );
#     # define the critical points
     %{$self -> {rh_critical_points}} = critical_points($self -> {ra_x}, $self -> {ra_y}, $self -> {ra_yp},
                                                     $self -> { rf_f },
						     );
#     # define the inflection_points
     %{$self -> {rh_inflection_points}} = inflection_points($self -> {ra_x}, $self -> {ra_y}, $self -> {ra_yp} ,
                                                     $self -> { rf_f },
						     );
#     # define the maximum points

    # define the minimum points

}

# fix up these accesses to do more checking
sub ra_x {
    my $self = shift;
    my $ra_x  = shift;
    @{$self->{ra_x}} = @$ra_x if ref($ra_x) eq 'ARRAY';
    $self->{ra_x};

}

sub ra_y {
    my $self = shift;
    my $ra_y  = shift;
    @{$self->{ra_y}} = @$ra_y if ref($ra_y) eq 'ARRAY';
    $self->{ra_y};
}

sub ra_yp {
    my $self = shift;
    my $ra_yp  = shift;
    @{$self->{ra_yp}} = @$ra_yp if ref($ra_yp) eq 'ARRAY';
    $self->{ra_yp};
}

sub rf_f {
    my $self = shift;
    my $rf_f =shift;
    $self ->{rf_f} = $rf_f if defined( $rf_f );
    $self ->{rf_f};
}
sub rf_fp {
    my $self = shift;
    my $rf_fp =shift;
    $self ->{rf_fp} = $rf_fp if defined( $rf_fp );
    $self ->{rf_fp};
}
sub rf_fpp {
    my $self = shift;
    my $rf_fpp =shift;
    $self ->{rf_fpp} = $rf_fpp if defined( $rf_fpp );
    $self ->{rf_fpp};
}

sub rh_critical_points {
    my $self = shift;
    $self -> { rh_critical_points};
}

sub rh_inflection_points {
    my $self = shift;
    $self -> { rh_inflection_points};
}

##Internal subroutines


sub critical_points{
    my $ra_x = shift;
    my $ra_y =shift;
    my $ra_yp = shift;
    my $rf_hermite_fun =shift;
    my %critical_points = ();
    my $last_index = @$ra_x -2;
    foreach my $i (0 .. $last_index ) {
        internal_critical_points($ra_x->[$i],   $ra_y->[$i],   $ra_yp->[$i],
	                         $ra_x->[$i+1], $ra_y->[$i+1], $ra_yp->[$i+1],
				 \%critical_points,$rf_hermite_fun);
    }
    %critical_points;
}

sub inflection_points{
    my $ra_x = shift;
    my $ra_y =shift;
    my $ra_yp = shift;
    my $rf_hermite_fun =shift;
    my %inflection_points = ();
    my $last_index = @$ra_x -2;
    foreach my $i (0 .. $last_index ) {
        internal_inflection_points($ra_x->[$i],   $ra_y->[$i],   $ra_yp->[$i],
	                         $ra_x->[$i+1], $ra_y->[$i+1], $ra_yp->[$i+1],
				 \%inflection_points,$rf_hermite_fun);
    }
    %inflection_points;
}
sub internal_critical_points{
    my ($x0,$l,$lp, $x1,$r,$rp, $rh_roots ,$rf_function) = @_;
     #data for one segment of the hermite spline

     # coefficients for the approximating polynomial

     my @a = (   $l,
                  $lp,
		  -$lp/2 + (3*(-$lp - 2*($l - $r) - $rp))/2 + $rp/2,
		  $lp + 2*($l - $r) + $rp
               );

    my ($root1, $root2, $z1,$z2);
    if ( $a[3] == 0 ) {
	if ( $a[2] == 0 ) {
	} else {
	    $root1 = -$a[1]/( 2*$a[2] );
	    if ( 0 <= $root1 and $root1 < 1) {
		$z1 = $root1*($x1-$x0) + $x0;

	        $rh_roots -> {$z1} = &$rf_function($z1);
	    }
	}
   } else {
	my $discriminent = (4*$a[2]**2 - 12*$a[1]*$a[3]);


	if ( $discriminent >= 0 ) {
	    $discriminent = $discriminent**0.5;
	    $root1 = (-2*$a[2] - $discriminent )/( 6*$a[3] );
	    $root2 = (-2*$a[2] + $discriminent )/( 6*$a[3] );
	    $z1 = $root1*($x1-$x0) + $x0;
	    $z2 = $root2*($x1-$x0) + $x0;
	    $rh_roots -> {$z1} = &$rf_function($z1) if  0 <= $root1 and $root1 < 1;
	    $rh_roots -> {$z2} = &$rf_function($z1) if  0 <= $root2 and $root2 < 1;
	}
    }
}

sub internal_inflection_points {
    my ($x0,$l,$lp,,$x1,$r,$rp,$rh_roots,$rf_function) = @_;
     #data for one segment of the hermite spline

     # coefficients for the approximating polynomial

     my @a = (   $l,
                  $lp,
		  -$lp/2 + (3*(-$lp - 2*($l - $r) - $rp))/2 + $rp/2,
		  $lp + 2*($l - $r) + $rp
               );
    if ($a[3] == 0 ) {
    } else {
	my $root1 = -$a[2]/( 3*$a[3] );
	my $z1 = $root1*($x1-$x0) + $x0;
	$rh_roots -> {$z1} = &$rf_function($z1) if  0 <= $root1 and $root1 < 1;
    }

}



sub cubic_hermite {
    my ( $x0, $y0,$yp0, $x1, $y1, $yp1 ) = @_;
    my @a;
    my $width = $x1 - $x0;
    $yp0 = $yp0*$width;  # normalize to unit width
    $yp1 = $yp1*$width;

    $a[0] = $y0;
    $a[1] = $yp0;
    $a[2] = -3*$y0 - 2*$yp0 +3*$y1 -$yp1;
    $a[3] = 2*$y0 + $yp0 - 2*$y1 +$yp1;

    my $f = sub {
                        my $x = shift;
                        #normalize to unit width
			$x = ( $x - $x0 )/$width;
			( ($a[3]*$x + $a[2]) * $x + $a[1] )*$x + $a[0];

                };
    $f;
}

sub cubic_hermite_p {
    my ( $x0, $y0,$yp0, $x1, $y1, $yp1 )=@_;
    my @a;
    my $width = $x1 - $x0;
    $yp0 = $yp0*$width;  # normalize to unit width
    $yp1 = $yp1*$width;

    $a[0] = $y0;
    $a[1] = $yp0;
    $a[2] = -3*$y0 - 2*$yp0 +3*$y1 -$yp1;
    $a[3] = 2*$y0 + $yp0 - 2*$y1 +$yp1;

    my $fp = sub {
                        my $x = shift;
                        #normalize to unit width
			$x = ( $x - $x0 )/$width;
			( (3*$a[3]*$x + 2*$a[2]) * $x + $a[1] )/$width ;
                };



     $fp;

}

sub cubic_hermite_pp {
    my ( $x0, $y0,$yp0, $x1, $y1, $yp1 ) = @_;
    my @a;
    my $width = $x1 - $x0;
    $yp0 = $yp0*$width;  # normalize to unit width
    $yp1 = $yp1*$width;

    $a[0] = $y0;
    $a[1] = $yp0;
    $a[2] = -3*$y0 - 2*$yp0 +3*$y1 -$yp1;
    $a[3] = 2*$y0 + $yp0 - 2*$y1 +$yp1;



    my $fpp = sub {
                        my $x = shift;
                        #normalize to unit width
			$x = ( $x - $x0 )/$width;
			 (6*$a[3]*$x + 2*$a[2])/$width**2 ;
                };

    $fpp;
}



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, cubic_hermite($x0, $y0, $yp0 , $x1, $y1 , $yp1);
		$x0  = $x1;
		$y0  = $y1;
		$yp0 = $yp1;
	}


	my $hermite_spline_function = sub {
		my $x = shift;
		my $y;
		my $fun;
		my @xvals = @$xref;
		my @fns = @polys;
		return &{$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_function;
}

sub hermite_spline_p {
	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, cubic_hermite_p($x0, $y0, $yp0 , $x1, $y1 , $yp1);
		$x0  = $x1;
		$y0  = $y1;
		$yp0 = $yp1;
	}


	my $hermite_spline_function_p = 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_function_p;
}
sub hermite_spline_pp {
	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, cubic_hermite_pp($x0, $y0, $yp0 , $x1, $y1 , $yp1);
		$x0  = $x1;
		$y0  = $y1;
		$yp0 = $yp1;
	}


	my $hermite_spline_function_pp = 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_function_pp;
}


1;
