File Coverage

blib/lib/Math/Numerical.pm
Criterion Covered Total %
statement 136 144 94.4
branch 39 60 65.0
condition 27 45 60.0
subroutine 17 17 100.0
pod 2 2 100.0
total 221 268 82.4


line stmt bran cond sub pod time code
1             package Math::Numerical;
2              
3 2     2   417574 use 5.022;
  2         17  
4 2     2   9 use strict;
  2         4  
  2         33  
5 2     2   7 use warnings;
  2         6  
  2         35  
6 2     2   7 use utf8;
  2         4  
  2         10  
7              
8             our $VERSION = 0.02;
9              
10 2     2   84 use feature 'signatures';
  2         4  
  2         321  
11 2     2   12 no warnings 'experimental::signatures';
  2         4  
  2         92  
12              
13 2     2   11 use Carp;
  2         3  
  2         141  
14 2     2   10 use Config;
  2         4  
  2         91  
15 2     2   8 use Exporter 'import';
  2         12  
  2         75  
16 2     2   19 use POSIX ();
  2         5  
  2         215  
17              
18             =pod
19              
20             =encoding utf8
21              
22             =head1 NAME
23              
24             Math::Numerical
25              
26             =head1 SYNOPSIS
27              
28             Numerical analysis and scientific computing related functions.
29              
30             use Math::Numerical ':all';
31              
32             sub f { cos($_[0]) * $_[0] ** 2 } # cos(x)·x²
33              
34             my $root = find_root(\&f, -1, 1);
35              
36             =head1 DESCRIPTION
37              
38             This module offers functions to manipulate numerical functions such as root
39             finding (solver), derivatives, etc. Most of the functions of this module can receive a
40             C<$func> argument. This argument should always be a code reference (an anonymous
41             sub or a reference to a named code block). And that referenced function should
42             expect a single scalar (numeric) argument and return a single scalar (numeric)
43             value. For efficiency reason, it is recommended to not name the argument of that
44             function (see the L).
45              
46             =head1 FUNCTIONS
47              
48             By default, none of the functions below are exported by this package. They can
49             be selectively imported or you can import them all with the tag C<:all>.
50              
51             =cut
52              
53             our @EXPORT = ();
54             our @EXPORT_OK = qw(find_root bracket);
55             our %EXPORT_TAGS = (all => \@EXPORT_OK);
56              
57             # This will need to be adapted if we start using bigfloat.
58 2 50   2   13 use constant _EPS => $Config{uselongdouble} ? POSIX::LDBL_EPSILON : POSIX::DBL_EPSILON;
  2         3  
  2         326  
59 2     2   12 use constant _DEFAULT_TOLERANCE => 0.00001;
  2         4  
  2         2688  
