File Coverage

blib/lib/Algorithm/CurveFit.pm
Criterion Covered Total %
statement 123 135 91.1
branch 27 48 56.2
condition 13 36 36.1
subroutine 11 11 100.0
pod 1 1 100.0
total 175 231 75.7


line stmt bran cond sub pod time code
1             package Algorithm::CurveFit;
2              
3 3     3   506734 use 5.006;
  3         14  
4 3     3   20 use strict;
  3         8  
  3         109  
5 3     3   19 use warnings;
  3         5  
  3         606  
6              
7             our $VERSION = '1.06';
8              
9             require Exporter;
10              
11             our @ISA = qw(Exporter);
12              
13             our %EXPORT_TAGS = (
14             'all' => [ qw( curve_fit ) ]
15             );
16              
17             our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
18              
19             our @EXPORT = qw();
20              
21 3     3   20 use Carp qw/confess/;
  3         12  
  3         254  
22 3     3   1938 use Math::Symbolic qw/parse_from_string/;
  3         351375  
  3         427  
23 3     3   3526 use Math::MatrixReal;
  3         80533  
  3         227  
24 3     3   28 use Data::Dumper;
  3         6  
  3         192  
25              
26             # machine epsilon
27 3     3   25 use constant EPS => 2.2e-16;
  3         9  
  3         304  
28 3     3   19 use constant SQRT_EPS => sqrt(EPS);
  3         7  
  3         4173  
