File Coverage

blib/lib/ICC/Support/nPINT.pm
Criterion Covered Total %
statement 17 223 7.6
branch 2 100 2.0
condition 1 75 1.3
subroutine 5 21 23.8
pod 1 9 11.1
total 26 428 6.0


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