# documentation for spline.pm module / 2019-12-30

=encoding utf8

=head1 NAME

ICC::Support::spline - An object oriented module for modeling transfer functions (curves) using cubic splines.

=head1 SYNOPSIS

    use ICC::Support::spline;

    # create a new object
    $spline = ICC::Support::spline->new(); # empty object
    $spline = ICC::Support::spline->new($hash); # from a hash

    # create an inverse object
    $inv = $spline->inv(); # object is cloned, then inverted

    # get/set header hash
    $hash = $spline->header(); # get header hash reference
    $hash = $spline->header($replacement_hash_ref); # set header hash

    # get/set input parameters
    $input = $spline->input(); # get input array reference
    $input = $spline->input($replacement_array_ref); # set input array

    # get/set output parameters
    $output = $spline->output(); # get output array reference
    $output = $spline->output($replacement_array_ref); # set output array

    # transforms
    $out = $spline->transform($in);
    $out = $spline->inverse($in);
    $out = $spline->derivative($in);
    @pd = $spline->parametric($in);

    # directional transform
    $out = $spline->_transform($dir, $in);

    # normalize parameters
    $spline = $spline->normalize();
    $spline = $spline->normalize($hash);

    # min/max values
    @minmax = $spline->monotonic();
    @minmax = $spline->monotonic([$format]);

    # update object internals
    $spline->update();
    $spline->update($flag);

    # make LUT for profile objects
    $table = $spline->table($n);
    $table = $spline->table($n, $dir);

    # make equivalent 'curv' object
    $curv = $spline->curv($n);
    $curv = $spline->curv($n, $dir);

    # dump object
    $string = $spline->sdump([$format]);
    $spline->dump([$format]);

=head1 DESCRIPTION

Cubic splines are widely used to model functions defined by a set of x-y values (knots). The knots are connected
with cubic polynomials, having a common slope (derivative) at each knot. The resulting spline curve is continuous
in value and first derivative.

The C<ICC::Support::spline> module implements cubic splines with both uniform and non-uniform knot spacing. The
x-y values are stored in separate arrays – the x-values in the 'input' array, and the y-values in the 'output'
array. For non-uniform splines, the arrays are the same size, and sorted so the 'input' values are increasing.
For uniform splines, the 'input' array contains two values, which define the input range. The range is divided
into equally spaced 'virtual' values, corresponding to the 'output' values.

The knot derivatives are computed and stored in a third array. The method used to compute these derivatives, is
determined by the object 'type'. The supported types are 'natural' and 'akima'. Other types may be added in the
future.

=head2 Object structure

An C<ICC::Support::spline> object is a blessed array reference. The array contains six elements, the header hash,
the input array, the output array, the derivative array, the min/max output array, and the parametric derivative
matrix.

    # create empty 'spline' object
    my $self = [
        {},     # header hash
        [],     # input array
        [],     # output array
        [],     # derivative array
        [],     # min/max output array
        [],     # parametric derivative matrix
    ];

The header hash contains metadata, and may be used to attach information to the object. The object uses the keys
type' and 'damp'. The user may add other key/value pairs.

The 'input' array contains either the knot x-values, or the range of those values.

The 'output' array contains the knot y-values.

The derivative array contains the knot derivatives, computed by a method defined by the object type. If the
'input' or 'output' arrays are changed, the derivatives must be recomputed.

The min/max output array contains the minimum and maximum output values for each spline segment. These values
are used by the 'inverse' transform to determine if a solution lies within a segment.

The parametric derivative matrix contains precomputed partial derivatives for each knot. These values are used by
the 'parametric' method.

The C<ICC::Support::spline> object is not symmetrical. The 'input' and 'output' values cannot be interchanged to
invert the curves. Furthermore, the 'input' and 'output' values are sorted so the 'input' values are in ascending
order. Duplicate input values are not allowed.

=head2 t-values and s-values

