——package
ICC::Support::spline;
use
strict;
use
Carp;
our
$VERSION
= 0.12;
# revised 2018-09-04
#
# Copyright © 2004-2020 by William B. Birkett
# inherit from Shared
# add complex math functions
# enable static variables
# create new spline object
# hash keys are: 'input', 'output', 'type', 'damp', 'fix_hl', 'fix_sh'
# default values for 'input' and 'output' are [0, 1]
# parameters: ([ref_to_attribute_hash])
# returns: (ref_to_object)
sub
new {
# get object class
my
$class
=
shift
();
# create empty spline object
my
$self
= [
{},
# object header
[],
# input values
[],
# output values
[],
# derivative values
[],
# min/max output values
[],
# parametric derivative matrix
];
# if one parameter, a hash reference
if
(
@_
== 1 &&
ref
(
$_
[0]) eq
'HASH'
) {
# set object contents from hash
_new_from_hash(
$self
,
$_
[0]);
}
elsif
(
@_
) {
# error
croak(
'invalid parameter(s)'
);
}
# return blessed object
return
(
bless
(
$self
,
$class
));
}
# create inverse object
# swaps the input and output arrays
# returns: (ref_to_object)
sub
inv {
# clone new object
my
$inv
= Storable::dclone(
shift
());
# get min/max values
my
@s
= _minmax(
$inv
);
# verify object is monotonic
(!
@s
) or croak(
"cannot invert a non-monotonic 'spline' object"
);
# if input is a range
if
(@{
$inv
->[1]} == 2 && @{
$inv
->[2]} > 2) {
# interpolate input array
$inv
->[1] = [
map
{(1 -
$_
/$
#{$inv->[2]}) * $inv->[1][0] + $_/$#{$inv->[2]} * $inv->[1][-1]} (0 .. $#{$inv->[2]})];
}
# swap input and output arrays
(
$inv
->[1],
$inv
->[2]) = (
$inv
->[2],
$inv
->[1]);
# clear parameteric derivative matrix
$inv
->[5] = [];
# sort input/output values
_sort_values(
$inv
);
# compute knot derivatives
_objderv(
$inv
);
# add segment min/max y-values
_minmax(
$inv
);
# return inverted object
return
(
$inv
);
}
# write spline object to ICC profile
# note: writes an equivalent 'curv' object
# parameters: (ref_to_parent_object, file_handle, ref_to_tag_table_entry)
sub
write_fh {
# verify 4 parameters
(
@_
== 4) or croak(
'wrong number of parameters'
);
# write spline data to profile
goto
&_writeICCspline
;
}
# get tag size (for writing to profile)
# note: writes an equivalent curv object
# returns: (tag_size)
sub
size {
# get parameters
my
(
$self
) =
@_
;
# get count value (defaults to 4096, the maximum allowed in an 'mft2' tag)
my
$n
=
$self
->[0]{
'curv_points'
} // 4096;
# return size
return
(12 +
$n
* 2);
}
# compute curve function
# parameters: (input_value)
# returns: (output_value)
sub
transform {
# get parameters
my
(
$self
,
$in
) =
@_
;
# local variable
my
(
$low
);
# if an extrapolated solution (s < 0)
if
(
$in
<
$self
->[1][0]) {
# return extrapolated value
return
(
$self
->[2][0] +
$self
->[3][0] * (
$in
-
$self
->[1][0]));
# if an extrapolated solution (s >= 1)
}
elsif
(
$in
>=
$self
->[1][-1]) {
# return extrapolated value
return
(
$self
->[2][-1] +
$self
->[3][-1] * (
$in
-
$self
->[1][-1]));
# if input array contains x-values
}
elsif
($
#{$self->[1]} > 1) {
# initilize segment
$low
= 0;
# while input value > upper knot value
while
(
$in
>
$self
->[1][
$low
+ 1]) {
# increment segment
$low
++;
}
# return interpolated value
return
(_fwd(
$self
, (
$self
->[1][
$low
] -
$in
)/(
$self
->[1][
$low
] -
$self
->[1][
$low
+ 1]),
$low
));
# input array contains range
}
else
{
# return interpolated value
return
(_fwd(
$self
, POSIX::modf($
#{$self->[2]} * ($in - $self->[1][0])/($self->[1][1] - $self->[1][0]))));
}
}
# compute inverse curve function
# note: there may be multiple solutions
# parameters: (input_value)
# returns: (output_value -or- array_of_output_values)
sub
inverse {
# get parameters
my
(
$self
,
$in
) =
@_
;
# local variables
my
(
$ix
,
@sol
,
@t
);
# get output array upper index
$ix
= $
#{$self->[2]};
# if an extrapolated solution (s < 0)
if
((
$in
<
$self
->[2][0] &&
$self
->[3][0] > 0) || (
$in
>
$self
->[2][0] &&
$self
->[3][0] < 0)) {
# add solution
push
(
@sol
,
$self
->[1][0] + (
$in
-
$self
->[2][0])/
$self
->[3][0]);
}
# for each segment (0 <= s < 1)
for
my
$i
(0 ..
$ix
- 1) {
# if input is lower knot
if
(
$in
==
$self
->[2][
$i
]) {
# add solution (x-values : range)
push
(
@sol
, ($
#{$self->[1]} > 1) ? $self->[1][$i] : ($self->[1][0] * ($ix - $i) + $self->[1][1] * $i)/$ix);
}
# if input lies within the y-value range
if
(
$in
>=
$self
->[4][
$i
][0] &&
$in
<=
$self
->[4][
$i
][1]) {
# compute inverse values (t-values)
@t
= _rev(
$self
,
$in
,
$i
);
# if input array contains x-values
if
($
#{$self->[1]} > 1) {
# add solution(s)
push
(
@sol
,
map
{(1 -
$_
) *
$self
->[1][
$i
] +
$_
*
$self
->[1][
$i
+ 1]}
@t
);
# input array contains range
}
else
{
# add solution(s)
push
(
@sol
,
map
{(
$self
->[1][0] * (
$ix
-
$i
-
$_
) +
$self
->[1][1] * (
$i
+
$_
))/
$ix
}
@t
);
}
}
}
# if input is last knot (s == 1)
if
(
$in
==
$self
->[2][-1]) {
# add solution
push
(
@sol
,
$self
->[1][-1]);
}
# if an extrapolated solution (s > 1)
if
((
$in
>
$self
->[2][-1] &&
$self
->[3][-1] > 0) || (
$in
<
$self
->[2][-1] &&
$self
->[3][-1] < 0)) {
# add solution
push
(
@sol
,
$self
->[1][-1] + (
$in
-
$self
->[2][-1])/
$self
->[3][-1]);
}
# return result (array or first solution)
return
(
wantarray
?
@sol
:
$sol
[0]);
}
# compute curve derivative
# parameters: (input_value)
# returns: (derivative_value)
sub
derivative {
# get parameters
my
(
$self
,
$in
) =
@_
;
# local variable
my
(
$low
);
# if an extrapolated solution (s < 0)
if
(
$in
<
$self
->[1][0]) {
# return endpoint derivative
return
(
$self
->[3][0]);
# if an extrapolated solution (s >= 1)
}
elsif
(
$in
>=
$self
->[1][-1]) {
# return endpoint derivative
return
(
$self
->[3][-1]);
# if input array contains x-values
}
elsif
($
#{$self->[1]} > 1) {
# initilize segment
$low
= 0;
# while input value > upper knot value
while
(
$in
>
$self
->[1][
$low
+ 1]) {
# increment segment
$low
++;
}
# return interpolated derivative value
return
(_derv(
$self
, (
$self
->[1][
$low
] -
$in
)/(
$self
->[1][
$low
] -
$self
->[1][
$low
+ 1]),
$low
));
# input array contains range
}
else
{
# return interpolated derivative value
return
(_derv(
$self
, POSIX::modf($
#{$self->[2]} * ($in - $self->[1][0])/($self->[1][1] - $self->[1][0]))));
}
}
# compute parametric partial derivatives
# parameters: (input_value)
# returns: (partial_derivative_array)
sub
parametric {
# get parameters
my
(
$self
,
$in
) =
@_
;
# local variables
my
(
$dx
,
$low
,
$t
,
$tc
,
$h00
,
$h01
,
$h10
,
$h11
,
@pj
);
# update parametric derivative matrix, if necessary
_objpara(
$self
)
if
(!
defined
(
$self
->[5]) || $
#{$self->[5]} != $#{$self->[2]});
# if an extrapolated solution (s < 0)
if
(
$in
<
$self
->[1][0]) {
# for each output value (parameter)
for
my
$i
(0 .. $
#{$self->[2]}) {
# compute partial derivative
$pj
[
$i
] = (
$i
== 0) +
$self
->[5][0][
$i
] * (
$in
-
$self
->[1][0]);
}
# if an extrapolated solution (s >= 1)
}
elsif
(
$in
>=
$self
->[1][-1]) {
# for each output value (parameter)
for
my
$i
(0 .. $
#{$self->[2]}) {
# compute partial derivative
$pj
[
$i
] = (
$i
== $
#{$self->[2]}) + $self->[5][-1][$i] * ($in - $self->[1][-1]);
}
}
else
{
# if input array contains x-values
if
($
#{$self->[1]} > 1) {
# initialize segment
$low
= 0;
# while input value > upper knot value
while
(
$in
>
$self
->[1][
$low
+ 1]) {
# increment segment
$low
++;
}
# compute delta x-value
$dx
=
$self
->[1][
$low
+ 1] -
$self
->[1][
$low
];
# compute t-value
$t
= (
$in
-
$self
->[1][
$low
])/
$dx
;
# input array contains range
}
else
{
# compute delta x-value
$dx
= (
$self
->[1][1] -
$self
->[1][0])/$
#{$self->[2]};
# compute t-value, index
(
$t
,
$low
) = POSIX::modf((
$in
-
$self
->[1][0])/
$dx
);
}
# compute Hermite coefficients
$tc
= 1 -
$t
;
$h00
= (1 + 2 *
$t
) *
$tc
*
$tc
;
$h01
= 1 -
$h00
;
$h10
=
$t
*
$tc
*
$tc
;
$h11
= -
$t
*
$t
*
$tc
;
# for each output value (parameter)
for
my
$i
(0 .. $
#{$self->[2]}) {
# compute partial derivative
$pj
[
$i
] =
$h00
* (
$low
==
$i
) +
$h01
* (
$low
+ 1 ==
$i
) +
$h10
*
$dx
*
$self
->[5][
$low
][
$i
] +
$h11
*
$dx
*
$self
->[5][
$low
+ 1][
$i
];
}
}
# return
return
(
@pj
);
}
# get/set header hash reference
# parameters: ([ref_to_new_hash])
# returns: (ref_to_hash)
sub
header {
# get object reference
my
$self
=
shift
();
# if there are parameters
if
(
@_
) {
# if one parameter, a hash reference
if
(
@_
== 1 &&
ref
(
$_
[0]) eq
'HASH'
) {
# set header to copy of hash
$self
->[0] = Storable::dclone(
shift
());
}
else
{
# error
croak(
'parameter must be a hash reference'
);
}
}
# return reference
return
(
$self
->[0]);
}
# get/set input array reference
# an array of two values is a range
# otherwise, it contains input values
# parameters: ([ref_to_array])
# returns: (ref_to_array)
sub
input {
# get object reference
my
$self
=
shift
();
# if one parameter supplied
if
(
@_
== 1) {
# verify 'input' vector (two or more numeric values)
(ICC::Shared::is_num_vector(
$_
[0]) && 2 <= @{
$_
[0]}) or croak(
'invalid input values'
);
# verify 'input' vector size
(@{
$_
[0]} == 2 || @{
$_
[0]} == @{
$self
->[2]}) or carp(
'number of input values differs from object output values'
);
# store output values
$self
->[1] = [@{
$_
[0]}];
# update object internal elements
update(
$self
, 1);
}
elsif
(
@_
) {
# error
carp(
'too many parameters'
);
}
# return array reference
return
(
$self
->[1]);
}
# get/set output array reference
# parameters: ([ref_to_array])
# returns: (ref_to_array)
sub
output {
# get object reference
my
$self
=
shift
();
# if one parameter supplied
if
(
@_
== 1) {
# verify 'output' vector (two or more numeric values)
(ICC::Shared::is_num_vector(
$_
[0]) && 2 <= @{
$_
[0]}) or croak(
'invalid output values'
);
# verify 'output' vector size
(@{
$self
->[1]} == 2 || @{
$_
[0]} == @{
$self
->[1]}) or carp(
'number of output values differs from object input values'
);
# store output values
$self
->[2] = [@{
$_
[0]}];
# update object internal elements
update(
$self
);
}
elsif
(
@_
) {
# error
carp(
'too many parameters'
);
}
# return array reference
return
(
$self
->[2]);
}
# normalize transform
# adjusts Bernstein coefficients linearly
# adjusts 'input' and 'output' by default
# hash keys: 'input', 'output'
# parameter: ([hash_ref])
# returns: (ref_to_object)
sub
normalize {
# get parameters
my
(
$self
,
$hash
) =
@_
;
# local variables
my
(
$m
,
$b
,
$val
,
$src
,
$x0
,
$x1
,
$y0
,
$y1
,
$f0
,
$f1
,
@minmax
,
$p
);
# set hash key array
my
@key
=
qw(input output)
;
# for ('input', 'output')
for
my
$i
(0, 1) {
# undefine slope
$m
=
undef
;
# if no parameter (default)
if
(!
defined
(
$hash
)) {
# if array has 2 or more coefficients
if
(@{
$self
->[
$i
+ 1]} > 1 && (
$x0
=
$self
->[
$i
+ 1][0]) != (
$x1
=
$self
->[
$i
+ 1][-1])) {
# special case, if 'output' and 'fix_hl' was called
$x0
= 0
if
(
$i
&&
defined
(
$self
->[0]{
'hl_retention'
}));
# compute adjustment values
$m
=
abs
(1/(
$x0
-
$x1
));
$b
= -
$m
* (
$x0
<
$x1
?
$x0
:
$x1
);
# set endpoints
(
$y0
,
$y1
) =
$x0
<
$x1
? (0, 1) : (1, 0);
}
}
else
{
# if hash contains key
if
(
defined
(
$val
=
$hash
->{
$key
[
$i
]})) {
# if array has 2 or more coefficients
if
(@{
$self
->[
$i
+ 1]} > 1 &&
$self
->[
$i
+ 1][0] !=
$self
->[
$i
+ 1][-1]) {
# if hash value is an array reference
if
(
ref
(
$val
) eq
'ARRAY'
) {
# if two parameters
if
(@{
$val
} == 2) {
# get parameters
(
$y0
,
$y1
) = @{
$val
};
# get endpoints
$x0
=
$self
->[
$i
+ 1][0];
$x1
=
$self
->[
$i
+ 1][-1];
# if three parameters
}
elsif
(@{
$val
} == 3) {
# get parameters
(
$src
,
$y0
,
$y1
) = @{
$val
};
# if source is 'endpoints'
if
(!
ref
(
$src
) &&
$src
eq
'endpoints'
) {
# get endpoints
$x0
=
$self
->[
$i
+ 1][0];
$x1
=
$self
->[
$i
+ 1][-1];
# if source is 'minmax'
}
elsif
(!
ref
(
$src
) &&
$src
eq
'minmax'
) {
# if 'output' curve
if
(
$i
) {
# get all min/max values
@minmax
= (
map
{@{
$_
}} @{
$self
->[4]});
# get min/max values
$x0
= List::Util::min(
@minmax
);
$x1
= List::Util::max(
@minmax
);
# if 'input' curve
}
else
{
# get min/max values (endpoints)
$x0
=
$self
->[1][0];
$x1
=
$self
->[1][-1];
}
}
# if four parameters
}
elsif
(@{
$val
} == 4) {
# get parameters
(
$x0
,
$x1
,
$y0
,
$y1
) = @{
$val
};
}
# if x/y values are numeric
if
((4 ==
grep
{Scalar::Util::looks_like_number(
$_
)} (
$x0
,
$x1
,
$y0
,
$y1
)) && (
$x0
!=
$x1
)) {
# compute slope
$m
= (
$y0
-
$y1
)/(
$x0
-
$x1
);
# compute offset
$b
=
$y0
-
$m
*
$x0
;
}
else
{
# warning
carp(
"invalid '$key[$i]' parameter(s)"
);
}
}
}
else
{
# warning
carp(
"can't normalize '$key[$i]' coefficients"
);
}
}
}
# if slope is defined
if
(
defined
(
$m
)) {
# set endpoint flags
$f0
=
$self
->[
$i
+ 1][0] ==
$x0
;
$f1
=
$self
->[
$i
+ 1][-1] ==
$x1
;
# adjust Bernstein coefficients (y = mx + b)
@{
$self
->[
$i
+ 1]} =
map
{
$m
*
$_
+
$b
} @{
$self
->[
$i
+ 1]};
# set endpoints (to ensure exact values)
$self
->[
$i
+ 1][0] =
$y0
if
(
$f0
);
$self
->[
$i
+ 1][-1] =
$y1
if
(
$f1
);
# save input polarity
$p
=
$m
< 0
if
(
$i
== 0);
# if 'input' and 'hl_retention' is defined
if
(!
$i
&&
defined
(
$val
=
$self
->[0]{
'hl_retention'
})) {
# adjust value
$self
->[0]{
'hl_retention'
} =
$m
*
$val
+
$b
;
}
# if 'output' and 'hl_original' is defined
if
(
$i
&&
defined
(
$val
=
$self
->[0]{
'hl_original'
})) {
# adjust values
$self
->[0]{
'hl_original'
} = [
map
{
$m
*
$_
+
$b
} @{
$val
}];
}
# if 'output' and 'sh_original' is defined
if
(
$i
&&
defined
(
$val
=
$self
->[0]{
'sh_original'
})) {
# adjust values
$self
->[0]{
'sh_original'
} = [
map
{
$m
*
$_
+
$b
} @{
$val
}];
}
}
}
# update object internals, sorting values if 'input' polarity changed
update(
$self
,
$p
);
# return
return
(
$self
);
}
# check if monotonic
# returns min/max values
# returns s-values by default
# returns input-values if format is 'input'
# returns output-values if format is 'output'
# curve is monotonic if no min/max values
# parameters: ([format])
# returns: (array_of_values)
sub
monotonic {
# get object reference
my
(
$self
,
$fmt
) =
@_
;
# local variables
my
(
$t
,
$low
);
# if format undefined
if
(!
defined
(
$fmt
)) {
# return s-values
return
(_minmax(
$self
));
# if format 'input'
}
elsif
(
$fmt
eq
'input'
) {
# if input array contains x-values
if
($
#{$self->[1]} > 1) {
# return input values
return
(
map
{(
$t
,
$low
) = POSIX::modf(
$_
); (1 -
$t
) *
$self
->[1][
$low
] +
$t
*
$self
->[1][
$low
+ 1]} _minmax(
$self
));
# input array contains range
}
else
{
# return input values
return
(
map
{
$t
=
$_
/$
#{$self->[2]}; (1 - $t) * $self->[1][0] + $t * $self->[1][1]} _minmax($self));
}
# if format 'output'
}
elsif
(
$fmt
eq
'output'
) {
# return output values
return
(
map
{_fwd(
$self
, POSIX::modf(
$_
))} _minmax(
$self
));
}
else
{
# error
croak(
'unsupported format for min/max values'
);
}
}
# update internal object elements
# this method used primarily when optimizing
# call after changing 'input' or 'output' values
# if the 'input' values were changed, set the flag
# parameter: ([flag])
sub
update {
# get parameters
my
(
$self
,
$flag
) =
@_
;
# sort values if flag set
_sort_values(
$self
)
if
(
$flag
);
# recompute knot derivatives
_objderv(
$self
);
# recompute segment min/max y-values
_minmax(
$self
);
# recompute parametric derivative matrix if flag set, or 'akima' type spline
_objpara(
$self
)
if
(
$flag
|| (
defined
(
$self
->[0]{
'type'
} &&
$self
->[0]{
'type'
} eq
'akima'
)));
}
# make table for 'curv' objects
# assumes curve domain/range is (0 - 1)
# parameters: (number_of_table_entries, [direction])
# returns: (ref_to_table_array)
sub
table {
# get parameters
my
(
$self
,
$n
,
$dir
) =
@_
;
# local variables
my
(
$up
,
$table
);
# validate number of table entries
(
$n
==
int
(
$n
) &&
$n
>= 2) or carp(
'invalid number of table entries'
);
# purify direction flag
$dir
= (
$dir
) ? 1 : 0;
# array upper index
$up
=
$n
- 1;
# for each table entry
for
my
$i
(0 ..
$up
) {
# compute table value
$table
->[
$i
] = _transform(
$self
,
$dir
,
$i
/
$up
);
}
# return table reference
return
(
$table
);
}
# make equivalent 'curv' object
# assumes curve domain/range is (0 - 1)
# parameters: (number_of_table_entries, [direction])
# returns: (ref_to_curv_object)
sub
curv {
# return 'curv' object reference
return
(ICC::Profile::curv->new(table(
@_
)));
}
# print object contents to string
# format is an array structure
# parameter: ([format])
# returns: (string)
sub
sdump {
# get parameters
my
(
$self
,
$p
) =
@_
;
# local variables
my
(
$s
,
$fmt
,
$fmt2
);
# resolve parameter to an array reference
$p
=
defined
(
$p
) ?
ref
(
$p
) eq
'ARRAY'
?
$p
: [
$p
] : [];
# get format string
$fmt
=
defined
(
$p
->[0]) && !
ref
(
$p
->[0]) ?
$p
->[0] :
'undef'
;
# set string to object ID
$s
=
sprintf
(
"'%s' object, (0x%x)\n"
,
ref
(
$self
),
$self
);
# if input_parameter_array defined
if
(
defined
(
$self
->[1])) {
# set format
$fmt2
=
join
(
', '
, (
'%.3f'
) x @{
$self
->[1]});
# append parameter string
$s
.=
sprintf
(
" input parameters [$fmt2]\n"
, @{
$self
->[1]});
}
# if output_parameter_array defined
if
(
defined
(
$self
->[2])) {
# set format
$fmt2
=
join
(
', '
, (
'%.3f'
) x @{
$self
->[2]});
# append parameter string
$s
.=
sprintf
(
" output parameters [$fmt2]\n"
, @{
$self
->[2]});
}
# return
return
(
$s
);
}
# combined transform
# direction: 0 - normal, 1 - inverse
# parameters: (ref_to_object, direction, input_value)
# returns: (output_value)
sub
_transform {
# get parameters
my
(
$self
,
$dir
,
$in
) =
@_
;
# if inverse direction
if
(
$dir
) {
# return inverse
return
(
scalar
(inverse(
$self
,
$in
)));
}
else
{
# return transform
return
(transform(
$self
,
$in
));
}
}
# combined derivative
# direction: 0 - normal, 1 - inverse
# parameters: (ref_to_object, direction, input_value)
# returns: (derivative_value)
sub
_derivative {
# get parameters
my
(
$self
,
$dir
,
$in
) =
@_
;
# if inverse direction
if
(
$dir
) {
# unimplemented function error
croak(
'unimplemented function'
);
}
else
{
# return derivative
return
(derivative(
$self
,
$in
));
}
}
# compute knot derivatives
# parameter: (ref_to_object)
sub
_objderv {
# get parameter
my
$self
=
shift
();
# verify input values are unique
_unique(
$self
->[1]) or croak(
'input values not unique'
);
# if spline type undefined or 'natural'
if
(!
defined
(
$self
->[0]{
'type'
}) ||
$self
->[0]{
'type'
} eq
'natural'
) {
# compute knot derivatives for natural spline
_natural_derv(
$self
)
# if spline type is 'akima'
}
elsif
(
$self
->[0]{
'type'
} eq
'akima'
) {
# compute knot derivatives for Akima spline
_akima_derv(
$self
);
}
else
{
# error
croak(
'undefined spline type'
);
}
}
# compute knot derivatives for natural spline
# parameter: (ref_to_object)
sub
_natural_derv {
# get parameter
my
$self
=
shift
();
# local variables
my
(
$ix
,
$rhs
,
$dx
,
$info
,
$derv
);
# verify object has two or more knots
((
$ix
= $
#{$self->[2]}) > 0) or croak('object must have two or more knots');
# check if ICC::Support::Lapack module is loaded
state
$lapack
=
defined
(
$INC
{
'ICC/Support/Lapack.pm'
});
# make empty Math::Matrix object
$rhs
=
bless
([],
'Math::Matrix'
);
# if input array contains input values
if
($
#{$self->[1]} > 1) {
# for each inner element
for
my
$i
(1 ..
$ix
- 1) {
# compute rhs (6 * (y[i + 1] - y[i - 1])/(x[i + 1] - x[i - 1]))
$rhs
->[
$i
][0] = 6 * (
$self
->[2][
$i
+ 1] -
$self
->[2][
$i
- 1])/(
$self
->[1][
$i
+ 1] -
$self
->[1][
$i
- 1]);
}
# set rhs endpoint values
$rhs
->[0][0] = 3 * (
$self
->[2][1] -
$self
->[2][0])/(
$self
->[1][1] -
$self
->[1][0]);
$rhs
->[
$ix
][0] = 3 * (
$self
->[2][
$ix
] -
$self
->[2][
$ix
- 1])/(
$self
->[1][
$ix
] -
$self
->[1][
$ix
- 1]);
# input array contains range
}
else
{
# compute x-segment length
$dx
= (
$self
->[1][1] -
$self
->[1][0])/
$ix
;
# for each inner element
for
my
$i
(1 ..
$ix
- 1) {
# compute rhs (6 * (y[i + 1] - y[i - 1]))/(x[i + 1] - x[i - 1])
$rhs
->[
$i
][0] = 3 * (
$self
->[2][
$i
+ 1] -
$self
->[2][
$i
- 1])/
$dx
;
}
# set rhs endpoint values
$rhs
->[0][0] = 3 * (
$self
->[2][1] -
$self
->[2][0])/
$dx
;
$rhs
->[
$ix
][0] = 3 * (
$self
->[2][
$ix
] -
$self
->[2][
$ix
- 1])/
$dx
;
}
# if ICC::Support::Lapack module is loaded
if
(
$lapack
) {
# solve for derivative matrix
(
$info
,
$derv
) = ICC::Support::Lapack::trisolve([(1) x
$ix
], [2, (4) x (
$ix
- 1), 2], [(1) x
$ix
],
$rhs
);
# otherwise, use Math::Matrix module
}
else
{
# solve for derivative matrix
$derv
= Math::Matrix->tridiagonal([2, (4) x (
$ix
- 1), 2])->concat(
$rhs
)->solve();
}
# for each knot
for
my
$i
(0 ..
$ix
) {
# set derivative value
$self
->[3][
$i
] =
$derv
->[
$i
][0];
}
}
# compute knot derivatives for Akima spline
# parameter: (ref_to_object)
sub
_akima_derv {
# get parameter
my
$self
=
shift
();
# local variables
my
(
$ix
,
@m
,
$s
,
$b
,
$d1
,
$d2
);
# verify object has two or more knots
((
$ix
= $
#{$self->[2]}) > 0) or croak('object must have two or more knots');
# compute uniform segment length (for 'input' range)
$s
= (
$self
->[1][1] -
$self
->[1][0])/
$ix
if
($
#{$self->[1]} == 1);
# for each segment
for
my
$i
(0 .. (
$ix
- 1)) {
# compute segment slope (range // x-values)
$m
[
$i
+ 2] = (
$self
->[2][
$i
+ 1] -
$self
->[2][
$i
])/(
$s
// (
$self
->[1][
$i
+ 1] -
$self
->[1][
$i
]));
}
# if 2 points
if
(
$ix
== 1) {
# linear slopes
$m
[0] =
$m
[1] =
$m
[3] =
$m
[4] =
$m
[2];
# if 3 or more points
}
else
{
# quadratic slopes
$m
[1] = 2 *
$m
[2] -
$m
[3];
$m
[0] = 2 *
$m
[1] -
$m
[2];
$m
[
$ix
+ 2] = 2 *
$m
[
$ix
+ 1] -
$m
[
$ix
];
$m
[
$ix
+ 3] = 2 *
$m
[
$ix
+ 2] -
$m
[
$ix
+ 1];
}
# if damping value is undefined
if
(!
defined
(
$self
->[0]{
'damp'
})) {
# set default damping value
$self
->[0]{
'damp'
} = List::Util::sum(
map
{
$_
*
$_
}
@m
)/(
@m
* 50);
}
# Akima used abs() functions to calulate the slopes
# this can lead to abrupt changes in curve shape and parametric derivatives
# we replace abs(x) with sqrt(x^2 + b) to add stability
# get damping value and verify >= 0
((
$b
=
$self
->[0]{
'damp'
}) >= 0) or carp(
'akima damping value is negative'
);
# compute Akima derivative
for
my
$i
(0 ..
$ix
) {
# compute slope differences (with damping)
$d1
=
sqrt
((
$m
[
$i
+ 1] -
$m
[
$i
])**2 +
$b
);
$d2
=
sqrt
((
$m
[
$i
+ 3] -
$m
[
$i
+ 2])**2 +
$b
);
# if denominator not 0
if
(
$d1
+
$d2
) {
# use Akima slope (weighted average with damping)
$self
->[3][
$i
] = (
$d2
*
$m
[
$i
+ 1] +
$d1
*
$m
[
$i
+ 2])/(
$d1
+
$d2
);
}
else
{
# otherwise, use average slope of adjoining segments
$self
->[3][
$i
] = (
$m
[
$i
+ 1] +
$m
[
$i
+ 2])/2;
}
}
}
# compute partial derivative matrix
# parameter: (ref_to_object)
sub
_objpara {
# get parameter
my
$self
=
shift
();
# verify input values are unique
_unique(
$self
->[1]) or croak(
'input values not unique'
);
# if spline type undefined or 'natural'
if
(!
defined
(
$self
->[0]{
'type'
}) ||
$self
->[0]{
'type'
} eq
'natural'
) {
# compute knot derivatives for natural spline
_natural_para(
$self
)
# if spline type is 'akima'
}
elsif
(
$self
->[0]{
'type'
} eq
'akima'
) {
# compute knot derivatives for Akima spline
_akima_para(
$self
);
}
else
{
# error
croak(
'undefined spline type'
);
}
}
# compute partial derivative matrix for natural spline
# parameter: (ref_to_object)
sub
_natural_para {
# get parameter
my
$self
=
shift
();
# local variables
my
(
$ix
,
$n
,
$rhs
,
$x
,
$info
,
$derv
);
# verify object has two or more knots
((
$ix
= $
#{$self->[2]}) > 0) or croak('object must have two or more knots');
# check if ICC::Support::Lapack module is loaded
state
$lapack
=
defined
(
$INC
{
'ICC/Support/Lapack.pm'
});
# make rhs matrix (filled with zeros)
$rhs
=
$lapack
? ICC::Support::Lapack::zeros(
$ix
+ 1) :
bless
([
map
{[(0) x (
$ix
+ 1)]} (0 ..
$ix
)],
'Math::Matrix'
);
# if input array contains input values
if
($
#{$self->[1]} > 1) {
# for each inner element
for
my
$i
(1 ..
$ix
- 1) {
# set rhs diagonal values
$rhs
->[
$i
][
$i
- 1] =
$x
= 6/(
$self
->[1][
$i
- 1] -
$self
->[1][
$i
+ 1]);
$rhs
->[
$i
][
$i
+ 1] = -
$x
;
}
# set rhs endpoint values
$rhs
->[0][0] =
$x
= 3/(
$self
->[1][0] -
$self
->[1][1]);
$rhs
->[0][1] = -
$x
;
$rhs
->[
$ix
][
$ix
] =
$x
= 3/(
$self
->[1][
$ix
] -
$self
->[1][
$ix
- 1]);
$rhs
->[
$ix
][
$ix
-1] = -
$x
;
# input array contains range
}
else
{
# compute rhs value
$x
= 3 *
$ix
/(
$self
->[1][1] -
$self
->[1][0]);
# for each inner element
for
my
$i
(1 ..
$ix
- 1) {
# set rhs diagonal values
$rhs
->[
$i
- 1][
$i
] =
$x
;
$rhs
->[
$i
+ 1][
$i
] = -
$x
;
}
# set rhs endpoint values
$rhs
->[0][0] = -
$x
;
$rhs
->[1][0] = -
$x
;
$rhs
->[
$ix
][
$ix
] =
$x
;
$rhs
->[
$ix
- 1][
$ix
] =
$x
;
}
# if ICC::Support::Lapack module is loaded
if
(
$lapack
) {
# solve for derivative matrix
(
$info
,
$derv
) = ICC::Support::Lapack::trisolve([(1) x
$ix
], [2, (4) x (
$ix
- 1), 2], [(1) x
$ix
],
$rhs
);
# set object
$self
->[5] =
bless
(
$derv
,
'Math::Matrix'
);
# otherwise, use Math::Matrix module
}
else
{
# solve for derivative matrix
$self
->[5] = Math::Matrix->tridiagonal([2, (4) x (
$ix
- 1), 2])->concat(
$rhs
)->solve();
}
}
# compute partial derivative matrix for Akima spline
# parameter: (ref_to_object)
sub
_akima_para {
# get object
my
$self
=
shift
();
# local variables
my
(
$ix
,
$s
,
@m
,
@x
,
$b
);
my
(
$d1
,
$d2
,
$dif
,
@dm
,
$dd1
,
$dd2
,
$derv
);
# see 'akima_parametric.plx' for details on this function
# verify object has two or more knots
((
$ix
= $
#{$self->[2]}) > 0) or croak('object must have two or more knots');
# compute uniform segment length (for 'input' range)
$s
= (
$self
->[1][1] -
$self
->[1][0])/
$ix
if
($
#{$self->[1]} == 1);
# for each segment
for
my
$i
(0 .. (
$ix
- 1)) {
# compute segment x-length
$x
[
$i
+ 2] =
$s
// (
$self
->[1][
$i
+ 1] -
$self
->[1][
$i
]);
# compute segment slope (range // x-values)
$m
[
$i
+ 2] = (
$self
->[2][
$i
+ 1] -
$self
->[2][
$i
])/
$x
[
$i
+ 2];
}
# if 2 points
if
(
$ix
== 1) {
# linear slopes
$m
[0] =
$m
[1] =
$m
[3] =
$m
[4] =
$m
[2];
# if 3 or more points
}
else
{
# quadratic slopes
$m
[1] = 2 *
$m
[2] -
$m
[3];
$m
[0] = 2 *
$m
[1] -
$m
[2];
$m
[
$ix
+ 2] = 2 *
$m
[
$ix
+ 1] -
$m
[
$ix
];
$m
[
$ix
+ 3] = 2 *
$m
[
$ix
+ 2] -
$m
[
$ix
+ 1];
}
# get the damping value
$b
=
$self
->[0]{
'damp'
};
# for each node
for
my
$i
(0 ..
$ix
) {
# add row of zeros
$derv
->[
$i
] = [(0) x (
$ix
+ 1)];
}
# if 2 knots
if
(
$ix
== 1) {
# set partial derivatives
$derv
= [[-1/
$x
[2], 1/
$x
[2]], [-1/
$x
[2], 1/
$x
[2]]];
}
else
{
# for each row
for
my
$i
(0 ..
$ix
) {
# compute slope differences (with damping)
$d1
=
sqrt
((
$m
[
$i
+ 1] -
$m
[
$i
])**2 +
$b
);
$d2
=
sqrt
((
$m
[
$i
+ 3] -
$m
[
$i
+ 2])**2 +
$b
);
# for each column
for
my
$j
(0 ..
$ix
) {
# compute difference
$dif
=
$j
-
$i
;
# skip outer corner regions
next
if
(
abs
(
$dif
) > 2);
# the @dm array contains (∂m0/∂n, ∂m1/∂n, ∂m2/∂n, ∂m3/∂n)
# where m0 - m3 are the chord slopes, and n is the node value
# if difference -2
if
(
$dif
== -2) {
# if row 1
if
(
$i
==
$ix
) {
# set partial derivatives
@dm
= (-1/
$x
[
$i
], 0, 1/
$x
[
$i
], 2/
$x
[
$i
]);
}
else
{
# set partial derivatives
@dm
= (-1/
$x
[
$i
], 0, 0, 0);
}
# if difference -1
}
elsif
(
$dif
== -1) {
# if row 1
if
(
$i
== 1) {
# if more than 3 knots
if
(
$ix
> 2) {
# set partial derivatives
@dm
= (-2/
$x
[
$i
+ 1], -1/
$x
[
$i
+ 1], 0, 0);
}
else
{
# set partial derivatives
@dm
= (-2/
$x
[
$i
+ 1], -1/
$x
[
$i
+ 1], 0, 1/
$x
[
$i
+ 1]);
}
# if next to last row
}
elsif
(
$i
==
$ix
- 1) {
# set partial derivatives
@dm
= (1/
$x
[
$i
], -1/
$x
[
$i
+ 1], 0, 1/
$x
[
$i
+ 1]);
# if last row
}
elsif
(
$i
==
$ix
) {
# set partial derivatives
@dm
= (1/
$x
[
$i
], -1/
$x
[
$i
+ 1], -2/
$x
[
$i
+ 1] - 1/
$x
[
$i
], -3/
$x
[
$i
+ 1] - 2/
$x
[
$i
]);
}
else
{
# set partial derivatives
@dm
= (1/
$x
[
$i
], -1/
$x
[
$i
+ 1], 0, 0);
}
# if difference 0
}
elsif
(
$dif
== 0) {
# if row 0
if
(
$i
== 0) {
# set partial derivatives
@dm
= (-3/
$x
[
$i
+ 2], -2/
$x
[
$i
+ 2], -1/
$x
[
$i
+ 2], 0);
# if row 1
}
elsif
(
$i
== 1) {
# if more than three knots
if
(
$ix
> 2) {
# set partial derivatives
@dm
= (2/
$x
[
$i
+ 1] + 1/
$x
[
$i
+ 2], 1/
$x
[
$i
+ 1], -1/
$x
[
$i
+ 2], 0);
}
else
{
# set partial derivatives
@dm
= (2/
$x
[
$i
+ 1] + 1/
$x
[
$i
+ 2], 1/
$x
[
$i
+ 1], -1/
$x
[
$i
+ 2], -2/
$x
[
$i
+ 2] - 1/
$x
[
$i
+ 1]);
}
# if next to last row
}
elsif
(
$i
==
$ix
- 1) {
# set partial derivatives
@dm
= (0, 1/
$x
[
$i
+ 1], -1/
$x
[
$i
+ 2], -2/
$x
[
$i
+ 2] - 1/
$x
[
$i
+ 1]);
# if last row
}
elsif
(
$i
==
$ix
) {
# set partial derivatives
@dm
= (0, 1/
$x
[
$i
+ 1], 2/
$x
[
$i
+ 1], 3/
$x
[
$i
+ 1]);
}
else
{
# set partial derivatives
@dm
= (0, 1/
$x
[
$i
+ 1], -1/
$x
[
$i
+ 2], 0);
}
# if difference 1
}
elsif
(
$dif
== 1) {
# if row 0
if
(
$i
== 0) {
# set partial derivatives
@dm
= (3/
$x
[
$i
+ 2] + 2/
$x
[
$i
+ 3], 2/
$x
[
$i
+ 2] + 1/
$x
[
$i
+ 3], 1/
$x
[
$i
+ 2], -1/
$x
[
$i
+ 3]);
# if row 1
}
elsif
(
$i
== 1) {
# if more than 3 knots
if
(
$ix
> 2) {
# set partial derivatives
@dm
= (-1/
$x
[
$i
+ 2], 0, 1/
$x
[
$i
+ 2], -1/
$x
[
$i
+ 3]);
}
else
{
# set partial derivatives
@dm
= (-1/
$x
[
$i
+ 2], 0, 1/
$x
[
$i
+ 2], 2/
$x
[
$i
+ 2]);
}
# if next to last row
}
elsif
(
$i
==
$ix
- 1) {
# set partial derivatives
@dm
= (0, 0, 1/
$x
[
$i
+ 2], 2/
$x
[
$i
+ 2]);
}
else
{
# set partial derivatives
@dm
= (0, 0, 1/
$x
[
$i
+ 2], -1/
$x
[
$i
+ 3]);
}
# if difference 2
}
elsif
(
$dif
== 2) {
# if row 0
if
(
$i
== 0) {
# set partial derivatives
@dm
= (-2/
$x
[
$i
+ 3], -1/
$x
[
$i
+ 3], 0, 1/
$x
[
$i
+ 3]);
}
else
{
# set partial derivatives
@dm
= (0, 0, 0, 1/
$x
[
$i
+ 3]);
}
}
# compute ∂d1/∂n
$dd1
= akima_dpd(
$m
[
$i
],
$m
[
$i
+ 1],
$dm
[0],
$dm
[1],
$d1
);
# compute ∂d2/∂n
$dd2
= akima_dpd(
$m
[
$i
+ 2],
$m
[
$i
+ 3],
$dm
[2],
$dm
[3],
$d2
);
# compute ∂s/∂n
$derv
->[
$i
][
$j
] = akima_spd(
$d1
,
$d2
,
$m
[
$i
+ 1],
$m
[
$i
+ 2],
$dd1
,
$dd2
,
$dm
[1],
$dm
[2]);
}
}
}
# set object
$self
->[5] =
bless
(
$derv
,
'Math::Matrix'
);
}
# compute partial derivative function for _akima_para
# parameters: (d1, d2, m1, m2, ∂d1/∂n, ∂d2/∂n, ∂m1/∂n, ∂m2/∂n)
# returns; (∂s/∂n)
sub
akima_spd {
=pod
https://www.wolframalpha.com/input/?i=d%2Fdx+(a(x)+*+c(x)+%2B+b(x)+*+d(x))%2F(a(x)+%2B+b(x))
d/dx((a(x) c(x) + b(x) d(x))/(a(x) + b(x))) =
(c(x) a'(x) + a(x) c'(x) + d(x) b'(x) + b(x) d'(x))/(a(x) + b(x)) - ((a'(x) + b'(x)) (a(x) c(x) + b(x) d(x)))/(a(x) + b(x))^2
=cut
# return partial derivative
return
((
$_
[2] *
$_
[5] +
$_
[1] *
$_
[6] +
$_
[3] *
$_
[4] +
$_
[0] *
$_
[7])/(
$_
[1] +
$_
[0]) - ((
$_
[5] +
$_
[4]) * (
$_
[1] *
$_
[2] +
$_
[0] *
$_
[3]))/(
$_
[1] +
$_
[0])**2);
}
# compute partial derivative function for _akima_para
# parameters: (m1, m2, ∂m1/∂n, ∂m2/∂n, d)
# returns; (∂d/∂n)
sub
akima_dpd {
=pod
https://www.wolframalpha.com/input/?i=d%2Fdx+(sqrt((a(x)+-+b(x))%5E2+%2B+c))
d/dx(sqrt((a(x) - b(x))^2 + c)) =
((a(x) - b(x)) (a'(x) - b'(x)))/sqrt((a(x) - b(x))^2 + c)
=cut
# return partial derivative
return
((
$_
[0] -
$_
[1]) * (
$_
[2] -
$_
[3])/
$_
[4]);
}
# add local min/max values
# parameter: (ref_to_object)
# returns: (s-value_array)
sub
_minmax {
# get object reference
my
$self
=
shift
();
# local variables
my
(
@t
,
@y
,
@s
);
# for each interval
for
my
$i
(0 .. $
#{$self->[2]} - 1) {
# get min/max location(s)
@t
= _local(
$self
,
$i
);
# add min/max s-values
push
(
@s
,
map
{
$_
+
$i
}
@t
);
# add inner knot s-value, if derivative is 0
push
(
@s
,
$i
)
if
(
$i
&&
$self
->[3][
$i
] == 0);
# compute local min/max y-values
@y
=
map
{_fwd(
$self
,
$_
,
$i
)}
@t
;
# add the knot values
push
(
@y
, (
$self
->[2][
$i
],
$self
->[2][
$i
+ 1]));
# sort the values
@y
=
sort
{
$a
<=>
$b
}
@y
;
# save y-value min/max span
$self
->[4][
$i
] = [
$y
[0],
$y
[-1]];
}
# return min/max s-values
return
(
sort
{
$a
<=>
$b
}
@s
);
}
# compute local min/max value(s)
# values are within the segment (t)
# parameters: (ref_to_object, segment)
# returns: (t-value_array)
sub
_local {
# get parameters
my
(
$self
,
$low
) =
@_
;
# local variables
my
(
$dx
,
$y1
,
$y2
,
$m1
,
$m2
);
my
(
$a
,
$b
,
$c
,
$dscr
,
@t
);
# compute delta x-value (x-values : range)
$dx
= ($
#{$self->[1]} > 1) ? ($self->[1][$low + 1] - $self->[1][$low]) : ($self->[1][1] - $self->[1][0])/$#{$self->[2]};
# get endpoint values
$y1
=
$self
->[2][
$low
];
$y2
=
$self
->[2][
$low
+ 1];
$m1
=
$self
->[3][
$low
] *
$dx
;
$m2
=
$self
->[3][
$low
+ 1] *
$dx
;
# compute coefficients of quadratic equation (at^2 + bt + c = 0)
$a
= 6 * (
$y1
-
$y2
) + 3 * (
$m1
+
$m2
);
$b
= -6 * (
$y1
-
$y2
) - 2 *
$m2
- 4 *
$m1
;
$c
=
$m1
;
# return if constant
return
()
if
(
abs
(
$a
) < 1E-15 &&
abs
(
$b
) < 1E-15);
# if linear equation (a is zero)
if
(
abs
(
$a
) < 1E-15) {
# add solution
push
(
@t
, -
$c
/
$b
);
# if quadratic equation
}
else
{
# compute discriminant
$dscr
=
$b
**2 - 4 *
$a
*
$c
;
# if discriminant > zero
if
(
$dscr
> 0) {
# add solutions (critical points)
push
(
@t
, -(
$b
+
sqrt
(
$dscr
))/(2 *
$a
));
push
(
@t
, -(
$b
-
sqrt
(
$dscr
))/(2 *
$a
));
}
}
# return solution(s) within interval (0 < t < 0)
return
(
grep
{
$_
> 0 &&
$_
< 1}
@t
);
}
# compute second derivative for unit spline segment
# parameters: (ref_to_object, t-value, segment)
# returns: (second_derivative)
sub
_derv2 {
# get parameters
my
(
$self
,
$t
,
$low
) =
@_
;
# local variables
my
(
$dx
,
$h00
,
$h01
,
$h10
,
$h11
);
# compute delta x-value (x-values : range)
$dx
= ($
#{$self->[1]} > 1) ? ($self->[1][$low + 1] - $self->[1][$low]) : ($self->[1][1] - $self->[1][0])/$#{$self->[2]};
# compute Hermite derivative coefficients
$h00
= 12 *
$t
- 6;
$h01
= -
$h00
;
$h10
= 6 *
$t
- 4;
$h11
= 6 *
$t
- 2;
# interpolate value
return
((
$h00
*
$self
->[2][
$low
]/
$dx
+
$h01
*
$self
->[2][
$low
+ 1]/
$dx
+
$h10
*
$self
->[3][
$low
] +
$h11
*
$self
->[3][
$low
+ 1])/
$dx
);
}
# compute derivative for unit spline segment
# parameters: (ref_to_object, t-value, segment)
# returns: (derivative)
sub
_derv {
# get parameters
my
(
$self
,
$t
,
$low
) =
@_
;
# local variables
my
(
$dx
,
$tc
,
$ttc
,
$h00
,
$h01
,
$h10
,
$h11
);
# compute delta x-value (x-values : range)
$dx
= ($
#{$self->[1]} > 1) ? ($self->[1][$low + 1] - $self->[1][$low]) : ($self->[1][1] - $self->[1][0])/$#{$self->[2]};
# if position is non-zero
if
(
$t
) {
# compute Hermite derivative coefficients
$tc
= (1 -
$t
);
$ttc
= -3 *
$t
*
$tc
;
$h00
= 2 *
$ttc
;
$h01
= -2 *
$ttc
;
$h10
=
$ttc
+
$tc
;
$h11
=
$ttc
+
$t
;
# interpolate value
return
(
$h00
*
$self
->[2][
$low
]/
$dx
+
$h01
*
$self
->[2][
$low
+ 1]/
$dx
+
$h10
*
$self
->[3][
$low
] +
$h11
*
$self
->[3][
$low
+ 1]);
}
else
{
# use lower knot derivative value
return
(
$self
->[3][
$low
]);
}
}
# compute transform value
# parameters: (ref_to_object, t-value, segment)
# returns: (y-value)
sub
_fwd {
# get parameters
my
(
$self
,
$t
,
$low
) =
@_
;
# local variables
my
(
$dx
,
$tc
,
$h00
,
$h01
,
$h10
,
$h11
);
# check if ICC::Support::Lapack module is loaded
state
$lapack
=
defined
(
$INC
{
'ICC/Support/Lapack.pm'
});
# if position is non-zero
if
(
$t
) {
# compute delta x-value (x-values : range)
$dx
= ($
#{$self->[1]} > 1) ? ($self->[1][$low + 1] - $self->[1][$low]) : ($self->[1][1] - $self->[1][0])/$#{$self->[2]};
# interpolate value (Lapack XS)
# if ICC::Support::Lapack module is loaded
if
(
$lapack
) {
return
(ICC::Support::Lapack::hermite(
$t
,
$self
->[2][
$low
],
$self
->[2][
$low
+ 1],
$dx
*
$self
->[3][
$low
],
$dx
*
$self
->[3][
$low
+ 1]));
}
else
{
# compute Hermite coefficients
$tc
= 1 -
$t
;
$h00
= (1 + 2 *
$t
) *
$tc
*
$tc
;
$h01
= 1 -
$h00
;
$h10
=
$t
*
$tc
*
$tc
;
$h11
= -
$t
*
$t
*
$tc
;
# interpolate value (no Lapack XS)
return
(
$h00
*
$self
->[2][
$low
] +
$h01
*
$self
->[2][
$low
+ 1] +
$h10
*
$dx
*
$self
->[3][
$low
] +
$h11
*
$dx
*
$self
->[3][
$low
+ 1]);
}
}
else
{
# use lower knot output value
return
(
$self
->[2][
$low
]);
}
}
# compute inverse value
# parameters: (ref_to_object, y-value, segment)
# there are 0 to 3 solutions in the range 0 < t < 1
# returns: (t-value_array)
sub
_rev {
# get parameters
my
(
$self
,
$in
,
$low
) =
@_
;
# local variables
my
(
$dx
,
$y1
,
$y2
,
$m1
,
$m2
);
my
(
$A
,
$B
,
$C
,
$D
,
$dscr
,
@t
);
my
(
$d0
,
$d1
,
$cs
,
$cc
,
@r
,
$ccr
,
$sol
,
$lim0
,
$lim1
);
# compute delta x-value (x-values : range)
$dx
= ($
#{$self->[1]} > 1) ? ($self->[1][$low + 1] - $self->[1][$low]) : ($self->[1][1] - $self->[1][0])/$#{$self->[2]};
# get endpoint values
$y1
=
$self
->[2][
$low
];
$y2
=
$self
->[2][
$low
+ 1];
$m1
=
$self
->[3][
$low
] *
$dx
;
$m2
=
$self
->[3][
$low
+ 1] *
$dx
;
# compute coefficients of cubic equation (At^3 + Bt^2 + Ct + D = 0)
$A
= 2 * (
$y1
-
$y2
) +
$m1
+
$m2
;
$B
= -3 * (
$y1
-
$y2
) -2 *
$m1
-
$m2
;
$C
=
$m1
;
$D
=
$y1
-
$in
;
# constant equation (A, B, C are zero)
if
(
abs
(
$A
) < 5E-15 &&
abs
(
$B
) < 5E-15 &&
abs
(
$C
) < 5E-15) {
# add solution
push
(
@t
, 0.5)
if
(
$D
== 0);
# linear equation (A, B are zero)
}
elsif
(
abs
(
$A
) < 5E-15 &&
abs
(
$B
) < 5E-15) {
# add solution
push
(
@t
, -
$D
/
$C
);
# quadratic equation (A is zero)
}
elsif
(
abs
(
$A
) < 5E-15) {
# compute discriminant: > 0 two real, == 0 one real, < 0 two complex
$dscr
=
$C
**2 - 4 *
$B
*
$D
;
# if discriminant is zero
if
(
abs
(
$dscr
) < 5E-15) {
# add solution (double root)
push
(
@t
, -
$C
/(2 *
$B
));
# if discriminant > zero
}
elsif
(
$dscr
> 0) {
# add solutions
push
(
@t
, -(
$C
+
sqrt
(
$dscr
))/(2 *
$B
));
push
(
@t
, -(
$C
-
sqrt
(
$dscr
))/(2 *
$B
));
}
# cubic equation
}
else
{
# compute discriminant: > 0 three real, == 0 one or two real, < 0 one real and two complex
$dscr
= 18 *
$A
*
$B
*
$C
*
$D
- 4 *
$B
**3 *
$D
+
$B
**2 *
$C
**2 - 4 *
$A
*
$C
**3 - 27 *
$A
**2 *
$D
**2;
# compute ∆0
$d0
=
$B
**2 - 3 *
$A
*
$C
;
# if discriminant is zero
if
(
abs
(
$dscr
) < 5E-15) {
# if ∆0 is zero
if
(
abs
(
$d0
) < 5E-15) {
# add solution (triple root)
push
(
@t
, -
$B
/(3 *
$A
));
}
else
{
# add solutions (double root and single root)
push
(
@t
, (9 *
$A
*
$D
-
$B
*
$C
)/(2 *
$d0
));
push
(
@t
, (4 *
$A
*
$B
*
$C
- 9 *
$A
**2 *
$D
-
$B
**3)/(
$A
*
$d0
));
}
}
else
{
# compute ∆1
$d1
= 2 *
$B
**3 - 9 *
$A
*
$B
*
$C
+ 27 *
$A
**2 *
$D
;
# compute C (a complex number)
$cs
=
sqrt
(
$d1
**2 - 4 *
$d0
**3);
$cc
= cbrt(
$d1
==
$cs
? (
$d1
+
$cs
)/2 : (
$d1
-
$cs
)/2);
# compute cube roots of 1 (three complex numbers)
@r
= root(1, 3);
# for each root
for
my
$i
(0 .. 2) {
# multiply by cube root of 1
$ccr
=
$r
[
$i
] *
$cc
;
# compute solution (a complex number)
$sol
= (
$B
+
$ccr
+
$d0
/
$ccr
)/(-3 *
$A
);
# add solution, if real
push
(
@t
, Re(
$sol
))
if
(
$dscr
> 0 ||
abs
(Im(
$sol
)) < 1E-15);
}
}
}
# set test limits to avoid duplicates at knots
$lim0
= (
$in
==
$y1
) ? 1E-14 : 0;
$lim1
= (
$in
==
$y2
) ? (1 - 1E-14) : 1;
# return valid solutions
return
(
sort
{
$a
<=>
$b
}
grep
{
$_
>
$lim0
&&
$_
<
$lim1
}
@t
);
}
# test vector values
# returns true if values are unique
# parameter: (vector)
# returns: (flag)
sub
_unique {
# get parameter
my
$vec
=
shift
();
# local variable
my
(
%x
);
# make count hash
for
(@{
$vec
}) {
$x
{
$_
}++}
# return
return
(@{
$vec
} ==
keys
(
%x
));
}
# sort object input/output values
# parameter: (ref_to_object)
sub
_sort_values {
# get parameter
my
$self
=
shift
();
# local variable
my
(
@p
);
# if input contains x-values
if
($
#{$self->[1]} > 1) {
# warn if vectors not same length
($
#{$self->[1]} == $#{$self->[2]}) or carp('input and output vectors not same length');
# for each point
for
my
$i
(0 .. $
#{$self->[1]}) {
# copy x-y values
$p
[
$i
] = [
$self
->[1][
$i
],
$self
->[2][
$i
]];
}
# sort points by input value
@p
=
sort
{
$a
->[0] <=>
$b
->[0]}
@p
;
# for each point
for
my
$i
(0 .. $
#{$self->[1]}) {
# copy x-y values
$self
->[1][
$i
] =
$p
[
$i
]->[0];
$self
->[2][
$i
] =
$p
[
$i
]->[1];
}
}
else
{
# if x-range is decreasing
if
(
$self
->[1][0] >
$self
->[1][-1]) {
# reverse x and y vectors
@{
$self
->[1]} =
reverse
(@{
$self
->[1]});
@{
$self
->[2]} =
reverse
(@{
$self
->[2]});
}
}
}
# fix highlight region data
# corrects the print-specific
# problem of highlight dot retention
# samples below the limit (0 - 1) are fixed
# parameter: (ref_to_object, limit)
sub
_fix_hl {
# get parameter
my
(
$self
,
$limit
) =
@_
;
# local variable
my
(
$span
,
$lo
,
$hi
,
$n
,
@ix
,
@xr
,
@x
,
@y
,
$bern
,
@new
);
# compute 'output' range
$span
= (
$self
->[2][-1] -
$self
->[2][0]);
# set 'output' limits
$lo
=
$self
->[2][0] + 0.002 *
$span
;
$hi
=
$self
->[2][0] + 2 *
$limit
*
$span
;
# get upper index of 'output' array
$n
= $
#{$self->[2]};
# find samples within limits
@ix
=
grep
{
$self
->[2][
$_
] >=
$lo
&&
$self
->[2][
$_
] <=
$hi
} (1 ..
$n
);
# use at least 4 samples
@ix
= (
$ix
[0] ..
$ix
[0] + 3)
if
(
@ix
< 4);
# if input is a range
if
(@{
$self
->[1]} == 2) {
# interpolate x-values
@xr
=
map
{(1 -
$_
/
$n
) *
$self
->[1][0] + (
$_
/
$n
) *
$self
->[1][1]} (0 ..
$n
);
# get 'fit' x-values
@x
=
@xr
[
@ix
];
}
else
{
# get 'fit' x-values
@x
= @{
$self
->[1]}[
@ix
];
}
# get 'fit' y-values
@y
= @{
$self
->[2]}[
@ix
];
# make 'bern' object of degree 3, fitted to x-y values (requires Lapack module)
$bern
= ICC::Support::bern->new({
'input'
=> [
$x
[0],
$x
[-1]],
'output'
=> [(
undef
) x 4],
'fit'
=> [\
@x
, \
@y
]});
# set 'output' limit
$hi
=
$self
->[2][0] +
$limit
*
$span
;
# for each sample
for
my
$i
(
reverse
(0 ..
$n
)) {
# if 'output' value ≤ limit
if
(
@new
||
$self
->[2][
$i
] <=
$hi
) {
# compute new 'output' value using 'bern' curve
push
(
@new
,
$bern
->transform(@{
$self
->[1]} > 2 ?
$self
->[1][
$i
] :
$xr
[
$i
]));
}
}
# splice new values, saving originals in hash
$self
->[0]{
'hl_original'
} = [
splice
(@{
$self
->[2]}, 0,
@new
,
reverse
(
@new
))];
# save x-axis intercept (highlight dot retention value)
$self
->[0]{
'hl_retention'
} =
$bern
->inverse(0);
# compute knot derivatives
_objderv(
$self
);
# add segment min/max y-values
_minmax(
$self
);
}
# fix shadow region data
# corrects the print-specific problem of
# non-monotonic data is the shadow region
# samples above the limit (0 - 1) are fixed
# parameter: (ref_to_object, limit)
sub
_fix_sh {
# get parameter
my
(
$self
,
$limit
) =
@_
;
# local variable
my
(
$span
,
$lo
,
$n
,
@ix
,
@xr
,
@x
,
@y
,
$bern
,
@new
);
# compute 'output' range
$span
= (
$self
->[2][-1] -
$self
->[2][0]);
# set 'output' limit
$lo
=
$self
->[2][-1] - 2 * (1 -
$limit
) *
$span
;
# get upper index of 'output' array
$n
= $
#{$self->[2]};
# find samples with 'output' > limit
@ix
=
grep
{
$self
->[2][
$_
] >=
$lo
} (0 ..
$n
);
# use at least 4 samples
@ix
= ((
$ix
[-1] - 3) ..
$ix
[-1])
if
(
@ix
< 4);
# if input is a range
if
(@{
$self
->[1]} == 2) {
# interpolate x-values
@xr
=
map
{(1 -
$_
/
$n
) *
$self
->[1][0] + (
$_
/
$n
) *
$self
->[1][1]} (0 ..
$n
);
# get 'fit' x-values
@x
=
@xr
[
@ix
];
}
else
{
# get 'fit' x-values
@x
= @{
$self
->[1]}[
@ix
];
}
# get 'fit' y-values
@y
= @{
$self
->[2]}[
@ix
];
# make 'bern' object of degree 3, fitted to x-y values (requires Lapack module)
$bern
= ICC::Support::bern->new({
'input'
=> [
$x
[0],
$x
[-1]],
'output'
=> [(
undef
) x 4],
'fit'
=> [\
@x
, \
@y
]});
# set 'output' limit
$lo
=
$self
->[2][0] +
$limit
*
$span
;
# for each sample
for
my
$i
(0 ..
$n
) {
# if 'output' value ≥ limit
if
(
@new
||
$self
->[2][
$i
] >=
$lo
) {
# compute new 'output' value using 'bern' curve
push
(
@new
,
$bern
->transform(@{
$self
->[1]} > 2 ?
$self
->[1][
$i
] :
$xr
[
$i
]));
}
}
# splice new values, saving originals in hash
$self
->[0]{
'sh_original'
} = [
splice
(@{
$self
->[2]}, -
@new
,
@new
,
@new
)];
# compute knot derivatives
_objderv(
$self
);
# add segment min/max y-values
_minmax(
$self
);
}
# set object contents from parameter hash
# parameters: (ref_to_object, ref_to_parameter_hash)
sub
_new_from_hash {
# get parameters
my
(
$self
,
$hash
) =
@_
;
# local variables
my
(
$in
,
$out
,
$type
,
$damp
,
$fix
,
$limit
,
$pok
,
$fok
);
# if 'input' is defined
if
(
defined
(
$in
=
$hash
->{
'input'
})) {
# if 'input' is a vector (2 or more values)
if
(ICC::Shared::is_num_vector(
$in
) && 2 <= @{
$in
}) {
# set object 'input' array
$self
->[1] = [@{
$in
}];
# if 'input' is an integer > 0 (2 or more values)
}
elsif
(Scalar::Util::looks_like_number(
$in
) &&
$in
==
int
(
$in
) &&
$in
> 0) {
# make identity curve
$self
->[1] = [
map
{
$_
/
$in
} (0 ..
$in
)];
}
else
{
# error
croak(
"invalid 'input' values"
);
}
}
else
{
# use default
$self
->[1] = [0, 1];
}
# if 'output' is defined
if
(
defined
(
$out
=
$hash
->{
'output'
})) {
# if 'output' is a vector (2 or more values)
if
(ICC::Shared::is_num_vector(
$out
) && 2 <= @{
$out
}) {
# set object 'output' array
$self
->[2] = [@{
$out
}];
# if 'output' is an integer > 0 (2 or more values)
}
elsif
(Scalar::Util::looks_like_number(
$out
) &&
$out
==
int
(
$out
) &&
$out
> 0) {
# make identity curve
$self
->[2] = [
map
{
$_
/
$out
} (0 ..
$out
)];
}
else
{
# error
croak(
"invalid 'output' values"
);
}
}
else
{
# use default
$self
->[2] = [0, 1];
}
# if 'type' is defined
if
(
defined
(
$type
=
$hash
->{
'type'
})) {
# verify supported type
(
$type
=~ m/^(natural|akima)$/) or croak(
'unsupported spline type'
);
# set object type in hash
$self
->[0]{
'type'
} =
$type
;
}
# if 'damp' is defined
if
(
defined
(
$damp
=
$hash
->{
'damp'
})) {
# verify supported type
(Scalar::Util::looks_like_number(
$damp
) &&
$damp
>= 0) or croak(
'invalid akima damping value'
);
# set object type in hash
$self
->[0]{
'damp'
} =
$damp
;
}
# verify number of input values
(@{
$self
->[1]} == 2 || @{
$self
->[1]} == @{
$self
->[2]}) or croak(
'wrong number of input values'
);
# sort input/output values
_sort_values(
$self
);
# compute knot derivatives
_objderv(
$self
);
# add segment min/max y-values
_minmax(
$self
);
# if 'fix_hl' is defined
if
(
defined
(
$fix
=
$hash
->{
'fix_hl'
})) {
# verify polarity
(
$pok
=
$self
->[2][-1] >
$self
->[2][0]) or carp(
"'fix_hl' requires increasing output values"
);
# verify fix limit (-1 to 1)
(
$fok
= (Scalar::Util::looks_like_number(
$fix
) &&
$fix
>= -1 &&
$fix
<= 1)) or carp(
"invalid 'fix_hl' limit"
);
# compute 'output' limit
$limit
=
$self
->[2][0] * (1 -
$fix
) +
$self
->[2][-1] *
$fix
;
# fix the data if polarity and limit are OK, and spline is non-monotonic in highlights
_fix_hl(
$self
,
abs
(
$fix
))
if
(
$pok
&&
$fok
&& (
$fix
< 0 ||
grep
{
$_
<=
$limit
} monotonic(
$self
,
'output'
)));
}
# if 'fix_sh' is defined
if
(
defined
(
$fix
=
$hash
->{
'fix_sh'
})) {
# verify polarity
(
$pok
=
$self
->[2][-1] >
$self
->[2][0]) or carp(
"'fix_sh' requires increasing output values"
);
# verify fix limit (-1 to 1)
(
$fok
= (Scalar::Util::looks_like_number(
$fix
) &&
$fix
>= -1 &&
$fix
<= 1)) or carp(
"invalid 'fix_sh' limit"
);
# compute 'output' limit
$limit
=
$self
->[2][0] * (1 -
$fix
) +
$self
->[2][-1] *
$fix
;
# fix the data if polarity and limit are OK, and spline is non-monotonic in shadows
_fix_sh(
$self
,
abs
(
$fix
))
if
(
$pok
&&
$fok
&& (
$fix
< 0 ||
grep
{
$_
>=
$limit
} monotonic(
$self
,
'output'
)));
}
}
# write spline object to ICC profile
# note: writes an equivalent curv object
# parameters: (ref_to_object, ref_to_parent_object, file_handle, ref_to_tag_table_entry)
sub
_writeICCspline {
# get parameters
my
(
$self
,
$parent
,
$fh
,
$tag
) =
@_
;
# local variables
my
(
$n
,
$up
,
@table
);
# get count value (defaults to 4096, the maximum allowed in an 'mft2' tag)
$n
=
$self
->[0]{
'curv_points'
} // 4096;
# validate number of table entries
(
$n
==
int
(
$n
) &&
$n
>= 2) or carp(
'invalid number of table entries'
);
# array upper index
$up
=
$n
- 1;
# for each table entry
for
my
$i
(0 ..
$up
) {
# transform value
$table
[
$i
] = transform(
$self
,
$i
/
$up
);
}
# seek start of tag
seek
(
$fh
,
$tag
->[1], 0);
# write tag type signature and count
$fh
pack
(
'a4 x4 N'
,
'curv'
,
$n
);
# write array
$fh
pack
(
'n*'
,
map
{
$_
< 0 ? 0 : (
$_
> 1 ? 65535 :
$_
* 65535 + 0.5)}
@table
);
}
1;