File Coverage

blib/lib/ICC/Support/nMIX.pm
Criterion Covered Total %
statement 17 344 4.9
branch 1 142 0.7
condition 0 72 0.0
subroutine 5 29 17.2
pod 1 10 10.0
total 24 597 4.0


line stmt bran cond sub pod time code
1             package ICC::Support::nMIX;
2              
3 2     2   80672 use strict;
  2         12  
  2         47  
4 2     2   9 use Carp;
  2         4  
  2         121  
5              
6             our $VERSION = 0.31;
7              
8             # revised 2016-05-17
9             #
10             # Copyright © 2004-2020 by William B. Birkett
11              
12             # inherit from Shared
13 2     2   380 use parent qw(ICC::Shared);
  2         266  
  2         9  
14              
15             # enable static variables
16 2     2   87 use feature 'state';
  2         3  
  2         7980  
17              
18             # create new nMIX object
19             # parameter may be a hash -or- an ICC::Support::Chart object
20             # columns is an optional column slice for the chart clut data
21             # hash may contain references to clut array or delta array
22             # hash keys are: ('array', 'delta')
23             # parameter: ()
24             # parameter: (ref_to_attribute_hash)
25             # parameter: (chart_object, [columns])
26             # returns: (ref_to_object)
27             sub new {
28              
29             # get object class
30 1     1 0 765 my $class = shift();
31              
32             # create empty nMIX object
33 1         3 my $self = [
34             {}, # object header
35             [], # clut
36             [], # delta
37             [], # clut exp
38             undef, # clut exp cache (for Lapack)
39             ];
40              
41             # if there are parameters
42 1 50       4 if (@_) {
43            
44             # if one parameter, a hash reference
45 0 0 0     0 if (@_ == 1 && ref($_[0]) eq 'HASH') {
    0 0        
      0        
46            
47             # make new nMIX object from attribute hash
48 0         0 _new_from_hash($self, @_);
49            
50             # if one or two parameters, first parameter an ICC::Support::Chart object
51             } elsif ((@_ == 1 || @_ == 2) && UNIVERSAL::isa($_[0], 'ICC::Support::Chart')) {
52            
53             # make new nMIX object from chart
54 0         0 _new_from_chart($self, @_);
55            
56             } else {
57            
58             # error
59 0         0 croak('parameter must be a hash reference or ICC::Support::Chart object');
60            
61             }
62            
63             }
64              
65             # bless object
66 1         2 bless($self, $class);
67              
68             # return object reference
69 1         2 return($self);
70              
71             }
72              
73             # get/set reference to header hash
74             # parameters: ([ref_to_new_hash])
75             # returns: (ref_to_hash)
76             sub header {
77              
78             # get object reference
79 0     0 0   my $self = shift();
80              
81             # if there are parameters
82 0 0         if (@_) {
83            
84             # if one parameter, a hash reference
85 0 0 0       if (@_ == 1 && ref($_[0]) eq 'HASH') {
86            
87             # set header to copy of hash
88 0           $self->[0] = {%{$_[0]}};
  0            
89            
90             } else {
91            
92             # error
93 0           croak('parameter must be a hash reference');
94            
95             }
96            
97             }
98              
99             # return reference
100 0           return($self->[0]);
101              
102             }
103              
104             # get/set reference to clut array
105             # parameters: ([ref_to_new_array])
106             # returns: (ref_to_array)
107             sub array {
108              
109             # get object reference
110 0     0 0   my $self = shift();
111              
112             # if there are parameters
113 0 0         if (@_) {
114            
115             # if one parameter, a 2-D array reference
116 0 0 0       if (@_ == 1 && ref($_[0]) eq 'ARRAY' && @{$_[0]} == grep {ref() eq 'ARRAY'} @{$_[0]}) {
  0 0 0        
  0   0        
  0            
117            
118             # copy array to object and bless
119 0           $self->[1] = bless(Storable::dclone($_[0]), 'Math::Matrix');
120            
121             # if one parameter, a Math::Matrix object
122             } elsif (@_ == 1 && UNIVERSAL::isa($_[0], 'Math::Matrix')) {
123            
124             # copy matrix to object
125 0           $self->[1] = Storable::dclone($_[0]);
126            
127             } else {
128            
129             # error
130 0           croak('clut must be a 2-D array reference or Math::Matrix object');
131            
132             }
133            
134             # for each corner point
135 0           for my $i (0 .. $#{$self->[1]}) {
  0            
136            
137             # for each spectral value
138 0           for my $j (0 .. $#{$self->[1][$i]}) {
  0            
139            
140             # set value to zero, if negative
141 0 0         $self->[1][$i][$j] = 0 if ($self->[1][$i][$j] < 0);
142            
143             }
144            
145             }
146            
147             # update arrays
148 0           _update_clut_exp($self);
149            
150             }
151              
152             # return reference
153 0           return($self->[1]);
154              
155             }
156              
157             # get/set reference to delta array
158             # scalar value parameter fills array with that value
159             # parameters: ([ref_to_new_array -or- scalar_value])
160             # returns: (ref_to_array)
161             sub delta {
162              
163             # get object reference
164 0     0 0   my $self = shift();
165              
166             # if there are parameters
167 0 0         if (@_) {
168            
169             # if one parameter, a 1-D array reference
170 0 0 0       if (@_ == 1 && ref($_[0]) eq 'ARRAY' && @{$_[0]} == grep {Scalar::Util::looks_like_number($_)} @{$_[0]}) {
  0 0 0        
  0   0        
  0            
171            
172             # make delta array
173 0           $self->[2] = [@{$_[0]}];
  0            
174            
175             # if one parameter, a scalar number
176             } elsif (@_ == 1 && Scalar::Util::looks_like_number($_[0])) {
177            
178             # if array is defined
179 0 0         if (defined($self->[1])) {
180            
181             # make delta array
182 0           $self->[2] = [($_[0]) x @{$self->[1][0]}];
  0            
183            
184             } else {
185            
186             # error
187 0           croak ('array must be defined when specifying delta as a scalar');
188            
189             }
190            
191             } else {
192            
193             # error
194 0           croak('\'delta\' parameter must be a scalar or a 1-D array reference');
195            
196             }
197            
198             # update arrays
199 0           _update_clut_exp($self);
200            
201             }
202              
203             # return reference
204 0           return($self->[2]);
205              
206             }
207              
208             # get number of input channels
209             # returns: (number)
210             sub cin {
211              
212             # get object reference
213 0     0 0   my $self = shift();
214              
215             # return
216 0           return(int(log(@{$self->[1]})/log(2) + 1E-12));
  0            
217              
218             }
219              
220             # get number of output channels
221             # returns: (number)
222             sub cout {
223              
224             # get object reference
225 0     0 0   my $self = shift();
226              
227             # return
228 0           return(scalar(@{$self->[1][0]}));
  0            
229              
230             }
231              
232             # transform data
233             # supported input types:
234             # parameters: (list, [hash])
235             # parameters: (vector, [hash])
236             # parameters: (matrix, [hash])
237             # parameters: (Math::Matrix_object, [hash])
238             # parameters: (structure, [hash])
239             # returns: (same_type_as_input)
240             sub transform {
241              
242             # set hash value (0 or 1)
243 0 0   0 0   my $h = ref($_[-1]) eq 'HASH' ? 1 : 0;
244              
245             # if input a 'Math::Matrix' object
246 0 0 0       if (@_ == $h + 2 && UNIVERSAL::isa($_[1], 'Math::Matrix')) {
    0 0        
    0          
247            
248             # call matrix transform
249 0           &_trans2;
250            
251             # if input an array reference
252             } elsif (@_ == $h + 2 && ref($_[1]) eq 'ARRAY') {
253            
254             # if array contains numbers (vector)
255 0 0 0       if (! ref($_[1][0]) && @{$_[1]} == grep {Scalar::Util::looks_like_number($_)} @{$_[1]}) {
  0 0 0        
  0            
  0            
256            
257             # call vector transform
258 0           &_trans1;
259            
260             # if array contains vectors (2-D array)
261 0 0         } elsif (ref($_[1][0]) eq 'ARRAY' && @{$_[1]} == grep {ref($_) eq 'ARRAY' && Scalar::Util::looks_like_number($_->[0])} @{$_[1]}) {
  0            
  0            
262            
263             # call matrix transform
264 0           &_trans2;
265            
266             } else {
267            
268             # call structure transform
269 0           &_trans3;
270            
271             }
272            
273             # if input a list (of numbers)
274 0           } elsif (@_ == $h + 1 + grep {Scalar::Util::looks_like_number($_)} @_) {
275            
276             # call list transform
277 0           &_trans0;
278            
279             } else {
280            
281             # error
282 0           croak('invalid transform input');
283            
284             }
285              
286             }
287              
288             # compute Jacobian matrix
289             # nominal input range is (0 - 1)
290             # hash key 'ubox' enables unit box extrapolation
291             # clipped outputs are extrapolated using Jacobian
292             # parameters: (input_vector, [hash])
293             # returns: (Jacobian_matrix, [output_vector])
294             sub jacobian {
295              
296             # get parameters
297 0     0 0   my ($self, $in, $hash) = @_;
298              
299             # local variables
300 0           my ($ext, $out, $jac, $jac_bc, $d, $s, $dx);
301              
302             # check if ICC::Support::Lapack module is loaded
303 0           state $lapack = defined($INC{'ICC/Support/Lapack.pm'});
304              
305             # if extrapolation required (any input outside the unit box)
306 0 0 0       if ($hash->{'ubox'} && grep {$_ < 0.0 || $_ > 1.0} @{$in}) {
  0 0          
  0            
307            
308             # compute intersection with unit box
309 0           ($ext, $in) = _intersect($in);
310            
311             }
312              
313             # if ICC::Support::Lapack module is loaded
314 0 0         if ($lapack) {
315            
316             # compute output using Lapack module
317 0           $out = ICC::Support::Lapack::nMIX_vec_trans($in, $self->[4], $self->[2]);
318            
319             # compute Jacobian using Lapack module
320 0           $jac = ICC::Support::Lapack::nMIX_jacobian($in, $self->[4], $self->[2], $out);
321            
322             # bless Jacobian as Math::Matrix object
323 0           bless($jac, 'Math::Matrix');
324            
325             } else {
326            
327             # compute output values
328 0           $out = _trans1($self, $in);
329            
330             # compute the barycentric jacobian
331 0           $jac_bc = _barycentric_jacobian($in);
332            
333             # compute Jacobian (before exponentiation)
334 0           $jac = $self->[3]->transpose * $jac_bc;
335            
336             # for each row (output)
337 0           for my $i (0 .. $#{$jac}) {
  0            
338            
339             # get delta value
340 0           $d = $self->[2][$i];
341            
342             # skip if delta is one
343 0 0         next if ($d == 1);
344            
345             # get output value
346 0           $s = $out->[$i];
347            
348             # compute exponentiation adjustment
349 0 0         $dx = $s ? $d ? abs($s)**(1 - $d)/$d : $s : 0;
    0          
350            
351             # for each column (input)
352 0           for my $j (0 .. $#{$jac->[0]}) {
  0            
353            
354             # adjust for exponentiation
355 0           $jac->[$i][$j] *= $dx;
356            
357             }
358            
359             }
360            
361             }
362              
363             # if output values wanted
364 0 0         if (wantarray) {
365            
366             # if output extrapolated
367 0 0         if (defined($ext)) {
368            
369             # for each output
370 0           for my $i (0 .. $#{$out}) {
  0            
371            
372             # add delta value
373 0           $out->[$i] += ICC::Shared::dotProduct($jac->[$i], $ext);
374            
375             }
376            
377             }
378            
379             # return Jacobian and output vector
380 0           return($jac, $out);
381            
382             } else {
383            
384             # return Jacobian only
385 0           return($jac);
386            
387             }
388            
389             }
390              
391             # compute parametric Jacobian matrix
392             # parameters: (input_vector)
393             # returns: (parametric_jacobian_matrix)
394             sub parajac {
395              
396             # get parameters
397 0     0 0   my ($self, $in) = @_;
398              
399             # return Jacobian matrix
400 0           return(Math::Matrix->diagonal(_parametric($self, $in)));
401              
402             }
403              
404             # print object contents to string
405             # format is an array structure
406             # parameter: ([format])
407             # returns: (string)
408             sub sdump {
409              
410             # get parameters
411 0     0 1   my ($self, $p) = @_;
412              
413             # local variables
414 0           my ($s, $fmt);
415              
416             # resolve parameter to an array reference
417 0 0         $p = defined($p) ? ref($p) eq 'ARRAY' ? $p : [$p] : [];
    0          
418              
419             # get format string
420 0 0 0       $fmt = defined($p->[0]) && ! ref($p->[0]) ? $p->[0] : 'undef';
421              
422             # set string to object ID
423 0           $s = sprintf("'%s' object, (0x%x)\n", ref($self), $self);
424              
425             # return
426 0           return($s);
427              
428             }
429              
430             # transform list
431             # parameters: (object_reference, list, [hash])
432             # returns: (list)
433             sub _trans0 {
434              
435             # local variables
436 0     0     my ($self, $hash);
437 0           my ($coef, $sum, $product, @out);
438              
439             # check if ICC::Support::Lapack module is loaded
440 0           state $lapack = defined($INC{'ICC/Support/Lapack.pm'});
441              
442             # get object reference
443 0           $self = shift();
444              
445             # get optional hash
446 0 0         $hash = pop() if (ref($_[-1]) eq 'HASH');
447              
448             # if ICC::Support::Lapack module is loaded
449 0 0         if ($lapack) {
450            
451             # compute output using Lapack module
452 0           @out = @{ICC::Support::Lapack::nMIX_vec_trans(\@_, $self->[4], $self->[2])};
  0            
453            
454             } else {
455            
456             # compute output using '_trans1'
457 0           @out = @{_trans1($self, \@_)};
  0            
458            
459             }
460              
461             # return
462 0           return(@out);
463              
464             }
465              
466             # transform vector
467             # parameters: (object_reference, vector, [hash])
468             # returns: (vector)
469             sub _trans1 {
470            
471             # get parameters
472 0     0     my ($self, $in, $hash) = @_;
473              
474             # local variables
475 0           my ($coef, $sum, $out);
476              
477             # check if ICC::Support::Lapack module is loaded
478 0           state $lapack = defined($INC{'ICC/Support/Lapack.pm'});
479              
480             # if ICC::Support::Lapack module is loaded
481 0 0         if ($lapack) {
482            
483             # compute output using Lapack module
484 0           $out = ICC::Support::Lapack::nMIX_vec_trans($in, $self->[4], $self->[2]);
485            
486             } else {
487            
488             # compute barycentric coefficients
489 0           $coef = _barycentric($in);
490            
491             # for each output value
492 0           for my $i (0 .. $#{$self->[3][0]}) {
  0            
493            
494             # initialize sum
495 0           $sum = 0;
496            
497             # for each coefficient
498 0           for my $j (0 .. $#{$coef}) {
  0            
499            
500             # add product to sum
501 0 0         $sum += $self->[3][$j][$i] * $coef->[$j] if ($coef->[$j]);
502            
503             }
504            
505             # save result
506 0           $out->[$i] = _pow1p($sum, $self->[2][$i]);
507            
508             }
509            
510             }
511              
512             # return
513 0           return($out);
514              
515             }
516              
517             # transform matrix (2-D array -or- Math::Matrix object)
518             # parameters: (object_reference, matrix, [hash])
519             # returns: (matrix)
520             sub _trans2 {
521              
522             # get parameters
523 0     0     my ($self, $in, $hash) = @_;
524              
525             # local variables
526 0           my ($coef, $sum, $product, $mean, $ratio, $out);
527              
528             # check if ICC::Support::Lapack module is loaded
529 0           state $lapack = defined($INC{'ICC/Support/Lapack.pm'});
530              
531             # if ICC::Support::Lapack module is loaded
532 0 0         if ($lapack) {
533            
534             # compute output using Lapack module
535 0           $out = ICC::Support::Lapack::nMIX_mat_trans($in, $self->[4], $self->[2]);
536            
537             } else {
538            
539             # for each input vector
540 0           for my $k (0 .. $#{$in}) {
  0            
541            
542             # compute barycentric coefficients
543 0           $coef = _barycentric($in->[$k]);
544            
545             # for each output value
546 0           for my $i (0 .. $#{$self->[3][0]}) {
  0            
547            
548             # initialize sum
549 0           $sum = 0;
550            
551             # for each coefficient
552 0           for my $j (0 .. $#{$coef}) {
  0            
553            
554             # add product to sum
555 0 0         $sum += $self->[3][$j][$i] * $coef->[$j] if ($coef->[$j]);
556            
557             }
558            
559             # save result
560 0           $out->[$k][$i] = _pow1p($sum, $self->[2][$i]);
561            
562             }
563            
564             }
565            
566             }
567              
568             # return
569 0 0         return(UNIVERSAL::isa($in, 'Math::Matrix') ? bless($out, 'Math::Matrix') : $out);
570              
571             }
572              
573             # transform structure
574             # parameters: (object_reference, structure, [hash])
575             # returns: (structure)
576             sub _trans3 {
577              
578             # get parameters
579 0     0     my ($self, $in, $hash) = @_;
580              
581             # local variables
582 0           my ($out);
583              
584             # transform the array structure
585 0           _crawl($self, $in, $out = [], $hash);
586              
587             # return
588 0           return($out);
589              
590             }
591              
592             # recursive transform
593             # array structure is traversed until scalar arrays are found and transformed
594             # parameters: (ref_to_object, input_array_reference, output_array_reference, hash)
595             sub _crawl {
596              
597             # get parameters
598 0     0     my ($self, $in, $out, $hash) = @_;
599              
600             # if input is a vector (reference to a scalar array)
601 0 0         if (@{$in} == grep {! ref()} @{$in}) {
  0            
  0            
  0            
602            
603             # transform input vector and copy to output
604 0           @{$out} = @{_trans1($self, $in, $hash)};
  0            
  0            
605            
606             } else {
607            
608             # for each input element
609 0           for my $i (0 .. $#{$in}) {
  0            
610            
611             # if an array reference
612 0 0         if (ref($in->[$i]) eq 'ARRAY') {
613            
614             # transform next level
615 0           _crawl($self, $in->[$i], $out->[$i] = [], $hash);
616            
617             } else {
618            
619             # error
620 0           croak('invalid transform input');
621            
622             }
623            
624             }
625            
626             }
627            
628             }
629              
630             # update exponentiated CLUT
631             # parameter: (ref_to_object)
632             sub _update_clut_exp {
633              
634             # get object reference
635 0     0     my $self = shift();
636              
637             # check if ICC::Support::Lapack module is loaded
638 0           state $lapack = defined($INC{'ICC/Support/Lapack.pm'});
639              
640             # if clut and delta are defined and not null
641 0 0 0       if (defined($self->[1]) && @{$self->[1]} && defined($self->[2]) && @{$self->[2]}) {
  0   0        
  0   0        
642            
643             # if ICC::Support::Lapack module is loaded
644 0 0         if ($lapack) {
645            
646             # compute mixture array
647 0           $self->[3] = ICC::Support::Lapack::nMIX_power($self->[1], $self->[2]);
648            
649             # create cached mixture array
650 0           $self->[4] = ICC::Support::Lapack::cache_2D($self->[3]);
651            
652             } else {
653            
654             # for each row
655 0           for my $i (0 .. $#{$self->[1]}) {
  0            
656            
657             # for each column
658 0           for my $j (0 .. $#{$self->[1][0]}) {
  0            
659            
660             # exponentiate corner point value
661 0           $self->[3][$i][$j] = _powm1($self->[1][$i][$j], $self->[2][$j]);
662            
663             }
664            
665             }
666            
667             }
668            
669             }
670              
671             # bless as Math::Matrix object
672 0           bless($self->[3], 'Math::Matrix');
673              
674             }
675              
676             # compute barycentric coefficients
677             # parameter: (input_vector)
678             # returns: (coefficient_array)
679             sub _barycentric {
680              
681             # get parameter
682 0     0     my $dev = shift();
683              
684             # local variables
685 0           my ($devc, $coef);
686              
687             # compute complement values
688 0           $devc = [map {1 - $_} @{$dev}];
  0            
  0            
689              
690             # initialize coefficient array
691 0           $coef = [(1.0) x 2**@{$dev}];
  0            
692              
693             # for each coefficient
694 0           for my $i (0 .. $#{$coef}) {
  0            
695            
696             # for each device value
697 0           for my $j (0 .. $#{$dev}) {
  0            
698            
699             # if $j-th bit set
700 0 0         if ($i >> $j & 1) {
701            
702             # multiply by device value
703 0           $coef->[$i] *= $dev->[$j];
704            
705             } else {
706            
707             # multiply by (1 - device value)
708 0           $coef->[$i] *= $devc->[$j];
709            
710             }
711            
712             }
713            
714             }
715              
716             # return
717 0           return($coef);
718              
719             }
720              
721             # compute barycentric Jacobian matrix
722             # parameter: (input_vector)
723             # returns: (Jacobian_matrix)
724             sub _barycentric_jacobian {
725              
726             # get parameter
727 0     0     my $dev = shift();
728              
729             # local variables
730 0           my ($devc, $rows, $jac);
731              
732             # compute complement values
733 0           $devc = [map {1 - $_} @{$dev}];
  0            
  0            
734              
735             # compute matrix rows
736 0           $rows = 2**@{$dev};
  0            
737              
738             # for each matrix row
739 0           for my $i (0 .. $rows - 1) {
740            
741             # initialize row
742 0           $jac->[$i] = [(1.0) x @{$dev}];
  0            
743            
744             # for each matrix column
745 0           for my $j (0 .. $#{$dev}) {
  0            
746            
747             # for each device value
748 0           for my $k (0 .. $#{$dev}) {
  0            
749            
750             # if $k-th bit set
751 0 0         if ($i >> $k & 1) {
752            
753             # multiply by device value -or- 1 (skip)
754 0 0         $jac->[$i][$j] *= $dev->[$k] if ($j != $k);
755            
756             } else {
757            
758             # multiply by (1 - device value) -or- -1
759 0 0         $jac->[$i][$j] *= ($j != $k) ? $devc->[$k] : -1;
760            
761             }
762            
763             }
764            
765             }
766            
767             }
768              
769             # return
770 0           return(bless($jac, 'Math::Matrix'));
771              
772             }
773              
774             # find unit box intersection
775             # with line from input to box-center
776             # parameters: (input_vector)
777             # returns: (extrapolation_vector, intersection_vector)
778             sub _intersect {
779              
780             # get input values
781 0     0     my ($in) = shift();
782              
783             # local variables
784 0           my (@cin, $dmax, $ubox, $ext);
785              
786             # compute input to box-center difference
787 0           @cin = map {$_ - 0.5} @{$in};
  0            
  0            
788              
789             # initialize
790 0           $dmax = 0;
791              
792             # for each difference
793 0           for (@cin) {
794            
795             # if larger absolute value
796 0 0         if (abs($_) > $dmax) {
797            
798             # new max difference
799 0           $dmax = abs($_);
800            
801             }
802            
803             }
804              
805             # multiply max difference by 2
806 0           $dmax *= 2;
807              
808             # compute intersection vector (on surface of unit box)
809 0           $ubox = [map {$_/$dmax + 0.5} @cin];
  0            
810              
811             # compute extrapolation vector (as Math::Matrix object)
812 0           $ext = [map {$in->[$_] - $ubox->[$_]} (0 .. $#{$in})];
  0            
  0            
813              
814             # return
815 0           return($ext, $ubox);
816              
817             }
818              
819             # compute parametric partial derivatives
820             # parameters: (object_reference, input_vector)
821             # returns: (partial_derivative_vector)
822             sub _parametric {
823              
824             # get parameters
825 0     0     my ($self, $in) = @_;
826              
827             # local variables
828 0           my ($bc, $cp, $d, $s1, $sum1, $sum2, $pj, $dk, $pd, $r);
829              
830             # calculate barycentric coordinates
831 0           $bc = _barycentric($in);
832              
833             # get corner point matrix
834 0           $cp = $self->[1];
835              
836             # for each delta/output value
837 0           for my $i (0 .. $#{$self->[2]}) {
  0            
838            
839             # get delta value
840 0           $d = $self->[2][$i];
841            
842             # if delta non-zeroish
843 0 0         if (abs($d) >= 1E-5) {
844            
845             # initialize sums
846 0           $sum1 = $sum2 = 0;
847            
848             # for each corner point
849 0           for my $j (0 .. $#{$cp}) {
  0            
850            
851             # accumulate sums
852 0           $sum1 += $s1 = $bc->[$j] * $cp->[$j][$i]**$d;
853 0           $sum2 += $s1 * log($cp->[$j][$i]);
854            
855             }
856            
857             # compute partial derivative
858 0           $pj->[$i] = $sum1**(1/$d) * ($sum2/$sum1 - log($sum1)/$d)/$d;
859            
860             } else {
861            
862             # for delta +/- 1E-5
863 0           for my $k (0 .. 1) {
864            
865             # set delta
866 0 0         $dk = $k ? -1E-5 : 1E-5;
867            
868             # initialize sums
869 0           $sum1 = $sum2 = 0;
870            
871             # for each corner point
872 0           for my $j (0 .. $#{$cp}) {
  0            
873            
874             # accumulate sums
875 0           $sum1 += $s1 = $bc->[$j] * $cp->[$j][$i]**$dk;
876 0           $sum2 += $s1 * log($cp->[$j][$i]);
877            
878             }
879            
880             # compute partial derivative
881 0           $pd->[$k] = $sum1**(1/$dk) * ($sum2/$sum1 - log($sum1)/$dk)/$dk;
882            
883             }
884            
885             # compute interpolation ratio
886 0           $r = 0.5 - $d/2E-5;
887            
888             # interpolate partial derivative
889 0           $pj->[$i] = $r * $pd->[0] + (1 - $r) * $pd->[1];
890            
891             }
892            
893             }
894              
895             # return array of partial derivatives
896 0           return($pj);
897              
898             }
899              
900             # exponentiate corner point
901             # uses expm1 function for small exponents
902             # parameters: (base, exponent)
903             # returns: (base^exponent)
904             sub _powm1 {
905              
906             # get parameters
907 0     0     my ($base, $exp) = @_;
908              
909 0 0         if ($exp == 0.0) {
    0          
910            
911 0 0         if ($base > 0.0) {
912            
913 0           return(log($base))
914            
915             } else {
916            
917 0           return(-(DBL_MAX))
918            
919             }
920            
921             } elsif ($exp < 1.0) {
922            
923 0 0         if ($base > 0.0) {
924            
925 0           return(ICC::Support::Lapack::expm1(log($base) * $exp))
926            
927             } else {
928            
929 0           return(-1.0)
930            
931             }
932            
933             } else {
934            
935 0 0         if ($base > 0.0) {
936            
937 0           return(POSIX::pow($base, $exp))
938            
939             } else {
940            
941 0           return(0)
942            
943             }
944            
945             }
946            
947             }
948              
949             # exponentiate barycentric sum
950             # uses log1p function for small exponents
951             # parameters: (base, exponent)
952             # returns: (base^(1/exponent))
953             sub _pow1p {
954              
955             # get parameters
956 0     0     my ($base, $exp) = @_;
957            
958 0 0         if ($exp == 0.0) {
    0          
959            
960 0           return(exp($base))
961            
962             } elsif ($exp < 1.0) {
963            
964 0 0         if ($base > -1.0) {
965            
966 0           return(exp(ICC::Support::Lapack::log1p($base)/$exp))
967            
968             } else {
969            
970 0           return(0.0)
971            
972             }
973            
974             } else {
975            
976 0 0         if ($base > 0.0) {
977            
978 0           return(POSIX::pow($base, 1.0/$exp))
979            
980             } else {
981            
982 0           return(0.0)
983            
984             }
985            
986             }
987            
988             }
989              
990             # make new nMIX object from attribute hash
991             # hash may contain pointers to clut array or delta array
992             # hash keys are: ('array', 'delta')
993             # object elements not specified in the hash are unchanged
994             # parameters: (ref_to_object, ref_to_attribute_hash)
995             sub _new_from_hash {
996              
997             # get parameters
998 0     0     my ($self, $hash) = @_;
999              
1000             # local variable
1001 0           my ($value);
1002              
1003             # if 'array' attribute
1004 0 0         if (defined($value = $hash->{'array'})) {
1005            
1006             # if reference to a 2-D array
1007 0 0 0       if (ref($value) eq 'ARRAY' && @{$value} == grep {ref() eq 'ARRAY'} @{$value}) {
  0 0          
  0            
  0            
1008            
1009             # copy array to object and bless
1010 0           $self->[1] = bless(Storable::dclone($value), 'Math::Matrix');
1011            
1012             # if reference to a Math::Matrix object
1013             } elsif (UNIVERSAL::isa($value, 'Math::Matrix')) {
1014            
1015             # copy matrix to object
1016 0           $self->[1] = Storable::dclone($value);
1017            
1018             } else {
1019            
1020             # wrong data type
1021 0           croak('\'array\' must be a 2-D array reference or Math::Matrix object');
1022            
1023             }
1024            
1025             # for each corner point
1026 0           for my $i (0 .. $#{$self->[1]}) {
  0            
1027            
1028             # for each spectral value
1029 0           for my $j (0 .. $#{$self->[1][$i]}) {
  0            
1030            
1031             # set value to zero, if negative
1032 0 0         $self->[1][$i][$j] = 0 if ($self->[1][$i][$j] < 0);
1033            
1034             }
1035            
1036             }
1037            
1038             }
1039              
1040             # if 'delta' attribute
1041 0 0         if (defined($value = $hash->{'delta'})) {
1042            
1043             # if reference to a 1-D array (vector)
1044 0 0 0       if (ref($value) eq 'ARRAY' && @{$value} == grep {Scalar::Util::looks_like_number($_)} @{$value}) {
  0 0          
  0            
  0            
1045            
1046             # make delta array
1047 0           $self->[2] = [@{$value}];
  0            
1048            
1049             # if scalar number
1050             } elsif (Scalar::Util::looks_like_number($value)) {
1051            
1052             # if array is defined
1053 0 0         if (defined($self->[1])) {
1054            
1055             # make delta array
1056 0           $self->[2] = [($value) x @{$self->[1][0]}];
  0            
1057            
1058             } else {
1059            
1060             # error
1061 0           croak ('array must be defined when specifying delta as a scalar');
1062            
1063             }
1064            
1065             } else {
1066            
1067             # wrong data type
1068 0           croak('\'delta\' must be a scalar or a 1-D array reference');
1069            
1070             }
1071            
1072             }
1073              
1074             # update arrays
1075 0           _update_clut_exp($self);
1076              
1077             }
1078              
1079             # make new nMIX object from ICC::Support::Chart object
1080             # chart must contain device values and, spectral or XYZ values
1081             # copies corner points into clut array, warns if corner point(s) missing
1082             # parameters: (ref_to_object, ref_to_chart, [columns])
1083             sub _new_from_chart {
1084              
1085             # get parameters
1086 0     0     my ($self, $chart, $cols) = @_;
1087              
1088             # local variables
1089 0           my ($dev, $fmt, $devc, $cs);
1090              
1091             # verify chart has device values
1092 0 0         ($dev = $chart->device()) or croak('chart must have device values');
1093              
1094             # verify clut column slice is defined
1095 0 0 0       (defined($cols) || ($cols = $chart->spectral() || $chart->xyz())) or croak('clut data slice is undefined');
      0        
1096              
1097             # get format keys
1098 0           $fmt = $chart->fmt_keys($cols);
1099              
1100             # verify spectral or XYZ data
1101 0 0 0       ((@{$fmt} == grep {m/^(?:(.*)\|)?(?:nm|SPECTRAL_NM_|SPECTRAL_NM|SPECTRAL_|NM_|R_)\d{3}$/} @{$fmt}) || (3 == grep {m/^(?:(.*)\|)?XYZ_[XYZ]$/} @{$fmt})) || warn('clut data neither spectral nor XYZ');
  0            
  0            
  0            
  0            
  0            
1102              
1103             # for each corner point
1104 0           for my $i (0 .. 2**@{$dev} - 1) {
  0            
1105            
1106             # for each device channel
1107 0           for my $j (0 .. $#{$dev}) {
  0            
1108            
1109             # get device value
1110 0           $devc->[$j] = $i >> $j & 1;
1111            
1112             }
1113            
1114             # get corner point samples
1115 0 0   0     ($cs = $chart->ramp(sub {@{$devc} == grep {$devc->[$_] == $_[$_]} (0 .. $#{$devc})})) or croak("no chart samples for corner point [@{$devc}]\n");
  0            
  0            
  0            
  0            
  0            
1116            
1117             # save clut vector
1118 0           $self->[1][$i] = $chart->slice($chart->add_avg($cs), $cols)->[0];
1119            
1120             # discard avg sample
1121 0           pop(@{$chart->array()});
  0            
1122            
1123             # for each spectral value
1124 0           for my $j (0 .. $#{$self->[1][$i]}) {
  0            
1125            
1126             # if value is negative
1127 0 0         if ($self->[1][$i][$j] < 0) {
1128            
1129             # set value to zero
1130 0           $self->[1][$i][$j] = 0;
1131            
1132             # print warning
1133 0           print "clut value [$i][$j] was negative, set to 0\n";
1134            
1135             }
1136            
1137             }
1138            
1139             }
1140              
1141             # bless clut
1142 0           bless($self->[1], 'Math::Matrix');
1143              
1144             }
1145              
1146             1;