These values describe the location of an input value within the spline segments. The 'input' array contains the
x-values of the knots in ascending order. If the input value lies within a spline segment, the t-value will
range from 0 at the first knot to 1 at the second knot. The s-value is the index of the first knot plus the
t-value.

The s-value will range from 0 to the upper index of the 'input' array. Keep in mind that while the relation
between the input value and s-value is linear within each segment, the slope will vary with the segment width.

=head2 Domain and range

The domain of an C<ICC::Support::spline> object is set by the 'input' endpoints. This array is always sorted
in ascending order. The range of the object is determined by the 'output' values.

When the 'transform' method receives an input value outside the domain, it is linearly extrapolated, using the
slope and value of the nearest endpoint.

=head1 METHODS

=head2 Creating ICC::Support::spline objects

=head3 new

This method creates an C<ICC::Support::spline> object.

With no parameters, the object contains the empty basic structure (see L<"Object structure">).

An object is normally created from a hash, as illustrated below.

B<Usage>

    $spline = ICC::Support::spline->new(); # empty object
    $spline = ICC::Support::spline->new($hash); # from a hash

B<Examples>

    use ICC::Support::spline;

    $spline = ICC::Support::spline->new(); # empty object
    $spline = ICC::Support::spline->new({}); # default object (see notes 1 and 2)
    $spline = ICC::Support::spline->new({'output' => [0, 100]}); # set 'output' array (see notes 2 and 3)
    $spline = ICC::Support::spline->new({'input' => [0, 100]}); # set 'input' array (see notes 2 and 4)
    $spline = ICC::Support::spline->new({'input' => [0, 3], 'output' => [2, 3, 5, 7]}); # set both arrays (see notes 2 and 5)
    $spline = ICC::Support::spline->new({'input' => [0, 1, 2, 3], 'output' => [2, 3, 5, 7]}); # set both arrays (see notes 2 and 6)
    $spline = ICC::Support::spline->new({'output' => 4}); # identity curve with 4 segments (see notes 2 and 7)

    $in = [0, 5, 10, 20, 50, 80, 90, 95, 100]; # input vector
    $out = [0, 4.5, 9.8, 22, 55, 78, 88, 92, 100]; # output vector

    $spline = ICC::Support::spline->new({'input' => $in, 'output' => $out}); # using named vectors
    $spline = ICC::Support::spline->new({'output' => $out, 'type' => 'akima'}); # akima spline (see note 8)
    $spline = ICC::Support::spline->new({'output' => $out, 'type' => 'akima', 'damp' => 0.2}); # akima spline (see note 9)

    $out2 = [0, 25, 50, 75, 90, 95, 98, 97.5, 99, 100]; # non-monotonic data in shadow region

    $spline = ICC::Support::spline->new({'output' => $out2, 'fix_sh' => 0.95}); # fix shadow data (see notes 10 and 12)

    $out3 = [0, 0.1, 3, 5, 10, 25, 50, 75, 100]; # data with poor highlight retention

    $spline = ICC::Support::spline->new({'output' => $out3, 'fix_hl' => 0.05}); # fix shadow data (see note 11 and 12)

=over

=item 1

An empty hash (C<{}>) makes an object with C<'input'> and C<'output'> arrays set to the default value of C<[0, 1]>.

=item 2

Hash keys are C<'input'>, C<'output'>, C<'type'>, C<'damp'>, C<'fix_hl'>, and C<'fix_sh'>

Hash values for C<'input'> and C<'output'> are either an integer > 0, or a vector. Vectors must contain at least two elements.

Hash values for C<'type'> are C<'natural'> and C<'akima'>.

Hash value for C<'damp'> is a number ≥ 0.

Hash values for C<'fix_hl'> and C<'fix_sh'> are numbers ≥ -1 and ≤ 1.

=item 3

The C<'input'> array is set to the default value of C<[0, 1]>.

=item 4

The C<'output'> array is set to the default value of C<[0, 1]>.

=item 5

The C<'input'> array is a range. This range is divided equally by the number of points in the C<'output'> array to get
'virtual' input values for each point.

