File Coverage

blib/lib/Math/CPWLF.pm
Criterion Covered Total %
statement 153 153 100.0
branch 56 58 96.5
condition 10 12 83.3
subroutine 23 23 100.0
pod 2 2 100.0
total 244 248 98.3


line stmt bran cond sub pod time code
1             package Math::CPWLF;
2              
3 5     5   142024 use warnings;
  5         14  
  5         181  
4 5     5   111 use strict;
  5         16  
  5         180  
5              
6 5     5   29 use Carp;
  5         14  
  5         376  
7 5     5   5072 use Want;
  5         37862  
  5         380  
8 5     5   372 use List::Util;
  5         9  
  5         877  
9              
10             use overload
11             fallback => 1,
12             '&{}' => sub
13             {
14 473     473   352981 my $self = $_[0];
15 473         1319 return _top_interp_closure( $self, $self->{_opts} )
16 5     5   10854 };
  5         6654  
  5         54  
17            
18             =head1 NAME
19              
20             Math::CPWLF - interpolation using nested continuous piece-wise linear functions
21              
22             =head1 VERSION
23              
24             Version 0.15
25              
26             =cut
27              
28             our $VERSION = '0.15';
29              
30             =head1 SYNOPSIS
31              
32             C provides an interface for defining continuous piece-wise linear
33             functions by setting knots with x,y pairs.
34              
35             use Math::CPWLF;
36            
37             $func = Math::CPWLF->new;
38              
39             $func->knot( 0 => 0 ); ## set the knot at f(0) equal to 0
40             $func->knot( 1 => 2 ); ## set the knot at f(1) equal to 2
41            
42             $y = $func->( 0.5 ); ## interpolate f(0.5) ($y == 1)
43            
44             Functions can be used in multiple dimensions, by specifying a C
45             object as the y value of a knot.
46              
47             $nested_func = Math::CPWLF->new;
48              
49             $nested_func->knot( 0 => 0 );
50             $nested_func->knot( 1 => 3 );
51            
52             $func->knot( 2 => $nested_func );
53            
54             $deep_y = $func->( 1.5 )( 0.5 ); ## $deep_y == 1.75
55            
56             As a convenience, you can specify arbitrarily deep knots by passing more than
57             two values two the C method.
58              
59             $func->knot( 2, 2 => 4 ); ## same as $nested_func->( 2 => 4);
60              
61             If any of the intermediate knots do not exist they will be autovivified as
62             C objects, much like perl hashes.
63              
64             $func->knot( 3, 2 => 4 ); ## autovivify top level f(3)
65              
66             =head1 FUNCTIONS
67              
68             =head2 new
69              
70             Construct a new C function with no knots, and the default out of
71             bounds behavior.
72              
73             my $func = Math::CPWLF->new;
74            
75             Optional parameters:
76              
77             =over 4
78              
79             =item * oob
80              
81             The C parameter controls how a function behaves when a given x value is out
82             of bounds of the current minimum and maximum knots. If a function defines an
83             C method in its constructor, that method is also used for any nested
84             functions that were not explicitly constructed with their own C methods.
85              
86             =over 4
87              
88             =item * C - Throw an exception (default).
89              
90             =item * C - Perform a linear extrapolation using the two nearest knots.
91              
92             =item * C - Return the y value of the nearest knot.
93              
94             =item * C - Return undef.
95              
96             =back
97              
98             Construct an instance that returns C or empty list when the requested x
99             is out of bounds:
100              
101             my $func = Math::CPWLF->new( oob => 'undef' );
102              
103             =back
104              
105             =cut
106              
107             sub new
108             {
109 494     494 1 8451 my $self = bless {}, shift();
110 494         850 my %opts = @_;
111 494         1235 $self->{_opts} = \%opts;
112 494         1304 return $self;
113             }
114            
115             =head2 knot
116              
117             This instance method adds a knot with the given x,y values.
118              
119             $func->knot( $x => $y );
120            
121             Knots can be specified at arbitrary depth and intermediate knots will autovivify
122             as needed. There are two alternate syntaxes for setting deep knots. The first
123             involves passing 3 or more values to the C call, where the last value
124             is the y value and the other values are the depth-ordered x values:
125              
126             $func->knot( $x1, $x2, $x3 => $y );
127            
128             The other syntax is a bit more hash-like in that it separates the x values. Note
129             that it starts with invoking the C method with no arguments.
130              
131             $func->knot->($x1)($x2)( $x3 => $y );
132              
133             =cut
134              
135             sub knot
136             {
137 6686     6686 1 18297 my $self = shift @_;
138            
139 6686         9025 delete $self->{_x_vals_order};
140              
141             ## caller intends to use hash-like multi-dimensional syntax
142             ## $f->knot->(1)(2)( 3 => 4 );
143 6686 100       21926 if ( @_ == 0 )
    100          
    100          
    50          
144             {
145             return sub
146             {
147 627     627   1265 $self->knot( @_ );
148 627         2295 };
149             }
150             ## caller is in the middle of using hash-like multi-dimensional syntax
151             elsif ( @_ == 1 )
152             {
153 1877         2035 my $x = shift;
154              
155 1877 100 66     8010 if ( ! defined $self->{_data}{$x} ||
156             ! ref $self->{_data}{$x} )
157             {
158 155         386 $self->{_data}{$x} = ( ref $self )->new;
159             }
160              
161             return sub
162             {
163 1877     1877   4531 $self->{_data}{$x}->knot( @_ );
164 1877         7490 };
165             }
166             ## args are an x,y pair
167             elsif ( @_ == 2 )
168             {
169 2285         2477 my ( $x, $y ) = @_;
170 2285         2395 $x += 0;
171 2285         5201 $self->{_data}{$x} = $y;
172             }
173             ## caller is using bulk multi-dimensional syntax
174             ## $f->knot( 1, 2, 3 => 4 );
175             elsif ( @_ > 2 )
176             {
177 1897         2164 my $x = shift;
178            
179 1897         1792 $x += 0;
180            
181 1897 100 66     7548 if ( ! defined $self->{_data}{$x} || ! ref $self->{_data}{$x} )
182             {
183 163         402 $self->{_data}{$x} = ( ref $self )->new;
184             }
185            
186 1897         4454 $self->{_data}{$x}->knot(@_);
187            
188             }
189              
190 4182         8130 return $self;
191             }
192              
193             ## - solves the first dimension lookup, or
194             ## - returns first dimension closure as needed
195             sub _top_interp_closure
196             {
197 473     473   676 my ( $func, $opts ) = @_;
198            
199             my $interp = sub
200             {
201 473     473   672 my ( $x_given ) = @_;
202              
203 473         659 $x_given += 0;
204              
205 473         1142 my $node = $func->_make_node($x_given, $opts);
206            
207 465 100       1360 return _nada() if ! defined $node;
208              
209 462         861 my @slice = ( $node );
210 462         1510 my @tree = ( \@slice );
211              
212 462 100 100     2824 return ref $node->{y_dn} || ref $node->{y_up}
213             ? _nested_interp_closure( \@tree, $opts )
214             : _reduce_tree( \@tree )
215 473         3012 };
216            
217             }
218              
219             ## - solves the 2+ dimension lookups, or
220             ## - returns 2+ dimension closures as needed
221             sub _nested_interp_closure
222             {
223 76     76   106 my ( $tree, $opts ) = @_;
224            
225             my $interp = sub
226             {
227 67     67   81 my ($x_given) = @_;
228            
229 67         73 $x_given += 0;
230            
231 67         67 my @slice;
232             my $make_closure;
233            
234 67         72 for my $node ( @{ $tree->[-1] } )
  67         122  
235             {
236            
237 120         154 for my $y_pos ( 'y_dn', 'y_up' )
238             {
239            
240 236 100       534 next if ! ref $node->{$y_pos};
241            
242 229         594 my $new_node = $node->{$y_pos}->_make_node($x_given, $opts);
243            
244 227 100       461 return _nada() if ! defined $new_node;
245            
246 225   100     718 $make_closure = ref $new_node->{y_dn} || ref $new_node->{y_up};
247            
248 225         458 $new_node->{into} = \$node->{$y_pos};
249            
250 225         588 push @slice, $new_node;
251              
252             }
253              
254             }
255            
256 63         82 push @{ $tree }, \@slice;
  63         96  
257            
258 63 100       155 return $make_closure ? _nested_interp_closure( $tree, $opts )
259             : _reduce_tree( $tree )
260            
261 76         303 };
262              
263 76         320 return $interp;
264             }
265              
266             ## converts the final tree of curried line segments and x values to the final
267             ## y value
268             sub _reduce_tree
269             {
270 449     449   1279 my ($tree) = @_;
271              
272 449         540 for my $slice ( reverse @{ $tree } )
  449         822  
273             {
274            
275 503         903 for my $node ( @{ $slice } )
  503         923  
276             {
277              
278 650         770 my @line = grep defined, @{ $node }{ qw/ x_dn x_up y_dn y_up / };
  650         3293  
279            
280 650         1536 my $y_given = _mx_plus_b( $node->{x_given}, @line );
281            
282 650 100       5924 return $y_given if ! $node->{into};
283              
284 201         187 ${ $node->{into} } = $y_given;
  201         522  
285              
286             }
287              
288             }
289            
290             }
291              
292             ## used to handle 'undef' oob exceptions
293             ## - returns a reference to itself in CODEREF context
294             ## - else returns undef
295             sub _nada
296             {
297 6 100   6   73 return want('CODE') ? \&_nada : ();
298             }
299            
300             {
301              
302             my $default_opts =
303             {
304             oob => 'die',
305             };
306              
307             ## - merges the options, priority from high to low is:
308             ## - object
309             ## - inherited
310             ## - defaults
311             sub _merge_opts
312             {
313 34     34   568 my ($self, $inherited_opts) = @_;
314            
315 34         30 my %opts;
316            
317 34         62 for my $opts ( $self->{_opts}, $inherited_opts, $default_opts )
318             {
319 102         102 for my $opt ( keys %{ $opts } )
  102         184  
320             {
321 85 100       219 next if defined $opts{$opt};
322 34         111 $opts{$opt} = $opts->{$opt};
323             }
324             }
325            
326 34         70 return \%opts;
327             }
328            
329             }
330              
331             ## - locate the neighboring x and y pairs to the given x values
332             ## - handles oob exceptions
333             ## - handles direct hits
334             ## - handles empty functions
335             sub _make_node
336             {
337 702     702   1029 my ($self, $x, $opts) = @_;
338            
339 702 100       2175 if ( ! exists $self->{_x_vals_order} )
340             {
341 118         240 $self->_order_x_vals;
342 118         238 $self->_index_x_vals;
343             }
344            
345 702 100       773 if ( ! @{ $self->{_x_vals_order} } )
  702         1874  
346             {
347 1         9 die "Error: cannot interpolate with no knots";
348             }
349              
350 701         870 my ( $x_dn_i, $x_up_i, $oob );
351            
352 701 100       5311 if ( exists $self->{_x_vals_index}{$x} )
    100          
    100          
353             {
354 359         642 $x_dn_i = $self->{_x_vals_index}{$x};
355 359         492 $x_up_i = $x_dn_i;
356             }
357             elsif ( $x < $self->{_x_vals_order}[0] )
358             {
359 5         9 $x_dn_i = 0;
360 5         5 $x_up_i = 0;
361 5         6 $oob = 1;
362             }
363             elsif ( $x > $self->{_x_vals_order}[-1] )
364             {
365 29         52 $x_dn_i = -1;
366 29         29 $x_up_i = -1;
367 29         32 $oob = 1;
368             }
369             else
370             {
371             ( $x_dn_i, $x_up_i ) = do
372 308         372 {
373 308         474 my $min = 0;
374 308         357 my $max = $#{ $self->{_x_vals_order} };
  308         557  
375 308         790 _binary_search( $self->{_x_vals_order}, $x, $min, $max );
376             };
377             }
378            
379 701 100       1799 if ( $oob )
380             {
381 34         62 my $merge_opts = $self->_merge_opts( $opts );
382 34 100       118 if ( $merge_opts->{oob} eq 'die' )
    100          
    100          
    100          
383             {
384 8         1015 Carp::confess "Error: given X ($x) was out of bounds of"
385             . " function min or max";
386             }
387             elsif ( $merge_opts->{oob} eq 'extrapolate' )
388             {
389 7 100       40 if ( $x < $self->{_x_vals_order}[0] )
    50          
390             {
391 1         2 $x_up_i = List::Util::min( $#{ $self->{_x_vals_order} }, $x_up_i + 1 );
  1         7  
392             }
393             elsif ( $x > $self->{_x_vals_order}[-1] )
394             {
395 6         27 $x_dn_i = List::Util::max( 0, $x_dn_i - 1 );
396             }
397             }
398             elsif ( $merge_opts->{oob} eq 'level' )
399             {
400             }
401             elsif ( $merge_opts->{oob} eq 'undef' )
402             {
403 5         12 return;
404             }
405             else
406             {
407 1         182 Carp::confess "Error: invalid oob option ($merge_opts->{oob})";
408             }
409             }
410              
411 687         1438 my $x_dn = $self->{_x_vals_order}[ $x_dn_i ];
412 687         1167 my $x_up = $self->{_x_vals_order}[ $x_up_i ];
413              
414 687         1526 my $y_dn = $self->{_data}{$x_dn};
415 687         1487 my $y_up = $self->{_data}{$x_up};
416            
417             return
418             {
419 687         3968 x_given => $x,
420             x_dn => $x_dn,
421             x_up => $x_up,
422             y_dn => $y_dn,
423             y_up => $y_up,
424             };
425             }
426              
427             ## converts a given x value and two points that define a line
428             ## to the corresponding y value
429             sub _mx_plus_b
430             {
431 650     650   1362 my ( $x, $x_dn, $x_up, $y_dn, $y_up ) = @_;
432            
433 650 100       1407 if ( $y_dn == $y_up )
434             {
435 385         727 return $y_dn;
436             }
437              
438 265         441 my $slope = ( $y_up - $y_dn ) / ( $x_up - $x_dn );
439 265         425 my $intercept = $y_up - ( $slope * $x_up );
440 265         438 my $y = $slope * $x + $intercept;
441              
442 265         701 return $y;
443             }
444              
445             ## vanilla binary search algorithm used to locate a given x value
446             ## that is within the defined range of the function
447             sub _binary_search
448             {
449 324     324   11657 my ( $array, $value, $min_index, $max_index ) = @_;
450            
451 324         433 my $range_max_index = $max_index - $min_index;
452            
453 324         808 while ( $range_max_index > 1 )
454             {
455              
456             ## size: 3 4 5 6 7 8 9 10 20
457             ## mid: 1 2 2 3 3 4 4 5 10
458 1743         2770 my $mid_index = $min_index + int( $range_max_index / 2 );
459            
460             ## value is inside the upper half
461 1743 100       3261 if ( $value > $array->[$mid_index] )
462             {
463 888         1039 $min_index = $mid_index;
464             }
465             ## value is inside the lower half
466             else
467             {
468 855         1007 $max_index = $mid_index;
469             }
470              
471 1743         3665 $range_max_index = $max_index - $min_index;
472              
473             }
474              
475 324 100       1141 return $range_max_index > -1
476             ? ( $min_index, $max_index )
477             : ( undef, undef );
478              
479             }
480            
481             ## - called on the first lookup after a knot has been set
482             ## - caches an array of ordered x values
483             sub _order_x_vals
484             {
485 118     118   139 my ( $self ) = @_;
486            
487 118         136 my @ordered_x_vals = sort { $a <=> $b } keys %{ $self->{_data} };
  1905         2162  
  118         706  
488            
489 118         359 $self->{_x_vals_order} = \@ordered_x_vals;
490             }
491              
492             ## - called on the first lookup after a knot has been set
493             ## - creates an index mapping knot x values to their ordered indexes
494             sub _index_x_vals
495             {
496 118     118   148 my ( $self ) = @_;
497              
498 118         186 delete $self->{_x_vals_index};
499 118         127 for my $i ( 0 .. $#{ $self->{_x_vals_order} } )
  118         320  
500             {
501 669         1561 $self->{_x_vals_index}{ $self->{_x_vals_order}[$i] } = $i;
502             }
503             }
504              
505             =head1 AUTHOR
506              
507             Dan Boorstein, C<< >>
508              
509             =head1 BUGS
510              
511             Please report any bugs or feature requests to C, or through
512             the web interface at L. I will be notified, and then you'll
513             automatically be notified of progress on your bug as I make changes.
514              
515             =head1 SUPPORT
516              
517             You can find documentation for this module with the perldoc command.
518              
519             perldoc Math::CPWLF
520              
521              
522             You can also look for information at:
523              
524             =over 4
525              
526             =item * RT: CPAN's request tracker
527              
528             L
529              
530             =item * AnnoCPAN: Annotated CPAN documentation
531              
532             L
533              
534             =item * CPAN Ratings
535              
536             L
537              
538             =item * Search CPAN
539              
540             L
541              
542             =back
543              
544              
545             =head1 ACKNOWLEDGEMENTS
546              
547              
548             =head1 COPYRIGHT & LICENSE
549              
550             Copyright 2009 Dan Boorstein.
551              
552             This program is free software; you can redistribute it and/or modify it
553             under the terms of either: the GNU General Public License as published
554             by the Free Software Foundation; or the Artistic License.
555              
556             See http://dev.perl.org/licenses/ for more information.
557              
558              
559             =cut
560              
561             1; # End of Math::CPWLF