changeset 27952:6d35ab1cd544

quadv.m: Overhaul implementation (bug #57603). Divide input range in to three intervals in the same manner as Matlab. This avoids issues with periodic functions where a, b, and (a+b)/2 may all be special values. Implement Richardson extrapolation correction which improves performance by reducing required function evaluations by ~3.5X. * quadv.m: Add http link to Wikipedia describing Adaptive Simpson's Method. Add column headers to the trace display output. Divide integral in to 3 parts using Matlab factor of 0.27158 for compatibility. Adjust comments about the implementation of algorithm to use either NOTE or FIXME. Correctly increment number of function evaluations nfun if the function is re-evaluated due to a singularity at an endpoint. Adjust BIST tolerances for new implementation. Add BIST test for bug #57603. * quadv.m (simpsonstp): Re-order argument in function prototype for clarity. Use "return" to exit subroutine quickly if nfun > 10,000. Use printf() with better format codes, rather than disp(), to display trace activity. Change stopping criteria to "norm (delta, Inf) > 15*tol". Add FIXME note about how Matlab, and Octave following it, do not correctly reduce the tolerance to tol/2 for each recursive call. Use Richardson extrapolation correction and add 1/15 of the error term (q - q0) back to the computed q for the current subinterval.
author Rik <rik@octave.org>
date Thu, 16 Jan 2020 20:58:34 -0800
parents d34d33837fc4
children 55a2bbb9a71e
files scripts/general/quadv.m
diffstat 1 files changed, 96 insertions(+), 41 deletions(-) [+]
line wrap: on
line diff
--- a/scripts/general/quadv.m	Fri Jan 17 10:25:38 2020 +0900
+++ b/scripts/general/quadv.m	Thu Jan 16 20:58:34 2020 -0800
@@ -67,6 +67,9 @@
 ##          integral2, integral3}
 ## @end deftypefn
 
+## Algorithm: See https://en.wikipedia.org/wiki/Adaptive_Simpson%27s_method
+## for basic explanation.  See NOTEs and FIXME for Octave modifications.
+
 function [q, nfun] = quadv (f, a, b, tol = [], trace = false, varargin)
 
   if (nargin < 3)
@@ -84,27 +87,67 @@
     error ("quadv: TOL must be a scalar >=0");
   endif
 
-  ## Split the interval into 3 abscissa, and apply a 3-point Simpson's rule
-  c = (a + b) / 2;
-  fa = feval (f, a, varargin{:});
-  fc = feval (f, c, varargin{:});
-  fb = feval (f, b, varargin{:});
-  nfun = 3;
+  if (trace)
+    ## Print column headers once above trace display.
+    printf ("  nfun          a            (b - a)         q_interval\n");
+  endif
 
-  ## If there are edge singularities, move edge point by eps*(b-a) as
+  ## NOTE: Split the interval into 3 parts which avoids problems with periodic
+  ## functions when a, b, and (a + b)/2 fall on boundaries such as 0, 2*pi.
+  ## For compatibility with Matlab, split in to two equal size regions on the
+  ## left and right, and one larger central region.
+  alpha = 0.27158;   # factor for region 1 & region 3 size (~27%)
+  b1 = a + alpha * (b - a);
+  b2 = b - alpha * (b - a);
+  c1 = (a + b1) / 2;
+  c2 = (a + b)  / 2;
+  c3 = (b2 + b) / 2;
+
+  fa  = feval (f, a,  varargin{:});
+  fc1 = feval (f, c1, varargin{:});
+  fb1 = feval (f, b1, varargin{:});
+  fc2 = feval (f, c2, varargin{:});
+  fb2 = feval (f, b2, varargin{:});
+  fc3 = feval (f, c3, varargin{:});
+  fb  = feval (f, b,  varargin{:});
+  nfun = 7;
+
+  ## NOTE: If there are edge singularities, move edge point by eps*(b-a) as
   ## discussed in Shampine paper used to implement quadgk.
   if (any (isinf (fa(:))))
     fa = feval (f, a + eps * (b-a), varargin{:});
+    nfun++;
   endif
   if (any (isinf (fb(:))))
     fb = feval (f, b - eps * (b-a), varargin{:});
+    nfun++;
   endif
 
-  h = (b - a);
-  q = h / 6 * (fa + 4 * fc + fb);
+  ## Region 1
+  h = (b1 - a);
+  q1 = h / 6 * (fa + 4*fc1 + fb1);
+
+  [q1, nfun, hmin1] = simpsonstp (f, a, b1, c1, fa, fb1, fc1, q1, tol,
+                                  nfun, abs (h), trace, varargin{:});
+
+  ## Region 2
+  h = (b2 - b1);
+  q2 = h / 6 * (fb1 + 4*fc2 + fb2);
 
-  [q, nfun, hmin] = simpsonstp (f, a, b, c, fa, fb, fc, q, nfun, abs (h),
-                                tol, trace, varargin{:});
+  [q2, nfun, hmin2] = simpsonstp (f, b1, b2, c2, fb1, fb2, fc2, q2, tol,
+                                  nfun, abs (h), trace, varargin{:});
+
+  ## Region 3
+  h = (b - b2);
+  q3 = h / 6 * (fb2 + 4*fc3 + fb);
+
+  [q3, nfun, hmin3] = simpsonstp (f, b2, b, c3, fb2, fb, fc3, q3, tol,
+                                  nfun, abs (h), trace, varargin{:});
+
+  ## Total integral over all 3 regions and verify results
+  q = q1 + q2 + q3;
+
+  hmin = min ([hmin1, hmin2, hmin3]);
 
   if (nfun > 10_000)
     warning ("quadv: maximum iteration count reached -- possible singular integral");
@@ -116,39 +159,48 @@
 
 endfunction
 
-function [q, nfun, hmin] = simpsonstp (f, a, b, c, fa, fb, fc, q0, nfun, hmin,
-                                       tol, trace, varargin)
+function [q, nfun, hmin] = simpsonstp (f, a, b, c, fa, fb, fc, q0, tol,
+                                       nfun, hmin, trace, varargin)
 
-  if (nfun > 10_000)
+  if (nfun > 10_000)   # stop endless recursion
     q = q0;
-  else
-    d = (a + c) / 2;
-    e = (c + b) / 2;
-    fd = feval (f, d, varargin{:});
-    fe = feval (f, e, varargin{:});
-    nfun += 2;
-    q1 = (c - a) / 6 * (fa + 4 * fd + fc);
-    q2 = (b - c) / 6 * (fc + 4 * fe + fb);
-    q = q1 + q2;
+    return;
+  endif
+
+  d = (a + c) / 2;
+  e = (c + b) / 2;
+  fd = feval (f, d, varargin{:});
+  fe = feval (f, e, varargin{:});
+  nfun += 2;
+  q1 = (c - a) / 6 * (fa + 4*fd + fc);
+  q2 = (b - c) / 6 * (fc + 4*fe + fb);
+  q = q1 + q2;
+
+  if (abs (a - c) < hmin)
+    hmin = abs (a - c);
+  endif
 
-    if (abs (a - c) < hmin)
-      hmin = abs (a - c);
-    endif
+  delta = q - q0;   # error term between new estimate and old estimate
 
-    if (trace)
-      disp ([nfun, a, b-a, q]);
-    endif
+  if (trace)
+    printf ("%5d   %#14.11g   %16.10e   %-16.11g\n",
+            nfun,  a,         b-a,      q + delta/15);
+  endif
 
-    ## Force at least one adaptive step (nfun == 5 test).
-    ## Not vectorizing q-q0 in the norm provides a more rigid criterion for
-    ## matrix-valued functions.
-    if (norm (q - q0, Inf) > tol || nfun == 5)
-      [q1, nfun, hmin] = simpsonstp (f, a, c, d, fa, fc, fd, q1, nfun, hmin,
-                                     tol, trace, varargin{:});
-      [q2, nfun, hmin] = simpsonstp (f, c, b, e, fc, fb, fe, q2, nfun, hmin,
-                                     tol, trace, varargin{:});
-      q = q1 + q2;
-    endif
+  ## NOTE: Not vectorizing q-q0 in the norm provides a more rigid criterion
+  ##       for matrix-valued functions.
+  if (norm (delta, Inf) > 15*tol)
+    ## FIXME: To keep sum of sub-interval integrands within overall tolerance
+    ## each bisection interval should use tol/2.  However, Matlab does not
+    ## do this, and it would also profoundly increase the number of function
+    ## evaluations required.
+    [q1, nfun, hmin] = simpsonstp (f, a, c, d, fa, fc, fd, q1, tol,
+                                   nfun, hmin, trace, varargin{:});
+    [q2, nfun, hmin] = simpsonstp (f, c, b, e, fc, fb, fe, q2, tol,
+                                   nfun, hmin, trace, varargin{:});
+    q = q1 + q2;
+  else
+    q += delta / 15;   # NOTE: Richardson extrapolation correction
   endif
 
 endfunction
@@ -158,14 +210,17 @@
 %!assert (quadv (@sin, 0, pi), 2, 1e-6)
 
 ## Test weak singularities at the edge
-%!assert (quadv (@(x) 1 ./ sqrt (x), 0, 1), 2, 2e-6);
+%!assert (quadv (@(x) 1 ./ sqrt (x), 0, 1), 2, 15*1e-6);
 
 ## Test vector-valued functions
 %!assert (quadv (@(x) [(sin (x)), (sin (2 * x))], 0, pi), [2, 0], 1e-6)
 
 ## Test matrix-valued functions
 %!assert (quadv (@(x) [ x,x,x; x,1./sqrt(x),x; x,x,x ], 0, 1),
-%!        [0.5,0.5,0.5; 0.5,2,0.5; 0.5,0.5,0.5], 2e-6);
+%!        [0.5,0.5,0.5; 0.5,2,0.5; 0.5,0.5,0.5], 15*1e-6);
+
+## Test periodic function
+%!assert <*57603> (quadv (@(t) sin (t) .^ 2, 0, 8*pi), 4*pi, 1e-6)
 
 ## Test input validation
 %!error quadv ()