=item 6

The C<'input'> array contains knot values corresponding to the C<'output'> array. The arrays must be the same size.

=item 7

The C<'input'> array is the range C<[0, 1]> (default). The C<'output'> array is C<[0, 1]> divided into 4 equal segments,
or C<[0, 0.25, 0.5, 0.75, 1]>.

=item 8

The C<'akima'> method is used to calculate the knot derivatives. The C<'damp'> parameter is set to a default value.

=item 9

The default C<'damp'> value is overidden.

=item 10

The non-monotonic shadow data is 'fixed' by fitting a cubic function to the shadow region, and replacing the problem
values. For this example, the 'fixed' object C<'output'> array contains these values,
C<[0, 25, 50, 75, 90, 95.19, 97.48, 98.17, 98.60, 100.10]>.

The C<'sh_original'> key/value is added to the header hash. In this example, C<'sh_original' =E<gt> [95, 98, 97.5, 99, 100]>.

=item 11

The highlight data is 'fixed' by fitting a cubic function to the highlight region, and replacing the problem
values. For this example, the 'fixed' object C<'output'> array contains these values,
C<[-2.67, 0.17, 3, 5, 10, 25, 50, 75, 100]>.

The C<'hl_original'> key/value is added to the header hash. In this example, C<'hl_original' =E<gt> [0, 0.1, 3, 5]>.

The C<'hl_retention'> key/value is added to the header hash. In this example, C<'hl_retention' =E<gt> '0.117647058823529'>.

=item 12

If the C<'fix_hl'> or C<'fix_sh'> value is positive, that function is only executed if the spline is non-monotonic in the
region specified. if the value is negative, the function is executed regardless.

=back

=head3 inv

This method creates an inverted C<ICC::Support::spline> object from an existing object.

B<Usage>

    $inv = $spline->inv();

B<Examples>

    use ICC::Support::spline;

    $spline = ICC::Support::spline->new({'input' => [1, 2, 3], 'output' => [4, 5, 7]});
    $spline2 = ICC::Support::spline->new({'input' => [4, 5, 7], 'output' => [1, 2, 3]});

    $inv = $spline->inv(); # $inv is identical to $spline2 (see note 1)

    printf "%f %f\n", $inv->transform(6), $spline->inverse(6); # compare transforms (see note 2)

=over

=item 1

The inverted object is made by cloning the source object, then swapping the 'input' and 'output' arrays.
The source object must be monotonic.

=item 2

Be aware the 'inv' object is an approximation of the 'inverse' function.

=back

=head2 General accessors

=head3 header

This method returns a reference to the header hash (see L<"Object structure"> section). It may also be
used to replace the header hash.

B<Usage>

    $hash = $spline->header(); # get header hash reference
    $hash = $spline->header($replacement_hash_ref); # set header hash

B<Examples>

    use ICC::Support::spline;

    $spline = ICC::Support::spline->new({}); # make 'spline' object

    $hash = $spline->header(); # get header hash reference
    $hash->{'key'} = 'value'; # add key/value to header hash
    $spline->header->{'ink'} = 'Cyan'; # set ink color
    $ink = $spline->header->{'ink'}; # get ink color
    $spline->header({'new' => 'hash'}); # replace header (see note 1)

=over

=item 1

The parameter is copied to the object.

=back

=head3 input

This method returns a reference to the input curve parameters (see L<"Object structure"> section). It may
also be used to replace the parameters.

B<Usage>

    $input = $spline->input(); # get input array reference
    $input = $spline->input($replacement_array_ref); # set input array

B<Examples>

    use ICC::Support::spline;

$spline = ICC::Support::spline->new({'input' => [1, 2, 3], 'output' => [7, 8, 9]}); # make 'spline' object

    $input = $spline->input(); # get input array reference
    $c = $spline->input->[0]; # get first input parameter
    $spline->input->[0] = 0.1; # set first input parameter
    $spline->input([4, 5, 6]); # set input array (see note 1)

