File Coverage

blib/lib/Algorithm/CurveFit.pm
Criterion Covered Total %
statement 123 134 91.7
branch 27 48 56.2
condition 13 36 36.1
subroutine 11 11 100.0
pod 1 1 100.0
total 175 230 76.0


line stmt bran cond sub pod time code
1             package Algorithm::CurveFit;
2              
3 3     3   76884 use 5.006;
  3         10  
  3         108  
4 3     3   16 use strict;
  3         3  
  3         100  
5 3     3   13 use warnings;
  3         5  
  3         455  
6              
7             our $VERSION = '1.05';
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   17 use Carp qw/confess/;
  3         5  
  3         237  
22 3     3   3487 use Math::Symbolic qw/parse_from_string/;
  3         574457  
  3         1794  
23 3     3   5206 use Math::MatrixReal;
  3         73039  
  3         278  
24 3     3   44 use Data::Dumper;
  3         6  
  3         176  
25              
26             # machine epsilon
27 3     3   16 use constant EPS => 2.2e-16;
  3         5  
  3         386  
28 3     3   15 use constant SQRT_EPS => sqrt(EPS);
  3         6  
  3         5057  
29              
30             sub curve_fit {
31 2 100 33 2 1 252 shift @_ if not ref $_[0] and defined $_[0] and $_[0] eq 'Algorithm::CurveFit';
      66        
32              
33 2 50       12 confess('Uneven number of arguments to Algorithm::CurveFit::curve_fit.')
34             if @_ % 2;
35              
36 2         18 my %args = @_;
37              
38             # Formula
39 2 50       8 confess("Missing 'formula' parameter.") if not defined $args{formula};
40 2         5 my $formula;
41 2 50       11 if (ref($args{formula}) =~ /^Math::Symbolic/) {
42 0         0 $formula = $args{formula};
43             }
44             else {
45 2         6 eval { $formula = parse_from_string( $args{formula} ); };
  2         17  
46 2 50 33     41643 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       7 $variable = 'x' if not defined $variable;
53 8         619 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();
58              
59             # Parameters
60 2         9 my $params = $args{params};
61              
62 2 50 33     35 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       8 confess('No parameters specified.') if not @parameters;
67 6   33     42 confess('Individual parameters need to be array references.')
68 2 50       6 if grep { not defined $_ or not ref($_) eq 'ARRAY' } @parameters;
69 2         6 foreach my $p (@parameters) {
70 18         41 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     39 or grep { not defined $_ } @$p;
      33        
78              
79 26         901 confess("Formula '"
80             . $args{formula}
81             . "' not explicitly dependent on "
82             . "parameter '"
83             . $p->[0]
84             . "'." )
85 6 50       18 if not grep { $_ eq $p->[0] } $formula->explicit_signature();
86             }
87              
88             # XData
89 2         7 my $xdata = $args{xdata};
90 2 50 33     24 confess('X-Data missing.')
      33        
91             if not defined $xdata
92             or not ref($xdata) eq 'ARRAY'
93             or not @$xdata;
94 2         30 my @xdata = @$xdata;
95              
96             # YData
97 2         5 my $ydata = $args{ydata};
98 2 50 33     23 confess('Y-Data missing.')
      33        
99             if not defined $ydata
100             or not ref($ydata) eq 'ARRAY'
101             or not @$ydata;
102 2 50       7 confess('Y-Data and X-Data need to have the same number of elements.')
103             if not @$ydata == @xdata;
104 2         30 my @ydata = @$ydata;
105              
106             # Max_Iter (optional)
107 2         5 my $max_iter = $args{maximum_iterations};
108 2 50       6 $max_iter = 0 if not defined $max_iter;
109              
110             # Add third element (dlamda) to parameter arrays in case they're missing.
111 2         4 foreach my $param (@parameters) {
112 6 50       15 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         4 my @param_names = ($variable, map {$_->[0]} @parameters);
  6         13  
119 2         5 foreach my $param (@parameters) {
120 6         30 my $deriv =
121             Math::Symbolic::Operator->new( 'partial_derivative', $formula,
122             $param->[0] );
123 6         18251 $deriv = $deriv->simplify()->apply_derivatives()->simplify();
124 6         131820 my ($sub, $trees) = Math::Symbolic::Compiler->compile_to_sub($deriv, \@param_names);
125 6 50       4824 if ($trees) {
126 6         88 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         5 my $formula_sub = do {
134 2         12 my ($sub, $trees) = Math::Symbolic::Compiler->compile_to_sub($formula, \@param_names);
135             $trees
136             ? sub {
137 2089         5033 $formula->value(
138 539     539   1073 map { ($param_names[$_] => $_[$_]) } 0..$#param_names
139             )
140             }
141 2 50       1189 : $sub
142             };
143              
144 2         4 my $dbeta;
145              
146             # Iterative approximation of the parameters
147 2         7 my $iteration = 0;
148              
149             # As long as we're under max_iter or maxiter==0
150 2   33     24 while ( !$max_iter || ++$iteration < $max_iter ) {
151             # Generate Matrix A
152 6         13 my @cols;
153 6         10 my $pno = 0;
154 6         13 my @par_values = map {$_->[1]} @parameters;
  20         49  
155 6         17 foreach my $param (@parameters) {
156 20         57 my $deriv = $derivatives[ $pno++ ];
157              
158 20         37 my @ary;
159 20 50       89 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, @parameters)
166             }
167 0         0 push @ary, $value;
168             }
169             }
170             else {
171 20         88 $deriv = $deriv->new; # better safe than sorry
172 20         8406 foreach my $x ( 0 .. $#xdata ) {
173 1156         1780 my $xv = $xdata[$x];
174 3816         9846 my $value = $deriv->value(
175             $variable => $xv,
176 1156         1785 map { ( @{$_}[ 0, 1 ] ) } @parameters # a, guess
  3816         4007  
177             );
178 1156 100       1096491 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         6 $value = $formula_sub->($xv, @parameters)
181             }
182 1156         3063 push @ary, $value;
183             }
184             }
185 20         1554 push @cols, \@ary;
186             }
187              
188             # Prepare matrix of datapoints X parameters
189 6         287 my $A = Math::MatrixReal->new_from_cols( \@cols );
190              
191             # transpose
192 6         49899 my $AT = ~$A;
193 6         2634 my $M = $AT * $A;
194              
195             # residuals
196 1156         2237 my @beta =
197             map {
198 6         7208 $ydata[$_] - $formula_sub->(
199             $xdata[$_],
200 390         206729 map { $_->[1] } @parameters
201             )
202             } 0 .. $#xdata;
203 6         3435 $dbeta = Math::MatrixReal->new_from_cols( [ \@beta ] );
204              
205 6         18467 my $N = $AT * $dbeta;
206              
207             # Normalize before solving => better accuracy.
208 6         2579 my ( $matrix, $vector ) = $M->normalize($N);
209              
210             # solve
211 6         1300 my $LR = $matrix->decompose_LR();
212 6         9959 my ( $dim, $x, $B ) = $LR->solve_LR($vector);
213              
214             # extract parameter modifications and test for convergence
215 6         908 my $last = 1;
216 6         25 foreach my $pno ( 1 .. @parameters ) {
217 20         89 my $dlambda = $x->element( $pno, 1 );
218 20 100       237 $last = 0 if abs($dlambda) > $parameters[ $pno - 1 ][2];
219 20         51 $parameters[ $pno - 1 ][1] += $dlambda;
220             }
221 6 100       475 last if $last;
222             }
223              
224             # Recalculate dbeta for the squared residuals.
225 390         768 my @beta =
226             map {
227 2         22 $ydata[$_] - $formula_sub->(
228             $xdata[$_],
229 148         74094 map { $_->[1] } @parameters
230             )
231             } 0 .. $#xdata;
232 2         1429 $dbeta = Math::MatrixReal->new_from_cols( [ \@beta ] );
233              
234 2         7590 my $square_residual = $dbeta->scalar_product($dbeta);
235 2         485 return $square_residual;
236             }
237              
238             1;
239             __END__