File Coverage

blib/lib/Math/Numerical.pm
Criterion Covered Total %
statement 232 233 99.5
branch 64 64 100.0
condition 53 66 80.3
subroutine 25 25 100.0
pod 3 3 100.0
total 377 391 96.4


line stmt bran cond sub pod time code
1             package Math::Numerical;
2              
3 2     2   374695 use 5.022;
  2         15  
4 2     2   10 use strict;
  2         3  
  2         52  
5 2     2   9 use warnings;
  2         2  
  2         41  
6 2     2   8 use utf8;
  2         3  
  2         9  
7              
8             our $VERSION = 0.05;
9              
10 2     2   79 use feature 'signatures';
  2         4  
  2         264  
11 2     2   12 no warnings 'experimental::signatures';
  2         2  
  2         72  
12              
13 2     2   9 use Carp;
  2         3  
  2         101  
14 2     2   12 use Config;
  2         3  
  2         71  
15 2     2   735 use English;
  2         5680  
  2         10  
16 2     2   731 use Exporter 'import';
  2         3  
  2         55  
17 2     2   4242 use Hash::Util 'lock_keys';
  2         9950  
  2         12  
18 2     2   202 use POSIX ();
  2         3  
  2         32  
19 2     2   406 use Readonly;
  2         3259  
  2         4782  