=over

=item 1

The parameter is copied to the object. The replacement array must contain 2 or more elements. If there are more
than 2 elements, that number should be the same as the 'output' array.

=back

=head3 output

This method returns a reference to the output curve parameters (see L<"Object structure"> section). It may
also be used to replace the parameters.

B<Usage>

    $output = $spline->output(); # get output array reference
    $output = $spline->output($replacement_array_ref); # set output array

B<Examples>

    use ICC::Support::spline;

    $spline = ICC::Support::spline->new({'output' => [1, 2, 3]}); # make 'spline' object

    $output = $spline->output(); # get output array reference
    $c = $spline->output->[0]; # get first output parameter
    $spline->output->[0] = 0.1; # set first output parameter
    $spline->output([4, 5, 6]); # set output array (see note 1)

=over

=item 1

The parameter is copied to the object. The replacement array must contain 2 or more elements. That number should
be the same as the 'input' array, if the 'input' array contains more than two elements.

=back

=head2 Transforms

=head3 transform

This method transforms a single input value to a single output value.

B<Usage>

    $out = $spline->transform($in);

B<Examples>

    use ICC::Support::spline;

    $spline = ICC::Support::spline->new({'output' => [0, 0.4, 0.7, 1]}); # make 'spline' object

    $out = $spline->transform(0.5); # input value within the object domain (0 - 1)
    $out = $spline->transform(-0.5); # input value outside the domain (see note 1)
    $out = $spline->transform(1.5); # input value outside the domain (see note 1)

=over

=item 1

When the input value lies outside the object domain, the output value is extrapolated using the slope at the
nearest endpoint.

=back

=head3 inverse

This method transforms a single input value to a single output value. It is identical to the B<transform>
method, except the 'input' and 'output' curves are swapped. The inverts the B<transform> function, so the output
is the inverse of the input.

B<Usage>

    $out = $spline->inverse($in);

B<Examples>

    use ICC::Support::spline;

    $spline = ICC::Support::spline->new({'output' => [0, 0.4, 0.7, 1]}); # make 'spline' object
    $spline2 = ICC::Support::spline->new({'output' => [0, 1, -1, 0]}); # non-monotonic 'output' curve

    $out = $spline->inverse(0.5); # input value within the object range (0 - 1)
    $out = $spline->inverse(-0.5); # input value outside the range (see note 1)
    $out = $spline->inverse(1.5); # input value outside the range (see note 1)

    $out = $spline2->inverse(0); # non-monotonic 'output' curve, three possible solutions (see note 2)
    $out = $spline2->inverse(1.088); # non-monotonic 'output' curve, three possible solutions (see note 3)
    $out = $spline2->inverse(1.089); # non-monotonic 'output' curve, one solution (see note 3)

=over

=item 1

When the input value lies outside the object range, the output value is extrapolated using the slope at the
nearest endpoint.

=item 2

Because the curve is non-monotonic, there are 3 possible solutions. The first solution, 0, is returned.

=item 3

With an input of 1.088, there are 3 possible solutions. The first solution, 0.267, is returned.
When the input is increased to 1.089, there is just one solution, 1.1815. Obviously, this discontinuity may
cause problems.

=back

=head3 derivative

This method returns the derivative of the B<transform> method, for a single input value.

B<Usage>

    $out = $spline->derivative($in);

B<Examples>

    use ICC::Support::spline;

    $spline = ICC::Support::spline->new({'output' => [0, 0.4, 0.7, 1]}); # make 'spline' object

    $out = $spline->derivative(0.5);
    $out = $spline->derivative(-0.5); # input value outside the range (see note 1)
    $out = $spline->derivative(1.5); # input value outside the range (see note 1)

=over

=item 1

When the input value lies outside the object range, the output value is the slope at the nearest endpoint.

=back

=head3 parametric

This method returns an array of partial derivatives, ∂out/∂p[i], where p[i] is an 'output' curve parameter.