60              
61             # Wraps the given numerical function in a way where we’re guaranteeing that it’s
62             # called in a scalar context and where we’re trapping its errors.
63 8     8   10 sub _wrap_func($func) {
  8         12  
  8         10  
64 8 50       20 croak "The passed $func is not a code reference" unless ref($func) eq 'CODE';
65             return sub {
66 39     39   45 my $r = eval { &$func };
  39         145  
67 39 50       315 return $r if defined $r;
68 0 0       0 croak "The function failed: $@" if $@;
69 0         0 croak "The function returned no value";
70             }
71 8         43 }
72              
73             # Returns a value with the same magnitude as $val and the same sign as $sign.
74             sub _sign { # ($val, $sign)
75 3 50   3   9 return $_[0] * $_[1] > 0 ? $_[0] : -$_[0];
76             }
77              
78             =head2 find_root
79              
80             find_root($func, $x1, $x2, %params)
81              
82             Given a function C<$func> assumed to be continuous and a starting interval
83             C<[$x1, $x2]>, tries to find a root of the function (a point where the
84             function’s value is 0). The root found may be either inside or outside the
85             starting interval.
86              
87             If the function is successful it returns the root found in scalar context or, in
88             list context, a list with the root and the value of the function at that point
89             (which may not be exactly C<0>).
90              
91             The current implementation of this function is based on the method C
92             from the I> book.
93              
94             The function supports the following parameters:
95              
96             =over
97              
98             =item C
99              
100             How many iterations of our algorithm will be applied at most while trying to
101             find a root for the given function. This gives an order of magnitude of the
102             number of times that C<$func> will be evaluated. Defaults to I<100>.
103              
104             =item C
105              
106             Whether the C> function should be used to bracket a root of the
107             function before finding the root. If this is set to a false value, then the
108             passed C<$x1> and C<$x2> values must already form a bracket (that is, the
109             function must take values of opposite sign at these two points). Note that, when
110             they do, the C> function will immediately return these values. So,
111             if C return a result with C set to a I value, it
112             will return the same result when C is set to a I value.
113             However, if C is set to a I value, then C will
114             immediately C> if the starting interval does not form a bracket
115             of a root of the function.
116              
117             When set to a I value, the C> function is called with the
118             same arguments as those given to C, so any parameter supported by
119             C> can also be passed to C.
120              
121             Defaults to I<1>.
122              
123             =item C
124              
125             Defaults to I<0.00001>.
126              
127             =back
128              
129             In addition, as noted above, when C is true, any of the parameters
130             supported by the C> function can be passed and they will be
131             forwarded to that function.
132              
133             =cut
134              
135 4     4 1 122447 sub find_root($func, $x1, $x2, %params) {
  4         8  
  4         7  
  4         5  
  4         8  
  4         4  
136 4   100     18 my $do_bracket = $params{do_bracket} // 1;
137 4   50     16 my $tol = $params{tolerance} // _DEFAULT_TOLERANCE;
138 4   50     13 my $max_iter = $params{max_iteration} // 100;
139 4         11 my $f = _wrap_func($func);
140 4         18 my ($a, $b, $c, $d, $e); # = ($x1, $x2, $x2);
141 4         0 my ($fa, $fb, $fc); # = ($f->($a), $f->($b));
142 4         0 my ($p, $q, $r, $s, $tol1, $xm);
143 4 100       7 if ($do_bracket) {
144 3         10 ($a, $b, $fa, $fb) = bracket($func, $x1, $x2, %params);
145 3 50       8 croak "Can’t bracket a root of the function" unless defined $a;
146             } else {
147 1         4 ($a, $b) = ($x1, $x2);
148 1         3 ($fa, $fb) = ($f->($a), $f->($b));
149 1 50 33     19 croak "A root must be bracketed in [\$x1; \$x2]"
      33        
      33        
150             if ($fa > 0 && $fb > 0) || ($fa < 0 && $fb <0);
151             }
152 4         7 ($c, $fc) = ($b, $fb);
153 4         21 for my $i (1..$max_iter) {
154 25 100 100     90 if (($fb > 0 && $fc > 0) || ($fb < 0 && $fc < 0)) {
      100        
      100        
155 21         23 ($c, $fc) = ($a, $fa);
156 21         34 $e = $d = $b - $a;
157             }
158 25 100       44 if (abs($fc) < abs($fb)) {
159 2         6 ($a, $b, $c) = ($b, $c, $b);
160 2         3 ($fa, $fb, $fc) = ($fb, $fc, $fb);
161             }
162 25         33 $tol1 = 2 * _EPS * abs($b) + 0.5 * $tol;
163 25         27 $xm = 0.5 * ($c - $b);
164 25 50 66     93 return wantarray ? ($b, $fb) : $b if abs($xm) <= $tol1 || $fb == 0;
    100          
165 21 100 66     53 if (abs($e) >= $tol1 && abs($fa) > abs($fb)) {
166 19         23 $s = $fb / $fa;
167 19 100       25 if ($a == $c) {
168 17         19 $p = 2 * $xm *$s;
169 17         19 $q = 1 - $s;
170             } else {
171 2         3 $q = $fa / $fc;
172 2         2 $r = $fb / $fc;
173 2         5 $p = $s * (2 * $xm * $q * ($q - $r)- ($b - $a) * ($r - 1));
174 2         4 $q = ($q - 1) * ($r - 1) * ($s - 1);
175             }
176 19 100       35 $q = -$q if $p > 0;
177 19         19 $p = abs($p);
178 19         26 my $min1 = 3 * $xm * $q - abs($tol1 * $q);
179 19         22 my $min2 = abs($e* $q);
180 19 100       31 if (2 * $p < ($min1 < $min2 ? $min1 : $min2)) {
    50          
181 19         20 $e = $d;
182 19         32 $d = $p / $q;
183             } else {
184 0         0 $d = $xm;
185 0         0 $e = $d;
186             }
187             } else {
188 2         3 $d = $xm;
189 2         4 $e = $d;
190             }
191 21         25 ($a, $fa) = ($b, $fb);
192 21 100       26 if (abs($d) > $tol1) {
193 18         20 $b +=$d;
194             } else {
195 3         11 $b += _sign($tol1, $xm);
196             }
197 21         29 $fb = $f->($b);
198             }
199 0         0 return;
200             }
201              
202             =head2 bracket
203              
204             bracket($func, $x1, $x2, %params)
205              
206             Given a function C<$func> assumed to be continuous and a starting interval
207             C<[$x1, $x2]>, tries to find a pair of point C<($a, $b)> such that the function
208             has a root somewhere between these two points (the root is I by these
209             points). The found points will be either inside or outside the starting
210             interval.
211              
212             If the function is successful, it returns a list of four elements with the values
213             C<$a> and C<$b> and then the values of function at these two points. Otherwise
214             it returns an empty list.
215              
216             The function will C> if C<$x1> and C<$x2> are equal.
217              
218             The current implementation of this method is a mix of the methods C and
219             C from the I> book.
220              
221             The function supports the following parameters:
222              
223             =over
224              
225             =item C
226              
227             How many iterations of our algorithm will be applied at most while trying to
228             bracket the given function. This gives an order of magnitude of the number of
229             times that C<$func> will be evaluated. Defaults to I<100>.
230              
231             =item C
232              
233             Whether the function will try to bracket a root in an interval larger than the
234             one given by C<[$x1, $x2]>. Defaults to I<1>.
235              
236             =item C
237              
238             Whether the function will try to bracket a root in an interval smaller than the
239             one given by C<[$x1, $x2]>. Defaults to I<1>.
240              
241             =item C
242              
243             Tuning parameter describing the starting number of intervals into which the
244             starting interval is split when looking inward for a bracket. Defaults to I<3>.
245              
246             Note that the algorithm may change and this parameter may stop working or may
247             take a different meaning in the future.
248              
249             =item C
250              
251             Tuning parameter describing a factor by which the inwards interval are split
252             at each iteration. Defaults to I<3>.
253              
254             Note that the algorithm may change and this parameter may stop working or may
255             take a different meaning in the future.
256              
257             =item C
258              
259             Tuning parameter describing how much the starting interval is grown at each
260             iteration when looking outward for a bracket. Defaults to I<1.6>.
261              
262             Note that the algorithm may change and this parameter may stop working or may
263             take a different meaning in the future.
264              
265             =back
266              
267             =cut
268              
269 4     4 1 104032 sub bracket ($func, $x1, $x2, %params) {
  4         7  
  4         6  
  4         6  
  4         6  
  4         5  
270 4 50       12 croak "\$x1 and \$x2 must be distinct in calls to Math::Numerical::bracket (${x1})" if $x1 == $x2;
271 4   50     28 my $max_iter = $params{max_iteration} // 100;
272 4 50       11 croak "max_iteration must be positive" unless $max_iter > 0;
273 4   50     12 my $do_outward = $params{do_outward} // 1;
274 4   50     12 my $do_inward = $params{do_inward} // 1;
275 4 50 33     10 croak "One of do_outward and do_inward at least should be true"
276             unless $do_outward || $do_inward;
277 4   50     14 my $inward_split = $params{inward_split} // 3;
278 4 50       11 croak "inward_split must be at least 2" unless $inward_split >= 2;
279 4   50     13 my $inward_factor = $params{inward_factor} // 3;
280 4 50       9 croak "inward_factor must be at least 2" unless $inward_factor >= 2;
281 4   50     20 my $outward_factor = $params{outward_factor} // 1.6;
282 4 50       14 croak "outward_factor must be larger than 1" unless $outward_factor > 1;
283 4         7 my $f = _wrap_func($func);
284 4         11 my ($xl1, $xl2) = ($x1, $x2);
285 4         8 my $f1 = $f->($x1);
286 4         10 my ($fl1, $fl2) = ($f1, $f->($xl2));
287 4         15 for my $i (1..$max_iter) {
288             # We start with outward because the first iteration does nothing and just
289             # checks the bounds that were given by the user.
290 6 50       12 if ($do_outward) {
291 6 100       39 return ($xl1, $xl2, $fl1, $fl2) if $fl1 * $fl2 < 0;
292 2 50       18 if (abs($fl1) < abs($fl2)) {
293 0         0 $xl1 += $outward_factor * ($xl1 -$xl2);
294 0         0 $fl1 = $f->($xl1);
295             } else {
296 2         7 $xl2 += $outward_factor * ($xl2 -$xl1);
297 2         5 $fl2 = $f->($xl2);
298             }
299             }
300 2 50       6 if ($do_inward) {
301 2         7 my $dx = ($x2 - $x1) / $inward_split;
302 2         3 my $a = $x1;
303 2         5 my ($fa, $fb) = ($f1);
304 2         5 for my $j (1..$inward_split) {
305 6         9 my $b = $a + $dx;
306 6         10 $fb = $f->($b);
307 6 50       16 return ($a, $b, $fa, $fb) if $fa * $fb < 0;
308 6         9 ($a, $fa) = ($b, $fb);
309             }
310 2         4 $inward_split *= $inward_factor;
311             }
312             # We stop doing the inward algorithm when the number of splits exceeds
313             # max_iteration, to bound the number of times the function is executed to a
314             # reasonable value.
315 2 50       19 $do_inward = 0 if $inward_split >= $max_iter;
316             }
317 0           return;
318             }
319              
320             =head1 AUTHOR
321              
322             Mathias Kende
323              
324             =head1 COPYRIGHT AND LICENSE
325              
326             Copyright 2022 Mathias Kende
327              
328             Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
329              
330             The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
331              
332             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
333              
334             =head1 SEE ALSO
335              
336             =over
337              
338             =item NR
339              
340             L
341              
342             =back
343              
344             =cut
345              
346             1;