File Coverage

blib/lib/Math/InterpolationCompiler.pm
Criterion Covered Total %
statement 88 91 96.7
branch 31 42 73.8
condition 5 9 55.5
subroutine 13 13 100.0
pod 2 4 50.0
total 139 159 87.4


line stmt bran cond sub pod time code
1             package Math::InterpolationCompiler;
2 1     1   529 use 5.006001;
  1         2  
  1         29  
3 1     1   406 use Moo 2;
  1         8539  
  1         4  
4 1     1   1475 use Types::Standard 1;
  1         44421  
  1         8  
5 1     1   368 use Carp;
  1         1  
  1         52  
6 1     1   3 use Exporter 'import';
  1         1  
  1         787  
7              
8             our @EXPORT_OK= qw( linear_clamp_fn linear_extrapolate_fn );
9              
10             our $VERSION= '0.002000';
11              
12             # ABSTRACT: Compile interpolations into perl coderefs
13              
14              
15             has domain => ( is => 'ro', isa => Types::Standard::ArrayRef, required => 1 );
16             has range => ( is => 'ro', isa => Types::Standard::ArrayRef, required => 1 );
17             has algorithm => ( is => 'ro', default => sub { 'linear' } );
18             has beyond_domain => ( is => 'ro', default => sub { 'clamp' } );
19             has perl_code => ( is => 'lazy' );
20             has fn => ( is => 'lazy' );
21             has sanitize => ( is => 'ro', default => sub { 1 } );
22              
23             sub BUILDARGS {
24 12     12 0 10894 my $self= shift;
25 12         44 my $args= $self->next::method(@_);
26 12 50 33     209 if ($args->{points} && !$args->{domain} && !$args->{range}) {
      33        
27 12         9 my (@domain, @range);
28 12 50       20 ref $args->{points} eq 'ARRAY'
29             or croak "points must be an arrayref";
30             # If points is an arrayref of arrayrefs, assume each point is a 2-element arrayref
31 12 100       25 if (ref $args->{points}[0]) {
32 8         7 for (@{ delete $args->{points} }) {
  8         17  
33 26         25 push @domain, $_->[0];
34 26         26 push @range, $_->[1];
35             }
36             }
37             # else assume points is an arrayref with the x/y in odd/even slots
38             else {
39 4         4 my $flip= 0;
40 4         3 for (@{ delete $args->{points} }) {
  4         7  
41 17 100       27 $flip++ & 1? (push @range, $_)
42             : (push @domain, $_);
43             }
44 4 100       111 !($flip & 1)
45             or croak "odd number of elements in points";
46             }
47 11         15 $args->{domain}= \@domain;
48 11         13 $args->{range}= \@range;
49             }
50 11         175 return $args;
51             }
52              
53             sub _sanitize_number_array {
54             return [
55 63 50       94 map {
56 22         25 defined $_ or croak " is not a number";
57 63         63 my $n= "$_";
58 63 100       281 $n =~ /^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?$/ or croak "$n is not a number";
59 62         84 $n
60 22     22   16 } @{ $_[0] }
61             ];
62             }
63              
64             sub BUILD {
65 11     11 0 52 my $self= shift;
66 0         0 @{ $self->domain } == @{ $self->range }
  11         22  
  11         24  
67 11 50       8 or croak "Domain and range differ in length (".@{ $self->domain }." != ".@{ $self->range }.")";
  0         0  
68 11 50       8 @{ $self->domain } > 1
  11         21  
69             or croak "Domain does not contain any intervals";
70 11         8 my $prev;
71 11 50       19 if ($self->sanitize) {
72 11         17 $self->{domain}= _sanitize_number_array($self->domain);
73 11         20 $self->{range}= _sanitize_number_array($self->range);
74             }
75 10         11 for (@{ $self->domain }) {
  10         13  
76 30 100 100     152 croak "Domain is not sorted in non-decreasing order"
77             if defined $prev && $_ < $prev;
78 29         28 $prev= $_;
79             }
80 9 50       157 $self->can("_gen_".$self->algorithm)
81             or croak "Unknown algorithm ".$self->algorithm;
82             }
83              
84             sub _build_perl_code {
85 9     9   303 my $self= shift;
86 9         22 my $method= $self->can("_gen_".$self->algorithm);
87 9         15 return $self->$method;
88             }
89              
90             sub _build_fn {
91 9     9   426 my $self= shift;
92 9 50       107 my $sub= eval $self->perl_code
93             or croak "Failed to build function: $@";
94 9         29 return $sub;
95             }
96              
97             # Create a linear interpolation
98             sub _gen_linear {
99 9     9   3 my $self= shift;
100 9         10 my $domain= $self->domain;
101 9         9 my $range= $self->range;
102 9         20 my @expressions;
103 9         16 for (my $i= 1; $i < @$domain; $i++) {
104             # skip discontinuities
105 18 100       40 next if $domain->[$i] == $domain->[$i-1];
106             # calculate slope and offset at x0
107 11         21 my $m= ($range->[$i] - $range->[$i-1]) / ($domain->[$i] - $domain->[$i-1]);
108 11         16 my $b= $range->[$i-1] - $domain->[$i-1] * $m;
109             # generate code
110 11         37 push @expressions, [ $domain->[$i-1], '$x * '.$m.' + '.$b ];
111             }
112 9 100       28 if ($self->beyond_domain eq 'clamp') {
    100          
    100          
    50          
113 4         13 unshift @expressions, [ undef, $range->[0] ];
114 4         8 push @expressions, [ $domain->[-1], $range->[-1] ];
115             }
116             elsif ($self->beyond_domain eq 'extrapolate') {
117             # just let the edge expressions do their thing
118             # ... unless there were discontinuities at the edges
119 2 50       5 unshift @expressions, [ undef, $range->[0] ]
120             if $domain->[0] == $domain->[1];
121 2 50       4 push @expressions, [ $domain->[-1], $range->[-1] ]
122             if $domain->[-1] == $domain->[-2];
123             }
124             elsif ($self->beyond_domain eq 'undef') {
125 1         2 unshift @expressions, [ undef, 'undef' ];
126 1         5 push @expressions, [ $domain->[-1], '$x == '.$domain->[-1].'? ('.$range->[-1].') : undef' ];
127             }
128             elsif ($self->beyond_domain eq 'die') {
129 2         6 unshift @expressions, [ undef, 'Carp::croak("argument out of bounds (<'.$domain->[0].')")' ];
130 2         6 push @expressions, [ $domain->[-1], '$x == '.$domain->[-1].'? ('.$range->[-1].') : Carp::croak("argument out of bounds (>'.$domain->[-1].')")' ];
131             }
132             else {
133 0         0 croak "Algorithm 'linear' does not support domain-edge '".$self->beyond_domain."'";
134             }
135             # Now tree-up the expressions
136 9         13 while (@expressions > 1) {
137 15         12 my ($i, $dest);
138 15         23 for ($i= 1, $dest= 0; $i < @expressions; $i+= 2) {
139 16         80 $expressions[$dest++]= [
140             $expressions[$i-1][0],
141             '$x < '.$expressions[$i][0]."?"
142             .' ('.$expressions[$i-1][1].")"
143             .':('.$expressions[$i][1].")"
144             ];
145             }
146             # odd number?
147 15 100       20 if ($i == @expressions) {
148 6         6 $expressions[$dest++]= $expressions[-1];
149             }
150             # truncate list
151 15         33 $#expressions= $dest-1;
152             }
153             # finally, wrap with function
154 9         663 return "sub {\n my \$x= shift;\n return ".$expressions[0][1].";\n}\n";
155             }
156              
157              
158             sub linear_clamp_fn {
159 1     1 1 954 Math::InterpolationCompiler->new(
160             algorithm => 'linear',
161             beyond_domain => 'clamp',
162             points => $_[0]
163             )->fn;
164             }
165              
166              
167             sub linear_extrapolate_fn {
168 1     1 1 59 Math::InterpolationCompiler->new(
169             algorithm => 'linear',
170             beyond_domain => 'extrapolate',
171             points => $_[0]
172             )->fn;
173             }
174              
175             1;
176              
177             __END__