B<Usage>

    @pd = $spline->parametric($in);

B<Examples>

    use ICC::Support::spline;

    $spline = ICC::Support::spline->new({'output' => [0, 0.4, 0.7, 1]}); # make 'spline' object

    @pd = $spline->parametric(0.5);

=head2 Transform (directional)

The directional transform is used internally by other modules.

=head3 _transform

This method transforms a single input value to a single output value, with a direction parameter.
If the parameter is 'true', the transform is inverse.

B<Usage>

    $out = $spline->_transform($dir, $in);

B<Examples>

    use ICC::Support::spline;

    $spline = ICC::Support::spline->new({'output' => [0, 0.4, 0.7, 1]}); # make 'spline' object

    $out = $spline->_transform(0, $in); # same as 'transform' method
    $out = $spline->_transform(1, $in); # same as 'inverse' method

=head2 General

=head3 normalize

This method sets the domain and/or range of the object by applying a linear transform to the object's 'input'
and/or 'output' arrays. The default behavior is to adjust both arrays to [0, ... 1] or [1, ... 0], preserving the
current polarity of the curve(s).

B<Usage>

    $spline = $spline->normalize();
    $spline = $spline->normalize($hash_ref);

B<Examples>

    use ICC::Support::spline;

    $spline = ICC::Support::spline->new({'output' => [0, 30, 57.5, 82, 100]}); # make 'spline' object
    $spline2 = ICC::Support::spline->new({'input' => [0, 100], 'output' => [0, 30, 57.5, 82, 100]});
    $spline3 = ICC::Support::spline->new({'input' => [100, 0], 'output' => [0, 30, 57.5, 82, 100]});
    $spline4 = ICC::Support::spline->new({'output' => [0.2, -3, 3, 0.6]}); # non-monotonic curve

    $spline->normalize(); # 'output' curve normalized, is now [0, 0.3, 0.575, 0.82, 1] (see note 1)
    $spline2->normalize(); # both 'input' and 'output' curves normalized, 'input' is now [0, 1]
    $spline3->normalize(); # curve polarity is preserved, 'input' is now [1, 0]

    $spline2->normalize({'output' => [0, 100]}); # 'output' curve is now [0, 30, 57.5, 82, 100] (see notes 2 and 3)
    $spline2->normalize({'output' => [100, 0]}); # 'output' curve is inverted [100, 70, 42.5, 18, 0]

    $spline4->normalize({'output' => ['endpoints' => 0, 1]}); # curve endpoints transformed to [0, 1] (see note 4)
    $spline4->normalize({'output' => ['minmax' => 0, 1]}); # curve minimum/maximum transformed to [0, 1]
    $spline4->normalize({'output' => [0, 1 => 4, 5]}); # curve transformed so that 0 => 4 and 1 => 5

    $cvst->array->[1] = $spline->normalize(); # returns object reference (see note 5)

=over

=item 1

This object has no 'input' curve, so the curve domain is [0 - 1].

=item 2

Array values are linearly transformed, y = mx + b. The constants m and b are determined from the x-y values
of two points. The hash specifies these x-y values in various ways.

=item 3

Hash keys are 'input', and 'output'.

Hash values for 'input' and 'output' are an array reference.

The array may have 2, 3, or 4 elements.
The last 2 elements are the y-values of two points for the linear transform.
If there are 3 array elements, the first element is either 'endpoints' or 'minmax'.
If there are 4 array elements, the first two elements are the transform x-values.

If there are 2 array elements, or 'endpoints' is specified, the x-values are the first and last array values.

If 'minmax' is specfied, the x-values are the minimum and maximum curve values. These values might not be the
endpoints.

=item 4

In Perl, 'C<=E<gt>>' is the same as a comma, so C<['endpoints' =E<gt> 0, 1]> is the same as C<['endpoints', 0, 1]>.

=item 5

The object reference is returned to support "pass-through" method calls.

=back

=head3 monotonic