29              
30             sub curve_fit {
31 2 100 33 2 1 355892 shift @_ if not ref $_[0] and defined $_[0] and $_[0] eq 'Algorithm::CurveFit';
      66        
32              
33 2 50       13 confess('Uneven number of arguments to Algorithm::CurveFit::curve_fit.')
34             if @_ % 2;
35              
36 2         21 my %args = @_;
37              
38             # Formula
39 2 50       12 confess("Missing 'formula' parameter.") if not defined $args{formula};
40 2         5 my $formula;
41 2 50       13 if (ref($args{formula}) =~ /^Math::Symbolic/) {
42 0         0 $formula = $args{formula};
43             }
44             else {
45 2         7 eval { $formula = parse_from_string( $args{formula} ); };
  2         23  
46 2 50 33     49547 confess( "Cannot parse formula '" . $args{formula} . "'. ($@)" )
47             if not defined $formula or $@;
48             }
49              
50             # Variable (optional)
51 2         9 my $variable = $args{variable};
52 2 50       9 $variable = 'x' if not defined $variable;
53             confess("Formula '"
54             . $args{formula}
55             . "' not explicitly dependent on "
56             . "variable '$variable'." )
57 2 50       12 if not grep { $_ eq $variable } $formula->explicit_signature();
  8         708  
58              
59             # Parameters
60 2         10 my $params = $args{params};
61              
62 2 50 33     21 confess("Parameter 'params' has to be an array reference.")
63             if not defined $params
64             or not ref($params) eq 'ARRAY';
65 2         10 my @parameters = @$params;
66 2 50       27 confess('No parameters specified.') if not @parameters;
67             confess('Individual parameters need to be array references.')
68 2 50 33     7 if grep { not defined $_ or not ref($_) eq 'ARRAY' } @parameters;
  6         35  
69 2         7 foreach my $p (@parameters) {
70             confess("Weird parameter\n'"
71             . Dumper($p)
72             . "' Should have the format\n"
73             . "[ NAME_STRING, GUESSED_VALUE, ACCURACY ]\n"
74             . "With the accuracy being optional. See docs." )
75             if @$p > 3
76             or @$p < 2
77 6 50 33     44 or grep { not defined $_ } @$p;
  18   33     45  
78              
79             confess("Formula '"
80             . $args{formula}
81             . "' not explicitly dependent on "
82             . "parameter '"
83             . $p->[0]
84             . "'." )
85 6 50       23 if not grep { $_ eq $p->[0] } $formula->explicit_signature();
  26         1231  
86             }
87              
88             # XData
89 2         10 my $xdata = $args{xdata};
90 2 50 33     62 confess('X-Data missing.')
      33        
91             if not defined $xdata
92             or not ref($xdata) eq 'ARRAY'
93             or not @$xdata;
94 2         40 my @xdata = @$xdata;
95              
96             # YData
97 2         9 my $ydata = $args{ydata};
98 2 50 33     27 confess('Y-Data missing.')
      33        
99             if not defined $ydata
100             or not ref($ydata) eq 'ARRAY'
101             or not @$ydata;
102 2 50       11 confess('Y-Data and X-Data need to have the same number of elements.')
103             if not @$ydata == @xdata;
104 2         56 my @ydata = @$ydata;
105              
106             # Max_Iter (optional)
107 2         9 my $max_iter = $args{maximum_iterations};
108 2 50       9 $max_iter = 0 if not defined $max_iter;
109              
110             # Add third element (dlamda) to parameter arrays in case they're missing.
111 2         7 foreach my $param (@parameters) {
112 6 50       25 push @$param, 0 if @$param < 3;
113             }
114              
115             # Array holding all first order partial derivatives of the function in respect
116             # to the parameters in order.
117 2         4 my @derivatives;
118 2         6 my @param_names = ($variable, map {$_->[0]} @parameters);
  6         18  
119 2         7 foreach my $param (@parameters) {
120 6         48 my $deriv =
121             Math::Symbolic::Operator->new( 'partial_derivative', $formula,
122             $param->[0] );
123 6         30429 $deriv = $deriv->simplify()->apply_derivatives()->simplify();
124 6         172779 my ($sub, $trees) = Math::Symbolic::Compiler->compile_to_sub($deriv, \@param_names);
125 6 50       6084 if ($trees) {
126 6         123 push @derivatives, $deriv; # residual trees, need to evaluate
127             } else {
128 0         0 push @derivatives, $sub;
129             }
130             }
131              
132             # if not compilable, close over a ->value call for convenience later on
133 2         6 my $formula_sub = do {
134 2         12 my ($sub, $trees) = Math::Symbolic::Compiler->compile_to_sub($formula, \@param_names);
135             $trees
136             ? sub {
137             $formula->value(
138 492     492   1079 map { ($param_names[$_] => $_[$_]) } 0..$#param_names
  1854         3796  
139             )
140             }
141 2 50       1326 : $sub
142             };
143              
144 2         6 my $dbeta;
145              
146             # Iterative approximation of the parameters
147 2         6 my $iteration = 0;
148              
149             # As long as we're under max_iter or maxiter==0
150 2   33     18 while ( !$max_iter || ++$iteration < $max_iter ) {
151             # Generate Matrix A
152 5         29 my @cols;
153 5         13 my $pno = 0;
154 5         12 my @par_values = map {$_->[1]} @parameters;
  16         42  
155 5         15 foreach my $param (@parameters) {
156 16         43 my $deriv = $derivatives[ $pno++ ];
157              
158 16         27 my @ary;
159 16 50       66 if (ref $deriv eq 'CODE') {
160 0         0 foreach my $x ( 0 .. $#xdata ) {
161 0         0 my $xv = $xdata[$x];
162 0         0 my $value = $deriv->($xv, @par_values);
163 0 0       0 if (not defined $value) { # fall back to numeric five-point stencil
164 0         0 my $h = SQRT_EPS*$xv; my $t = $xv + $h; $h = $t-$xv; # numerics. Cf. NR
  0         0  
  0         0  
165 0         0 $value = $formula_sub->($xv, map { $_->[1] } @parameters)
  0         0  
166             }
167 0         0 push @ary, $value;
168             }
169             }
170             else {
171 16         76 $deriv = $deriv->new; # better safe than sorry
172 16         6464 foreach my $x ( 0 .. $#xdata ) {
173 968         2051 my $xv = $xdata[$x];
174             my $value = $deriv->value(
175             $variable => $xv,
176 968         1838 map { ( @{$_}[ 0, 1 ] ) } @parameters # a, guess
  3064         4177  
  3064         7870  
177             );
178 968 100       913307 if (not defined $value) { # fall back to numeric five-point stencil
179 1         4 my $h = SQRT_EPS*$xv; my $t = $xv + $h; $h = $t-$xv; # numerics. Cf. NR
  1         2  
  1         2  
180 1         3 $value = $formula_sub->($xv, map { $_->[1] } @parameters)
  4         8  
181             }
182 968         3197 push @ary, $value;
183             }
184             }
185 16         248 push @cols, \@ary;
186             }
187              
188             # Prepare matrix of datapoints X parameters
189 5         66 my $A = Math::MatrixReal->new_from_cols( \@cols );
190              
191             # transpose
192 5         61658 my $AT = ~$A;
193 5         2529 my $M = $AT * $A;
194              
195             # residuals
196             my @beta =
197             map {
198 5         6889 $ydata[$_] - $formula_sub->(
199             $xdata[$_],
200 343         202979 map { $_->[1] } @parameters
  968         2061  
201             )
202             } 0 .. $#xdata;
203 5         3173 $dbeta = Math::MatrixReal->new_from_cols( [ \@beta ] );
204              
205 5         17122 my $N = $AT * $dbeta;
206              
207             # Normalize before solving => better accuracy.
208 5         2048 my ( $matrix, $vector ) = $M->normalize($N);
209              
210             # solve
211 5         1210 my $LR = $matrix->decompose_LR();
212 5         1346 my ( $dim, $x, $B ) = $LR->solve_LR($vector);
213              
214             # extract parameter modifications and test for convergence
215 5         812 my $last = 1;
216 5         20 foreach my $pno ( 1 .. @parameters ) {
217 16         37 my $dlambda = $x->element( $pno, 1 );
218 16 100       175 $last = 0 if abs($dlambda) > $parameters[ $pno - 1 ][2];
219 16         37 $parameters[ $pno - 1 ][1] += $dlambda;
220             }
221 5 100       331 last if $last;
222             }
223              
224             # Recalculate dbeta for the squared residuals.
225             my @beta =
226             map {
227 2         9 $ydata[$_] - $formula_sub->(
228             $xdata[$_],
229 148         51414 map { $_->[1] } @parameters
  390         540  
230             )
231             } 0 .. $#xdata;
232 2         859 $dbeta = Math::MatrixReal->new_from_cols( [ \@beta ] );
233              
234 2         7018 my $square_residual = $dbeta->scalar_product($dbeta);
235 2         591 return $square_residual;
236             }
237              
238             1;
239             __END__