package Math::GSL::Test; use base qw(Exporter); use base qw(DynaLoader); use strict; use warnings; use Test::More; use Math::GSL::Errno qw/:all/; use Math::GSL::Machine qw/:all/; use Math::GSL::Const qw/:all/; use Math::GSL::Sys qw/gsl_nan gsl_isnan gsl_isinf/; use Data::Dumper; use Carp qw/croak/; our @EXPORT = qw(); our @EXPORT_OK = qw( is_similar ok_similar ok_similar_relative is_similar_relative verify verify_results is_windows ok_status is_status_ok ); use constant GSL_IS_WINDOWS => ($^O =~ /MSWin32/i) ? 1 : 0 ; =head1 NAME Math::GSL::Test - Assertions and such =head1 SYNOPSIS use Math::GSL::Test qw/:all/; ok_similar($x,$y, $msg, $eps); =cut our %EXPORT_TAGS = ( all => \@EXPORT_OK,); sub _dump_result($) { my $r=shift; printf "result->err: %.18g\n", $r->{err}; printf "result->val: %.18g\n", $r->{val}; } =head2 is_windows() Returns true if current system is Windows-like. =cut sub is_windows() { GSL_IS_WINDOWS } =head2 is_similar($x,$y;$eps,$similarity_function) is_similar($x,$y); is_similar($x, $y, 1e-7); is_similar($x,$y, 1e-3, sub { ... } ); Return true if $x and $y are within $eps of each other, i.e. abs($x-$y) <= $eps If passed a code reference $similarity_function, it will pass $x and $y as parameters to it and will check to see if $similarity_function->($x,$y_) <= $eps The default value of $eps is 1e-8. Don't try sending anything to the Moon with this value... =cut sub is_similar { my ($x,$y, $eps, $similarity_function) = @_; local $Test::Builder::Level = $Test::Builder::Level + 1; $eps ||= 1e-8; if (ref $x eq 'ARRAY' && ref $y eq 'ARRAY') { if ( $#$x != $#$y ){ warn "is_similar(): items differ in length, $#$x != $#$y !!!"; return 0; } else { map { my $delta = (gsl_isnan($x->[$_]) or gsl_isnan($y->[$_])) ? gsl_nan() : abs($x->[$_] - $y->[$_]); if($delta > $eps){ warn "\n\tElements start differing at index $_, delta = $delta\n"; warn qq{\t\t\$x->[$_] = } . $x->[$_] . "\n"; warn qq{\t\t\$y->[$_] = } . $y->[$_] . "\n"; return 0; } } (0..$#$x); return 1; } } else { if( ref $similarity_function eq 'CODE') { $similarity_function->($x,$y) <= $eps ? return 1 : return 0; } elsif( defined $x && defined $y) { my $delta = (gsl_isnan($x) or gsl_isnan($y)) ? gsl_nan() : abs($x-$y); $delta > $eps ? warn qq{\t\t\$x=$x\n\t\t\$y=$y\n\t\tdelta=$delta\n} && return 0 : return 1; } else { return 0; } } } # this is a huge hack sub verify_results { my ($results,$class) = @_; # GSL uses a factor of 100 my $factor = 20; my $eps = 2048*$Math::GSL::Machine::GSL_DBL_EPSILON; # TOL3 my ($x,$res); local $Test::Builder::Level = $Test::Builder::Level + 1; croak "Usage: verify_results(%results, \$class)" unless $class; while (my($code,$expected)=each %$results){ my $r = Math::GSL::SF::gsl_sf_result_struct->new; my $status = eval qq{${class}::$code}; ok(0, qq{'$code' died} ) if !defined $status; if ( defined $r && $code =~ /_e\(.*\$r/) { $x = $r->{val}; $eps = $factor*$r->{err}; $res = abs($x-$expected); if ($ENV{DEBUG} ){ _dump_result($r); print "got $code = $x\n"; printf "expected : %.18g\n", $expected ; printf "difference : %.18g\n", $res; printf "unexpected error of %.18g\n", $res-$eps if ($res-$eps>0); } if (gsl_isnan($x)) { ok( gsl_isnan($expected), sprintf("'$expected'?='$x' (%16b ?= %16b)", $expected, $x) ); } elsif(gsl_isinf($x)) { ok( gsl_isinf($expected), sprintf("'$expected'?='$x' (%16b ?= %16b)", $expected, $x) ); } else { cmp_ok( $res,'<=', $eps, "$code ?= $x,\nres= +-$res, eps=$eps" ); } } } } =head2 verify( $results, $class) Takes a hash reference of key/value pairs where the keys are bits of code, which when evaluated should be within some tolerance of the value. For example: my $results = { 'gsl_cdf_ugaussian_P(2.0)' => [ 0.977250, 1e-5 ], 'gsl_cdf_ugaussian_Q(2.0)' => [ 0.022750, 1e-7 ], 'gsl_cdf_ugaussian_Pinv(0.977250)'=> [ 2.000000 ], }; verify($results, 'Math::GSL::CDF'); When no tolerance is given, a value of 1e-8 = 0.00000001 is used. One may use $GSL_NAN and $GSL_INF in comparisons and this routine will use the gsl_isnan() and gsl_isinf() routines to compare them. Note: Needing to pass in the class name is honky. This may change. =cut sub verify { my ($results,$class) = @_; local $Test::Builder::Level = $Test::Builder::Level + 1; croak "Usage: verify(%results, \$class)" unless $class; while (my($code,$result)=each %$results){ my $x = eval qq{${class}::$code}; ok(0, $@) if $@; croak(__PACKAGE__." : $result is not an array reference!") unless ref $result; my($expected,$eps)=@$result; $eps ||= 1e-8; if (gsl_isnan($x)) { ok( gsl_isnan($expected), "'$expected'?='$x'" ); } elsif(gsl_isinf($x)) { ok( gsl_isinf($expected), "'$expected'?='$x'" ); } else { my $res = abs($x - $expected); ok( $res <= $eps, "$code ?= $x,\nres= +-$res, eps=$eps" ); } } } =head2 ok_status( $got_status; $expected_status ) ok_status( $status ); # defaults to checking for $GSL_SUCCESS ok_status( $status, $GSL_ECONTINUE ); Pass a test if the GSL status codes match, with a default expected status of $GSL_SUCCESS. This function also stringifies the status codes into meaningful messages when it fails. =cut sub ok_status { my ($got, $expected, $msg ) = @_; local $Test::Builder::Level = $Test::Builder::Level + 1; $expected ||= $GSL_SUCCESS; my $strerr = $got ? gsl_strerror(int($got)) : ''; ok( defined $got && $got == $expected, $msg ? "$msg: " .$strerr : $strerr ); } =head2 is_status_ok($status) is_status_ok( $status ); Return true if $status is $GSL_SUCCESS, false otherwise. =cut sub is_status_ok { my ($got) = shift; local $Test::Builder::Level = $Test::Builder::Level + 1; return ( defined $got && $got == $GSL_SUCCESS ) ? 1 : 0 ; } =head2 ok_similar( $x, $y, $msg, $eps) ok_similar( $x, $y); ok_similar( $x, $y, 'reason'); ok_similar( $x, $y, 'reason', 1e-4); Pass a test if is_similar($x,$y,$msg,$eps) is true, otherwise fail. =cut sub ok_similar { my ($x,$y, $msg, $eps) = @_; local $Test::Builder::Level = $Test::Builder::Level + 1; ok(is_similar($x,$y,$eps), $msg); } =head2 is_similar_relative( $x, $y, $msg, $eps ) is_similar_relative($x, $y, $eps ); Returns true if $x has a relative error with repect to $y less than $eps. The current default for $eps is the same as is_similar(), i.e. 1e-8. This doesn't seem very useful. What should the default be? =cut sub is_similar_relative { my ($x,$y, $eps) = @_; local $Test::Builder::Level = $Test::Builder::Level + 1; return is_similar($x,$y,$eps, sub { abs( ($_[0] - $_[1])/abs($_[1]) ) } ); } =head2 ok_similar_relative( $x, $y, $msg, $eps ) ok_similar_relative($x, $y, $msg, $eps ); Pass a test if $x has a relative error with repect to $y less than $eps. =cut sub ok_similar_relative { my ($x,$y, $msg, $eps,) = @_; local $Test::Builder::Level = $Test::Builder::Level + 1; ok(is_similar_relative($x,$y,$eps), $msg ); } 1;