This method returns a combined list of min/max values for the 'input' and 'output' curves. By default, s-values
are returned. These values are converted to 'input' or 'output' values by adding the optional format parameter.
If the list is empty, there are no min/max values, and the curve is monotonic.

B<Usage>

    @minmax = $spline->monotonic();
    @minmax = $spline->monotonic($format);

B<Examples>

    use ICC::Support::spline;

    $spline = ICC::Support::spline->new({'output' => [0.5, -5, 2, 0.9]}); # non-monotonic curve

    @minmax = $spline->monotonic(); # returns s-values
    @minmax = $spline->monotonic('input'); # returns 'input' values
    @minmax = $spline->monotonic('output'); # returns 'output' values

    print "curve is non-monotonic\n" if (@minmax);

=head3 update

This method updates the internal elements of the object. It should be called when the contents of the curve arrays
are changed externally, e.g. curve optimization.

B<Usage>

    $spline->update();
    $spline->update($flag);

B<Examples>

    use ICC::Support::spline;

    $spline = ICC::Support::spline->new({'input' => [0, 1, 2, 3], 'output' => [4, 5, 6, 7]}); # make 'spline' object

    $spline->output->[2] = 6.17; # change an output value
    $spline->update(); # update (see note 1)

    $spline->input->[2] = 2.05; # change an input value
    $spline->update(1); # update (see note 1)

=over

=item 1

If 'output' values are changed externally, call 'update()'. If 'input' values are changed externally, call 'update(1)',
which invokes additional functions. It is not necessary to call 'update' when replacing the arrays with the 'input' and
'output' methods.

=back

=head3 table

This method makes a lookup table (LUT) for use in certain profile objects ('curv', 'mft1', 'mft2').
It is assumed that the domain and range of the 'spline' object is [0, 1].

B<Usage>

    $table = $spline->table($n);
    $table = $spline->table($n, $dir);

B<Examples>

    use ICC::Support::spline;

    $spline = ICC::Support::spline->new({'output' => [0, 30, 57.5, 82, 100]}); # make 'spline' object

    $spline->normalize(); # normalize the object
    $table = $spline->table(1285); # make a table with 1285 steps
    $table = $spline->table(1285, 1); # make an inverse table with 1285 steps

=head3 curv

This method makes an equivalent 'curv' object.
It is assumed that the domain and range of the 'spline' object is [0, 1].

B<Usage>

    $curv = $spline->curv($n);
    $curv = $spline->curv($n, $dir);

B<Examples>

    use ICC::Support::spline;

    $spline = ICC::Support::spline->new({'output' => [0, 30, 57.5, 82, 100]}); # make 'spline' object

    $spline->normalize(); # normalize the object
    $curv = $spline->curv(1285); # make a 'curv' object with 1285 steps
    $curv = $spline->curv(1285, 1); # make an inverse 'curv' object with 1285 steps

=head3 dump, sdump

This method returns a string showing the structure of the 'spline' object.

B<Usage>

    $string = $spline->sdump([$format]);
    $spline->dump([$format]);

B<Examples>

    use ICC::Support::spline;

    $spline = ICC::Support::spline->new({'input' => [0, 100], 'output' => 4});
    $string = $spline->sdump(); # dump to string
    $spline->dump(); # dump to STDOUT

=head1 SEE ALSO

=head2 Math References

Akima, Hiroshi "A New Method of Interpolation and Smooth Curve Fitting Based on Local Procedures" ACM Journal, Vol. 17, No. 4, October 1970.

Weisstein, Eric W. "Cubic Spline." MathWorld. L<http://mathworld.wolfram.com/CubicSpline.html>

=head1 LICENSE

Programs in this distribution, authored by William B. Birkett, are licensed under the GNU General Public License, v3.

See L<https://www.gnu.org/licenses/gpl-3.0.html> for license details.

=head1 AUTHOR

William B. Birkett, E<lt>wbirkett@doplganger.comE<gt>

=head1 COPYRIGHT

Copyright © 2004-2020 by William B. Birkett

=cut