20              
21             =pod
22              
23             =encoding utf8
24              
25             =head1 NAME
26              
27             Math::Numerical
28              
29             =head1 SYNOPSIS
30              
31             Numerical analysis and scientific computing related functions.
32              
33             use Math::Numerical ':all';
34              
35             sub f { cos($_[0]) * $_[0] ** 2 } # cos(x)·x²
36              
37             my $root = find_root(\&f, -1, 1);
38              
39             =head1 DESCRIPTION
40              
41             This module offers functions to manipulate numerical functions such as root
42             finding (solver), derivatives, etc. Most of the functions of this module can
43             receive a C<$func> argument. This argument should always be a code reference (an
44             anonymous sub or a reference to a named code block). And that referenced
45             function should expect a single scalar (numeric) argument and return a single
46             scalar (numeric) value. For efficiency reason, it is recommended to not name the
47             argument of that function (see the L).
48              
49             =head1 CONFIGURATION
50              
51             For now, this module has no global configuration available. All configuration
52             must be directly passed to the individual functions.
53              
54             =head1 FUNCTIONS
55              
56             By default, none of the functions below are exported by this package. They can
57             be selectively imported or you can import them all with the tag C<:all>.
58              
59             =cut
60              
61             our @EXPORT_OK = qw(find_root solve bracket);
62             our %EXPORT_TAGS = (all => \@EXPORT_OK);
63              
64             # This will need to be adapted if we start using bigfloat.
65             Readonly my $EPS => $Config{uselongdouble}
66             ? POSIX::LDBL_EPSILON
67             : POSIX::DBL_EPSILON;
68             Readonly our $_DEFAULT_TOLERANCE => 0.00001; # exposed for tests only.
69             Readonly my $DEFAULT_MAX_ITERATIONS => 100;
70              
71             # Wraps the given numerical function in a way where we’re guaranteeing that it’s
72             # called in a scalar context and where we’re trapping its errors.
73 36     36   43 sub _wrap_func ($func) {
  36         42  
  36         38  
74 36 100       178 croak "The passed \$func is not a code reference (${func})"
75             unless ref($func) eq 'CODE';
76             return sub {
77 861     861   872 my $r = eval { &{$func} };
  861         873  
  861         1271  
78 861 100       2181 return $r if defined $r;
79 2 100       119 croak "The function failed: $EVAL_ERROR" if $EVAL_ERROR;
80 1         138 croak 'The function returned no value';
81             }
82 35         124 }
83              
84             # Returns a value with the same magnitude as $val and the same sign as $sign.
85             sub _sign { # ($val, $sign)
86 10 100   10   24 return $_[0] * $_[1] > 0 ? $_[0] : -$_[0];
87             }
88              
89             =head2 find_root
90              
91             find_root($func, $x1, $x2, %params)
92              
93             Given a function C<$func> assumed to be continuous and a starting interval
94             C<[$x1, $x2]>, tries to find a root of the function (a point where the
95             function’s value is 0). The root found may be either inside or outside the
96             starting interval.
97              
98             If the function is successful it returns the root found in scalar context or, in
99             list context, a list with the root and the value of the function at that point
100             (which may not be exactly C<0>). Some options can control the precision of the
101             returned root. Note that, for discontinuous or pathological functions, the
102             returned value may not be a root at all.
103              
104             The current implementation of this function is based on the Brent method
105             described in the
106             I>
107             book, section 9.3.
108              
109             The function supports the following parameters:
110              
111             =over
112              
113             =item C
114              
115             How many iterations of our algorithm will be applied at most while trying to
116             find a root for the given function. This gives an order of magnitude of the
117             number of times that C<$func> will be evaluated. Defaults to I<100>.
118              
119             =item C
120              
121             Whether the C> function should be used to bracket a root of
122             the function before finding the root. If this is set to a false value, then the
123             passed C<$x1> and C<$x2> values must already form a bracket (that is, the
124             function must take values of opposite sign at these two points). Note that, when
125             they do, the C> function will immediately return these
126             values. So, if C return a result with C set to a I
127             value, it will return the same result when C is set to a I
128             value. However, if C is set to a I value, then C
129             will immediately C> if the starting interval does not form a
130             bracket of a root of the function.
131              
132             When set to a I value, the C> function is called with
133             the same arguments as those given to C, so any parameter supported by
134             C> can also be passed to C.
135              
136             Defaults to I<1>.
137              
138             =item C
139              
140             The tolerance of the root found on the x-axis. That is, the returned value or,
141             in list context, the first returned value will not be further away from the
142             actual root than this value.
143              
144             Defaults to I<0.00001>.
145              
146             =back
147              
148             In addition, as noted above, when C is true, any of the parameters
149             supported by the C> function can be passed and they will be
150             forwarded to that function.
151              
152             =cut
153              
154 8     8   9 sub _create_find_root_brent_state ($x1, $x2, $f1, $f2, %params) {
  8         9  
  8         9  
  8         9  
  8         8  
  8         19  
  8         9  
155 8         15 my $s = {ret => undef};
156 8   33     32 $s->{tol} = $params{tolerance} // $_DEFAULT_TOLERANCE;
157 8         62 @{$s}{qw(a b c fa fb fc)} = ($x1, $x2, $x2, $f1, $f2, $f2);
  8         30  
158 8         17 @{$s}{qw(d e)} = (undef) x 2;
  8         16  
159 8         12 @{$s}{qw(p q r s tol1 xm)} = (undef) x 6; ## no critic (ProhibitMagicNumbers)
  8         23  
160 8         10 lock_keys(%{$s});
  8         24  
161 8         73 return $s;
162             }
163              
164 56     56   59 sub _do_find_root_brent ($f, $s) {
  56         59  
  56         84  
  56         56  
165 56 100 100     205 if (($s->{fb} > 0 && $s->{fc} > 0) || ($s->{fb} < 0 && $s->{fc} < 0)) {
      100        
      100        
166 41         48 @{$s}{'c', 'fc'} = @{$s}{'a', 'fa'};
  41         59  
  41         49  
167 41         65 $s->{e} = $s->{d} = $s->{b} - $s->{a};
168             }
169 56 100       112 if (abs($s->{fc}) < abs($s->{fb})) {
170 10         15 @{$s}{'a', 'b', 'c'} = @{$s}{'b', 'c', 'b'};
  10         16  
  10         16  
171 10         14 @{$s}{'fa', 'fb', 'fc'} = @{$s}{'fb', 'fc', 'fb'};
  10         16  
  10         14  
172             }
173 56         114 $s->{tol1} = 2 * $EPS * abs($s->{b}) + $s->{tol} / 2;
174 56         273 $s->{xm} = ($s->{c} - $s->{b}) / 2;
175 56 100 100     159 if (abs($s->{xm}) <= $s->{tol1} || $s->{fb} == 0) {
176 8         17 $s->{ret} = [$s->{b}, $s->{fb}];
177 8         20 return 1;
178             }
179 48 100 100     132 if (abs($s->{e}) >= $s->{tol1} && abs($s->{fa}) > abs($s->{fb})) {
180 38         54 $s->{s} = $s->{fb} / $s->{fa};
181 38 100       56 if ($s->{a} == $s->{c}) {
182 34         45 $s->{p} = 2 * $s->{xm} * $s->{s};
183 34         41 $s->{q} = 1 - $s->{s};
184             } else {
185 4         7 $s->{q} = $s->{fa} / $s->{fc};
186 4         7 $s->{r} = $s->{fb} / $s->{fc};
187             $s->{p} =
188             $s->{s} *
189             (2 * $s->{xm} * $s->{q} * ($s->{q} - $s->{r}) -
190 4         10 ($s->{b} - $s->{a}) * ($s->{r} - 1));
191 4         8 $s->{q} = ($s->{q} - 1) * ($s->{r} - 1) * ($s->{s} - 1);
192             }
193 38 100       67 $s->{q} = -$s->{q} if $s->{p} > 0;
194 38         46 $s->{p} = abs($s->{p});
195 38         655 Readonly my $interp_coef => 3;
196 38         2774 my $min1 = $interp_coef * $s->{xm} * $s->{q} - abs($s->{tol1} * $s->{q});
197 38         218 my $min2 = abs($s->{e} * $s->{q});
198 38 100       83 if (2 * $s->{p} < ($min1 < $min2 ? $min1 : $min2)) {
    100          
199 37         49 $s->{e} = $s->{d};
200 37         79 $s->{d} = $s->{p} / $s->{q};
201             } else {
202 1         3 $s->{e} = $s->{d} = $s->{xm};
203             }
204             } else {
205 10         17 $s->{e} = $s->{d} = $s->{xm};
206             }
207 48         58 @{$s}{'a', 'fa'} = @{$s}{'b', 'fb'};
  48         66  
  48         68  
208 48 100       87 if (abs($s->{d}) > $s->{tol1}) {
209 38         51 $s->{b} += $s->{d};
210             } else {
211 10         22 $s->{b} += _sign($s->{tol1}, $s->{xm});
212             }
213 48         70 $s->{fb} = $f->($s->{b});
214 48         124 return 0;
215             }
216              
217 13     13 1 10923 sub find_root ($func, $x1, $x2, %params) {
  13         18  
  13         17  
  13         15  
  13         18  
  13         15  
218 13   100     41 my $do_bracket = $params{do_bracket} // 1;
219 13   33     51 my $max_iter = $params{max_iterations} // $DEFAULT_MAX_ITERATIONS;
220 13         98 my $f = _wrap_func($func);
221 12         21 my ($xa, $xb, $fa, $fb);
222 12 100       21 if ($do_bracket) {
223 10         23 ($xa, $xb, $fa, $fb) = bracket($func, $x1, $x2, %params);
224 8 100       146 croak 'Can’t bracket a root of the function' unless defined $xa;
225             } else {
226 2         4 ($xa, $xb) = ($x1, $x2);
227 2         5 ($fa, $fb) = ($f->($xa), $f->($xb));
228 2 100 66     240 croak 'A root must be bracketed in [\$x1; \$x2]'
      33        
      66        
229             if ($fa > 0 && $fb > 0) || ($fa < 0 && $fb < 0);
230             }
231              
232 8         19 my $brent_state = _create_find_root_brent_state($xa, $xb, $fa, $fb, %params);
233              
234 8         22 for my $i (1 .. $max_iter) {
235 56 100 66     105 if (defined $brent_state && _do_find_root_brent($f, $brent_state)) {
236 8 100       61 return wantarray ? @{$brent_state->{ret}} : $brent_state->{ret}[0];
  1         8  
237             }
238             }
239 0         0 return;
240             }
241              
242             =head2 solve
243              
244             solve($func, $x1, $x2, %params)
245              
246             This is an exact synonym of C in case you
247             prefer another name. See the documentation of the C>
248             function for all the details.
249              
250             =cut
251              
252 1     1 1 400 sub solve { return find_root(@_) }
253              
254             =head2 bracket
255              
256             bracket($func, $x1, $x2, %params)
257              
258             Given a function C<$func> assumed to be continuous and a starting interval
259             C<[$x1, $x2]>, tries to find a pair of point C<($a, $b)> such that the function
260             has a root somewhere between these two points (the root is I by these
261             points). The found points will be either inside or outside the starting
262             interval.
263              
264             If the function is successful, it returns a list of four elements with the
265             values C<$a> and C<$b> and then the values of function at these two points.
266             Otherwise it returns an empty list.
267              
268             If C<$x2> is omitted or equal to C<$x1> then a value slightly larger than C<$x1>
269             will be used. Note that if it is omitted then C<%params> cannot be specified.
270              
271             The current implementation is a mix of the inward and outward bracketing
272             approaches exposed in the
273             I>
274             book, section 9.1.
275              
276             The function supports the following parameters:
277              
278             =over
279              
280             =item C
281              
282             How many iterations of our algorithm will be applied at most while trying to
283             bracket the given function. This gives an order of magnitude of the number of
284             times that C<$func> will be evaluated. Defaults to I<100>.
285              
286             =item C
287              
288             Whether the function will try to bracket a root in an interval larger than the
289             one given by C<[$x1, $x2]>. Defaults to I<1>.
290              
291             =item C
292              
293             Whether the function will try to bracket a root in an interval smaller than the
294             one given by C<[$x1, $x2]>. Defaults to I<1>.
295              
296             One of C or C at least must be a true value.
297              
298             =item C
299              
300             Tuning parameter describing the starting number of intervals into which the
301             starting interval is split when looking inward for a bracket. Defaults to I<3>.
302              
303             Note that the algorithm may change and this parameter may stop working or may
304             take a different meaning in the future.
305              
306             =item C
307              
308             Tuning parameter describing a factor by which the inwards interval are split
309             at each iteration. Defaults to I<3>.
310              
311             Note that the algorithm may change and this parameter may stop working or may
312             take a different meaning in the future.
313              
314             =item C
315              
316             Tuning parameter describing how much the starting interval is grown at each
317             iteration when looking outward for a bracket. Defaults to I<1.6>.
318              
319             Note that the algorithm may change and this parameter may stop working or may
320             take a different meaning in the future.
321              
322             =back
323              
324             =cut
325              
326             Readonly my $DEFAULT_INWARD_SPLIT => 3;
327             Readonly my $DEFAULT_INWARD_FACTOR => 3;
328             Readonly my $DEFAULT_OUTWARD_FACTOR => 1.6;
329              
330 19     19   26 sub _create_bracket_inward_state ($x1, $x2, $f1, %params) {
  19         26  
  19         20  
  19         22  
  19         23  
  19         20  
331 19         35 my $s = {ret => undef};
332 19   66     55 $s->{split} = $params{inward_split} // $DEFAULT_INWARD_SPLIT;
333 19 100       208 croak 'inward_split must be at least 2' if $s->{split} < 2;
334 18   66     61 $s->{factor} = $params{inward_factor} // $DEFAULT_INWARD_FACTOR;
335 18 100       187 croak 'inward_factor must be at least 2' if $s->{factor} < 2;
336 17         41 @{$s}{'x1', 'x2'} = ($x1, $x2);
  17         37  
337 17         32 $s->{f1} = $f1;
338 17         22 lock_keys(%{$s});
  17         50  
339 17         173 return $s;
340             }
341              
342 24     24   26 sub _do_bracket_inward ($f, $s) {
  24         27  
  24         31  
  24         25  
343 24         37 my $dx = ($s->{x2} - $s->{x1}) / $s->{split};
344 24         54 my $xa = $s->{x1};
345 24         37 my ($fa, $fb) = ($s->{f1});
346 24         41 for my $j (1 .. $s->{split}) {
347 500         517 my $xb = $xa + $dx;
348 500         564 $fb = $f->($xb);
349 500 100       722 if ($fa * $fb < 0) {
350 2         6 $s->{ret} = [$xa, $xb, $fa, $fb];
351 2         5 return 1;
352             }
353 498         697 ($xa, $fa) = ($xb, $fb);
354             }
355 22         26 $s->{split} *= $s->{factor};
356 22         56 return 0;
357             }
358              
359 17     17   19 sub _create_bracket_outward_state ($f, $x1, $x2, $f1, %params) {
  17         21  
  17         20  
  17         25  
  17         21  
  17         20  
  17         17  
360 17         32 my $s = {ret => undef};
361 17   66     49 $s->{factor} = $params{outward_factor} // $DEFAULT_OUTWARD_FACTOR;
362 17 100       195 croak 'outward_factor must be larger than 1' if $s->{factor} <= 1;
363 16         26 @{$s}{'x1', 'x2'} = ($x1, $x2);
  16         25  
364 16         33 @{$s}{'f1', 'f2'} = ($f1, $f->($x2));
  16         30  
365 16         18 lock_keys(%{$s});
  16         36  
366 16         129 return $s;
367             }
368              
369 282     282   277 sub _do_bracket_outward ($f, $s) {
  282         267  
  282         264  
  282         260  
370 282 100       436 if ($s->{f1} * $s->{f2} < 0) {
371 12         15 $s->{ret} = [@{$s}{'x1', 'x2', 'f1', 'f2'}];
  12         32  
372 12         32 return 1;
373             }
374 270 100       387 if (abs($s->{f1}) < abs($s->{f2})) {
375 50         60 $s->{x1} += $s->{factor} * ($s->{x1} - $s->{x2});
376 50         63 $s->{f1} = $f->($s->{x1});
377             } else {
378 220         284 $s->{x2} += $s->{factor} * ($s->{x2} - $s->{x1});
379 220         264 $s->{f2} = $f->($s->{x2});
380             }
381 270         508 return 0;
382             }
383              
384 24     24 1 12924 sub bracket ($func, $x1, $x2 = undef, %params) {
  24         33  
  24         31  
  24         29  
  24         39  
  24         24  
385 24 100 100     88 if (!defined $x2 || $x1 == $x2) {
386 7         146 Readonly my $LARGISH_FACTOR => 1000;
387 7         548 $x2 += $LARGISH_FACTOR * $EPS;
388             }
389 24   66     145 my $max_iter = $params{max_iterations} // $DEFAULT_MAX_ITERATIONS;
390 24 100       261 croak 'max_iterations must be positive' if $max_iter <= 0;
391              
392 23         33 my $f = _wrap_func($func);
393 23         41 my $f1 = $f->($x1);
394              
395 21         26 my $inward_state;
396 21 100 100     66 if ($params{do_inward} // 1) {
397 19         44 $inward_state = _create_bracket_inward_state($x1, $x2, $f1, %params);
398             }
399 19         21 my $outward_state;
400 19 100 100     58 if ($params{do_outward} // 1) {
401 17         35 $outward_state = _create_bracket_outward_state($f, $x1, $x2, $f1, %params);
402             }
403              
404 18 100 100     211 croak 'One of do_outward and do_inward at least must be true'
405             unless defined $outward_state || defined $inward_state;
406              
407 17         33 for my $i (1 .. $max_iter) {
408             # We start with outward because the first iteration does nothing and just
409             # checks the bounds that were given by the user.
410 382 100 100     540 if (defined $outward_state && _do_bracket_outward($f, $outward_state)) {
411 12         13 return @{$outward_state->{ret}};
  12         109  
412             }
413 370 100 100     536 if (defined $inward_state && _do_bracket_inward($f, $inward_state)) {
414 2         3 return @{$inward_state->{ret}};
  2         20  
415             }
416             # We stop doing the inward algorithm when the number of splits exceeds
417             # max_iteration, to bound the number of times the function is executed to a
418             # reasonable value.
419 368 100 100     542 if (defined $inward_state && $inward_state->{split} > $max_iter) {
420 4         14 undef $inward_state;
421             }
422             }
423 3         19 return;
424             }
425              
426             =head1 AUTHOR
427              
428             Mathias Kende
429              
430             =head1 COPYRIGHT AND LICENSE
431              
432             Copyright 2022 Mathias Kende
433              
434             Permission is hereby granted, free of charge, to any person obtaining a copy of
435             this software and associated documentation files (the "Software"), to deal in
436             the Software without restriction, including without limitation the rights to
437             use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
438             the Software, and to permit persons to whom the Software is furnished to do so,
439             subject to the following conditions:
440              
441             The above copyright notice and this permission notice shall be included in all
442             copies or substantial portions of the Software.
443              
444             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
445             IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
446             FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
447             COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
448             IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
449             CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
450              
451             =head1 SEE ALSO
452              
453             =over
454              
455             =item L
456              
457             =back
458              
459             =cut
460              
461             1;