File Coverage

blib/lib/Math/Numerical.pm
Criterion Covered Total %
statement 223 224 99.5
branch 64 64 100.0
condition 53 66 80.3
subroutine 24 24 100.0
pod 3 3 100.0
total 367 381 96.3


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