File Coverage

lib/ICC/Support/nPINT.pm
Criterion Covered Total %
statement 20 226 8.8
branch 2 100 2.0
condition 1 75 1.3
subroutine 6 22 27.2
pod 1 9 11.1
total 30 432 6.9


line stmt bran cond sub pod time code
1             package ICC::Support::nPINT;
2              
3 2     2   102816 use strict;
  2         12  
  2         59  
4 2     2   10 use Carp;
  2         5  
  2         195  
5              
6             our $VERSION = 0.72;
7              
8             # revised 2018-09-08
9             #
10             # Copyright © 2004-2019 by William B. Birkett
11              
12             # add development directory
13 2     2   526 use lib 'lib';
  2         684  
  2         11  
14              
15             # inherit from Shared
16 2     2   661 use parent qw(ICC::Shared);
  2         324  
  2         13  
17              
18             # support modules
19 2     2   150 use POSIX ();
  2         4  
  2         4979  
20              
21             =encoding utf-8
22              
23             n-channel polynomial interpolation engine (nPINT), inspired by Francois Lekien, Chad Coulliette and Jerry
24             Marsden, authors of "Tricubic interpolation in three dimensions" and "Tricubic Engine Technical Notes and
25             Full Matrix".
26              
27             create new nPINT object parameter hash structure:
28              
29             {'pda' => [polynomial_degree_array], 'coef' => [nPINT_coefficients]}
30              
31             the polynomial_degree_array contains the polynomial degree for each input channel, e.g. [3, 3, 3, 3] => 4
32             channels, each of degree 3
33              
34             the nPINT coefficients define the behavior of the model. they are normally obtained using the 'fit'
35             method, which uses linear least squares to fit the model to the supplied data. the number of rows in the
36             coefficient array is equal to the product of each channel's degree + 1, e.g. if the pda is [3, 3, 3, 3],
37             there are 256 coefficient rows. the number columns in the coefficient array is equal to the number of
38             output channels.
39              
40             =cut
41              
42             # optional usage forms:
43             # parameters: ()
44             # parameters: (ref_to_parameter_hash)
45             # parameters: (path_to_storable_file)
46             # returns: (object_reference)
47             sub new {
48              
49             # get object class
50 1     1 0 1010 my $class = shift();
51              
52             # create empty nPINT object
53 1         5 my $self = [
54             {}, # object header
55             [], # input polynomial degree array
56             [], # output coefficient array
57             undef # cached output coefficient array
58             ];
59              
60             # if one parameter, a hash reference
61 1 50 33     10 if (@_ == 1 && ref($_[0]) eq 'HASH') {
    50          
62            
63             # create new object from parameter hash
64 0         0 _new_from_hash($self, @_);
65            
66             # if any parameters
67             } elsif (@_) {
68            
69             # create new object from Storable file
70 0 0       0 _new_from_storable($self, @_) or carp("couldn't read Storable file: $_[0]");
71            
72             }
73              
74             # bless object
75 1         3 bless($self, $class);
76              
77             # return object reference
78 1         2 return($self);
79              
80             }
81              
82             # fit nPINT object to data
83             # uses LAPACK dgels function to perform a least-squares fit
84             # parameters: (ref_to_pda_array, ref_to_input_array, ref_to_output_array)
85             # returns: (dgels_info_value)
86             sub fit {
87              
88             # get parameters
89 0     0 0   my ($self, $pda, $in, $out) = @_;
90              
91             # local variables
92 0           my ($info, $cols, $coef);
93              
94             # get number of coefficients
95 0           $cols = ICC::Support::Lapack::xcols($pda);
96            
97             # verify input is an array or Math::Matrix object
98 0 0 0       (ref($in) eq 'ARRAY' || UNIVERSAL::isa($in, 'Math::Matrix')) or croak('improper input array type');
99              
100             # verify input structure
101 0 0 0       (ref($in->[0]) eq 'ARRAY' && ! ref($in->[0][0])) or croak('improper input array structure');
102              
103             # verify output is an array or Math::Matrix object
104 0 0 0       (ref($out) eq 'ARRAY' || UNIVERSAL::isa($out, 'Math::Matrix')) or croak('improper output array type');
105              
106             # verify output structure
107 0 0 0       (ref($out->[0]) eq 'ARRAY' && ! ref($out->[0][0])) or croak('improper output array structure');
108              
109             # verify array dimensions
110 0 0         ($#{$in} == $#{$out}) or croak('fit input and output arrays have different number of rows');
  0            
  0            
111              
112             # fit nPINT model
113 0           ($info, $coef) = ICC::Support::Lapack::nPINT_fit($pda, $in, $out);
114              
115             # check result
116 0 0         carp('fit failed - bad parameter when calling dgels') if ($info < 0);
117 0 0         carp('fit failed - A matrix not full rank') if ($info > 0);
118              
119             # copy pda to object
120 0           $self->[1] = [@{$pda}];
  0            
121              
122             # copy coefficients to object
123 0           $self->[2] = [@{$coef}[0 .. $cols - 1]];
  0            
124              
125             # update cache
126 0           $self->[3] = ICC::Support::Lapack::cache_2D($self->[2]);
127              
128             # return info value
129 0           return($info);
130              
131             }
132              
133             # get/set reference to header hash
134             # parameters: ([ref_to_new_hash])
135             # returns: (ref_to_hash)
136             sub header {
137              
138             # get object reference
139 0     0 0   my $self = shift();
140              
141             # if there are parameters
142 0 0         if (@_) {
143            
144             # if one parameter, a hash reference
145 0 0 0       if (@_ == 1 && ref($_[0]) eq 'HASH') {
146            
147             # set header to new hash
148 0           $self->[0] = {%{shift()}};
  0            
149            
150             } else {
151            
152             # error
153 0           croak('parameter must be a hash reference');
154            
155             }
156            
157             }
158              
159             # return reference
160 0           return($self->[0]);
161              
162             }
163              
164             # get/set reference to pda array
165             # parameters: ([ref_to_new_array])
166             # returns: (ref_to_array)
167             sub pda {
168              
169             # get object reference
170 0     0 0   my $self = shift();
171              
172             # if there are parameters
173 0 0         if (@_) {
174            
175             # if one parameter, an array reference
176 0 0 0       if (@_ == 1 && ref($_[0]) eq 'ARRAY') {
177            
178             # set CLUT to new array
179 0           $self->[1] = [@{shift()}];
  0            
180            
181             } else {
182            
183             # error
184 0           croak('parameter must be an array reference');
185            
186             }
187            
188             }
189              
190             # return reference
191 0           return($self->[1]);
192              
193             }
194              
195             # get/set reference to coefficient array
196             # parameters: ([ref_to_new_array])
197             # returns: (ref_to_array)
198             sub array {
199              
200             # get object reference
201 0     0 0   my $self = shift();
202              
203             # if there are parameters
204 0 0         if (@_) {
205            
206             # if one parameter, an array or a Math::Matrix object
207 0 0 0       if (@_ == 1 && (ref($_[0]) eq 'ARRAY' || UNIVERSAL::isa($_[0], 'Math::Matrix'))) {
      0        
208            
209             # verify array structure
210 0 0 0       (ref($_[0][0]) eq 'ARRAY' && ! ref($_[0][0][0])) or croak('improper coefficient array structure');
211            
212             # set CLUT to new array
213 0           $self->[2] = bless(Storable::dclone(shift()), 'Math::Matrix');
214            
215             # update cache
216 0           $self->[3] = ICC::Support::Lapack::cache_2D($self->[2]);
217            
218             } else {
219            
220             # error
221 0           croak('improper coefficient array type');
222            
223             }
224            
225             }
226              
227             # return reference
228 0           return($self->[2]);
229              
230             }
231              
232             # transform data
233             # nominal input range is (0 - 1)
234             # hash key 'ubox' enables unit box extrapolation
235             # supported input types:
236             # parameters: (list, [hash])
237             # parameters: (vector, [hash])
238             # parameters: (matrix, [hash])
239             # parameters: (Math::Matrix_object, [hash])
240             # parameters: (structure, [hash])
241             # returns: (same_type_as_input)
242             sub transform {
243              
244             # set hash value (0 or 1)
245 0 0   0 0   my $h = ref($_[-1]) eq 'HASH' ? 1 : 0;
246              
247             # if input a 'Math::Matrix' object
248 0 0 0       if (@_ == $h + 2 && UNIVERSAL::isa($_[1], 'Math::Matrix')) {
    0 0        
    0          
249            
250             # call matrix transform
251 0           &_trans2;
252            
253             # if input an array reference
254             } elsif (@_ == $h + 2 && ref($_[1]) eq 'ARRAY') {
255            
256             # if array contains numbers (vector)
257 0 0 0       if (! ref($_[1][0]) && @{$_[1]} == grep {Scalar::Util::looks_like_number($_)} @{$_[1]}) {
  0 0 0        
  0            
  0            
258            
259             # call vector transform
260 0           &_trans1;
261            
262             # if array contains vectors (2-D array)
263 0 0         } elsif (ref($_[1][0]) eq 'ARRAY' && @{$_[1]} == grep {ref($_) eq 'ARRAY' && Scalar::Util::looks_like_number($_->[0])} @{$_[1]}) {
  0            
  0            
264            
265             # call matrix transform
266 0           &_trans2;
267            
268             } else {
269            
270             # call structure transform
271 0           &_trans3;
272            
273             }
274            
275             # if input a list (of numbers)
276 0           } elsif (@_ == $h + 1 + grep {Scalar::Util::looks_like_number($_)} @_) {
277            
278             # call list transform
279 0           &_trans0;
280            
281             } else {
282            
283             # error
284 0           croak('invalid transform input');
285            
286             }
287              
288             }
289              
290             # inverse transform
291             # note: number of undefined output values must equal number of defined input values
292             # note: the input and output vectors contain the final solution on return
293             # hash key 'init' specifies initial value vector
294             # hash key 'ubox' enables unit box extrapolation
295             # parameters: (input_vector, output_vector, [hash])
296             # returns: (RMS_error_value)
297             sub inverse {
298              
299             # get parameters
300 0     0 0   my ($self, $in, $out, $hash) = @_;
301              
302             # local variables
303 0           my ($i, $j, @si, @so, $init);
304 0           my ($int, $jac, $mat, $delta);
305 0           my ($max, $elim, $dlim, $accum, $error);
306              
307             # initialize indices
308 0           $i = $j = -1;
309              
310             # build slice arrays while validating input and output arrays
311 0 0         ((grep {$i++; defined() && push(@si, $i)} @{$in}) == (grep {$j++; ! defined() && push(@so, $j)} @{$out})) or croak('wrong number of undefined values');
  0 0          
  0 0          
  0            
  0            
  0            
  0            
312              
313             # get init array
314 0           $init = $hash->{'init'};
315              
316             # for each undefined output value
317 0           for my $i (@so) {
318            
319             # set to supplied initial value or 0.5
320 0 0         $out->[$i] = defined($init->[$i]) ? $init->[$i] : 0.5;
321            
322             }
323              
324             # set maximum loop count
325 0   0       $max = $hash->{'inv_max'} || 10;
326              
327             # loop error limit
328 0   0       $elim = $hash->{'inv_elim'} || 1E-6;
329              
330             # set delta limit
331 0   0       $dlim = $hash->{'inv_dlim'} || 0.5;
332              
333             # create empty solution matrix
334 0           $mat = Math::Matrix->new([]);
335              
336             # compute initial transform values
337 0           ($jac, $int) = jacobian($self, $out, $hash);
338              
339             # solution loop
340 0           for (1 .. $max) {
341            
342             # for each input
343 0           for my $i (0 .. $#si) {
344            
345             # for each output
346 0           for my $j (0 .. $#so) {
347            
348             # copy Jacobian value to solution matrix
349 0           $mat->[$i][$j] = $jac->[$si[$i]][$so[$j]];
350            
351             }
352            
353             # save residual value to solution matrix
354 0           $mat->[$i][$#si + 1] = $in->[$si[$i]] - $int->[$si[$i]];
355            
356             }
357            
358             # solve for delta values
359 0           $delta = $mat->solve;
360            
361             # for each output value
362 0           for my $i (0 .. $#so) {
363            
364             # add delta (limited using hyperbolic tangent)
365 0           $out->[$so[$i]] += POSIX::tanh($delta->[$i][0]/$dlim) * $dlim;
366            
367             }
368            
369             # compute updated transform values
370 0           ($jac, $int) = jacobian($self, $out, $hash);
371            
372             # initialize error accumulator
373 0           $accum = 0;
374            
375             # for each input
376 0           for my $i (0 .. $#si) {
377            
378             # accumulate delta squared
379 0           $accum += ($in->[$si[$i]] - $int->[$si[$i]])**2;
380            
381             }
382            
383             # compute RMS error
384 0           $error = sqrt($accum/@si);
385            
386             # if error less than limit
387 0 0         last if ($error < $elim);
388            
389             }
390              
391             # update input vector with final values
392 0           @{$in} = @{$int};
  0            
  0            
393              
394             # return
395 0           return($error);
396              
397             }
398              
399             # compute Jacobian matrix
400             # nominal input range is (0 - 1)
401             # hash key 'ubox' enables unit box extrapolation
402             # parameters: (input_vector, [hash])
403             # returns: (Jacobian_matrix, [output_vector])
404             sub jacobian {
405              
406             # get parameters
407 0     0 0   my ($self, $in, $hash) = @_;
408              
409             # local variables
410 0           my ($xflag, $ext, $ubox, $jac, $delta, $out);
411              
412             # verify input vector
413 0 0 0       (ref($in) eq 'ARRAY' && @{$in} == @{$self->[1]} && @{$in} == grep {! ref()} @{$in}) or croak('invalid jacobian input');
  0   0        
  0            
  0            
  0            
  0            
414              
415             # if extrapolation required (any input outside the unit box)
416 0 0 0       if ($xflag = $hash->{'ubox'} && grep {$_ < 0 || $_ > 1} @{$in}) {
417            
418             # get unit box intersection values
419 0           ($ext, $ubox) = _intersect($in);
420            
421             # get Jacobian (at unit box surface)
422 0           $jac = ICC::Support::Lapack::nPINT_jacobian($self->[1], $ubox, $self->[3]);
423            
424             } else {
425            
426             # get Jacobian (at input values)
427 0           $jac = ICC::Support::Lapack::nPINT_jacobian($self->[1], $in, $self->[3]);
428             }
429              
430             # bless to Math::Matrix object
431 0           bless($jac, 'Math::Matrix');
432              
433             # if output values wanted
434 0 0         if (wantarray) {
435            
436             # if extrapolation required
437 0 0         if ($xflag) {
438            
439             # compute ouptut values (at unit box surface)
440 0           $out = ICC::Support::Lapack::nPINT_vec_trans($self->[1], $ubox, $self->[3]);
441            
442             # for each output
443 0           for my $i (0 .. $#{$out}) {
  0            
444            
445             # add delta value
446 0           $out->[$i] += ICC::Shared::dotProduct($jac->[$i], $ext);
447            
448             }
449            
450             } else {
451            
452             # compute ouptut values (at input values)
453 0           $out = ICC::Support::Lapack::nPINT_vec_trans($self->[1], $in, $self->[3]);
454            
455             }
456            
457             # return Jacobian and output values
458 0           return($jac, $out);
459            
460             } else {
461            
462             # return Jacobian only
463 0           return($jac);
464            
465             }
466            
467             }
468              
469             # print object contents to string
470             # format is an array structure
471             # parameter: ([format])
472             # returns: (string)
473             sub sdump {
474              
475             # get parameters
476 0     0 1   my ($self, $p) = @_;
477              
478             # local variables
479 0           my ($s, $fmt);
480              
481             # resolve parameter to an array reference
482 0 0         $p = defined($p) ? ref($p) eq 'ARRAY' ? $p : [$p] : [];
    0          
483              
484             # get format string
485 0 0 0       $fmt = defined($p->[0]) && ! ref($p->[0]) ? $p->[0] : 'undef';
486              
487             # set string to object ID
488 0           $s = sprintf("'%s' object, (0x%x)\n", ref($self), $self);
489              
490             # return
491 0           return($s);
492              
493             }
494              
495             # transform list
496             # parameters: (object_reference, list, [hash])
497             # returns: (list)
498             sub _trans0 {
499              
500             # local variables
501 0     0     my ($self, $hash);
502              
503             # get object reference
504 0           $self = shift();
505              
506             # get optional hash
507 0 0         $hash = pop() if (ref($_[-1]) eq 'HASH');
508              
509             # call _trans1
510 0           return(@{_trans1($self, \@_, $hash)});
  0            
511              
512             }
513              
514             # transform vector
515             # parameters: (object_reference, vector, [hash])
516             # returns: (vector)
517             sub _trans1 {
518              
519             # get parameters
520 0     0     my ($self, $in, $hash) = @_;
521              
522             # local variables
523 0           my ($ext, $ubox, $out, $jac);
524              
525             # if unit box extrapolation
526 0 0 0       if ($hash->{'ubox'} && grep {$_ < 0.0 || $_ > 1.0} @{$in}) {
  0 0          
  0            
527            
528             # get unit box intersection values
529 0           ($ext, $ubox) = _intersect($in);
530            
531             # get transformed values (at unit box surface)
532 0           $out = ICC::Support::Lapack::nPINT_vec_trans($self->[1], $ubox, $self->[3]);
533            
534             # get Jacobian (at unit box surface)
535 0           $jac = ICC::Support::Lapack::nPINT_jacobian($self->[1], $ubox, $self->[3]);
536            
537             # for each output
538 0           for my $i (0 .. $#{$out}) {
  0            
539            
540             # add delta value
541 0           $out->[$i] += ICC::Shared::dotProduct($jac->[$i], $ext);
542            
543             }
544            
545             # return extrapolated value
546 0           return($out);
547            
548             } else {
549            
550             # call BLAS-based vector transform function
551 0           return(ICC::Support::Lapack::nPINT_vec_trans($self->[1], $in, $self->[3]));
552            
553             }
554              
555             }
556              
557             # transform matrix (2-D array -or- Math::Matrix object)
558             # parameters: (object_reference, matrix, [hash])
559             # returns: (matrix)
560             sub _trans2 {
561              
562             # get parameters
563 0     0     my ($self, $in, $hash) = @_;
564              
565             # local variable
566 0           my ($out);
567              
568             # if unit box extrapolation
569 0 0         if ($hash->{'ubox'}) {
570            
571             # for each matrix row
572 0           for my $i (0 .. $#{$in}) {
  0            
573            
574             # compute output vector
575 0           $out->[$i] = _trans1($self, $in->[$i], $hash);
576            
577             }
578            
579             } else {
580            
581             # call BLAS-based vector transform function
582 0           $out = ICC::Support::Lapack::nPINT_mat_trans($self->[1], $in, $self->[3]);
583            
584             }
585              
586             # return matrix, same form as input
587 0 0         return(UNIVERSAL::isa($in, 'Math::Matrix') ? bless($out, 'Math::Matrix') : $out);
588              
589             }
590              
591             # transform structure
592             # parameters: (object_reference, structure, [hash])
593             # returns: (structure)
594             sub _trans3 {
595              
596             # get parameters
597 0     0     my ($self, $in, $hash) = @_;
598              
599             # transform the array structure
600 0           _crawl($self, $in, my $out = []);
601              
602             # return output array reference
603 0           return($out);
604              
605             }
606              
607             # recursive transform
608             # array structure is traversed until scalar arrays are found and transformed
609             # parameters: (object_reference, input_array_reference, output_array_reference, hash)
610             sub _crawl {
611              
612             # get parameters
613 0     0     my ($self, $in, $out, $hash) = @_;
614              
615             # if input is a vector (reference to a scalar array)
616 0 0         if (@{$in} == grep {! ref()} @{$in}) {
  0            
  0            
  0            
617            
618             # transform input vector and copy to output
619 0           @{$out} = @{_trans1($self, $in, $hash)};
  0            
  0            
620            
621             } else {
622            
623             # for each input element
624 0           for my $i (0 .. $#{$in}) {
  0            
625            
626             # if an array reference
627 0 0         if (ref($in->[$i]) eq 'ARRAY') {
628            
629             # transform next level
630 0           _crawl($self, $in->[$i], $out->[$i] = [], $hash);
631            
632             } else {
633            
634             # error
635 0           croak('invalid transform input');
636            
637             }
638            
639             }
640            
641             }
642            
643             }
644              
645             # find unit box intersection
646             # with line from input to box-center
647             # parameters: (input_vector)
648             # returns: (extrapolation_vector, unit_box_intersection)
649             sub _intersect {
650              
651             # get input vector
652 0     0     my $in = shift();
653              
654             # local variables
655 0           my (@cin, $dmax, $ubox);
656              
657             # compute input to center difference
658 0           @cin = map {$_ - 0.5} @{$in};
  0            
  0            
659              
660             # initialize
661 0           $dmax = 0;
662              
663             # for each difference
664 0           for (@cin) {
665            
666             # if larger absolute value
667 0 0         if (abs($_) > $dmax) {
668            
669             # new max difference
670 0           $dmax = abs($_);
671            
672             }
673            
674             }
675              
676             # multiply max difference by 2
677 0           $dmax *= 2;
678              
679             # compute output values (on surface of unit box)
680 0           $ubox = [map {$_/$dmax + 0.5} @cin];
  0            
681              
682             # return extrapolation vector ($in - $ubox) and unit-box intersection vector
683 0           return([map {$in->[$_] - $ubox->[$_]} (0 .. $#{$in})], $ubox);
  0            
  0            
684              
685             }
686              
687             # make nPINT object from parameter hash
688             # parameters: (object_reference, ref_to_parameter_hash)
689             sub _new_from_hash {
690              
691             # get parameters
692 0     0     my ($self, $hash) = @_;
693              
694             # local variables
695 0           my ($pda, $coef, $size);
696              
697             # get input polynomial degree array (pda) reference
698 0 0         $pda = $hash->{'pda'} or croak('missing polynomial degree array');
699              
700             # get output coefficient array reference
701 0 0         $coef = $hash->{'coef'} or croak('missing coefficient array');
702              
703             # verify pda structure
704 0 0 0       (ref($pda) eq 'ARRAY' && ! ref($pda->[0])) or croak('improper polynomial degree array');
705              
706             # copy pda to object
707 0           $self->[1] = [@{$pda}];
  0            
708              
709             # initialize coefficient array size
710 0           $size = 1;
711              
712             # for each pda element
713 0           for my $d (@{$pda}) {
  0            
714            
715             # multiply by (degree + 1)
716 0           $size *= ($d + 1);
717            
718             }
719              
720             # verify coef is array or Math::Matrix object
721 0 0 0       (ref($coef) eq 'ARRAY' || UNIVERSAL::isa($coef, 'Math::Matrix')) or croak('improper coefficient array type');
722              
723             # verify coef structure
724 0 0 0       (ref($coef->[0]) eq 'ARRAY' && ! ref($coef->[0][0]) && @{$coef} == $size) or croak('improper coefficient array structure');
  0   0        
725              
726             # copy coefficient array to object
727 0           $self->[2] = bless(Storable::dclone($coef), 'Math::Matrix');
728              
729             # update cache
730 0           $self->[3] = ICC::Support::Lapack::cache_2D($self->[2]);
731              
732             }
733              
734             # make nPINT object from Storable file
735             # parameters: (object_reference, path_to_storable_file)
736             sub _new_from_storable {
737              
738             # get parameters
739 0     0     my ($self, $path) = @_;
740              
741             # local variables
742 0           my ($hash, @files);
743              
744             # filter path
745 0           ICC::Shared::filterPath($path);
746              
747             # retrieve hash reference
748 0           $hash = Storable::retrieve($path);
749              
750             # if successful
751 0 0         if (defined($hash)) {
752            
753             # create nPINT object from hash reference
754 0           _new_from_hash($self, $hash);
755            
756             # return success flag
757 0           return(1);
758            
759             } else {
760            
761             # return failure flag
762 0           return(0);
763            
764             }
765            
766             }
767              
768             1;