# HG changeset patch # User Nicholas R. Jankowski # Date 1508020251 14400 # Node ID 2e64bed0bb3a420e56330dc39bc2a222b347bc3e # Parent 9fab3273ff26c864f83e290461696dff2eddf67a Updated integral2.m and integral3.m and added quad2d (bug #52074). * /scripts/general/quad2d.m: New function file implementing tiled integration. * integral2.m: Updated to avoid dblquad and make use of quad2d. * integral3.m: Updated to avoid double or triplequad and make use of quad2d. * /scripts/general/module.mk: Add quad2d.m to build system. * /scripts/help/__unimplemented__.m: Removed quad2d from unimplemented list. Changed BIST test which had relied on quad2d being unimplemented. * quad.txi: Updated 2D&3D integration section, added quad2d DOCSTRING. * NEWS: Announce new function quad2d. diff -r 9fab3273ff26 -r 2e64bed0bb3a NEWS --- a/NEWS Sat Oct 14 18:16:55 2017 -0500 +++ b/NEWS Sat Oct 14 18:30:51 2017 -0400 @@ -78,6 +78,7 @@ integral2 integral3 openvar + quad2d repelem rticks thetaticks diff -r 9fab3273ff26 -r 2e64bed0bb3a doc/interpreter/quad.txi --- a/doc/interpreter/quad.txi Sat Oct 14 18:16:55 2017 -0500 +++ b/doc/interpreter/quad.txi Sat Oct 14 18:30:51 2017 -0400 @@ -274,13 +274,12 @@ @node Functions of Multiple Variables @section Functions of Multiple Variables -Octave does not have built-in functions for computing the integral of -functions of multiple variables directly. It is possible, however, to -compute the integral of a function of multiple variables using the -existing functions for one-dimensional integrals. +Octave includes several functions for computing the integral of functions of +multiple variables. This procedure can generally be performed by creating a +function that integrates @math{f} with respect to @math{x}, and then integrates +that function with respect to @math{y}. This procedure can be performed +manually using the following example which integrates the function: -To illustrate how the integration can be performed, we will integrate -the function @tex $$ f(x, y) = \sin(\pi x y)\sqrt{x y} @@ -289,19 +288,16 @@ @ifnottex @example -f(x, y) = sin(pi*x*y)*sqrt(x*y) +f(x, y) = sin(pi*x*y) * sqrt(x*y) @end example @end ifnottex for @math{x} and @math{y} between 0 and 1. -The first approach creates a function that integrates @math{f} with -respect to @math{x}, and then integrates that function with respect to -@math{y}. Because @code{quad} is written in Fortran it cannot be called -recursively. This means that @code{quad} cannot integrate a function -that calls @code{quad}, and hence cannot be used to perform the double -integration. Any of the other integrators, however, can be used which is -what the following code demonstrates. +Using @code{quadgk} in the example below, a double integration can be +performed. (Note that any of the 1-D quadrature functions can be used in this +fashion except for @code{quad} since it is written in Fortran and cannot be +called recursively.) @example @group @@ -318,9 +314,10 @@ @end group @end example -The above process can be simplified with the @code{dblquad} and -@code{triplequad} functions for integrals over two and three variables. For -example: +The algorithm above is implemented in the function @code{dblquad} for integrals +over two variables. The 3-D equivalent of this process is implemented in +@code{triplequad} for integrals over three variables. As an example, the +result above can be replicated with a call to @code{dblquad} as shown below. @example @group @@ -333,19 +330,32 @@ @DOCSTRING(triplequad) -@code{integral2} and @code{integral3} are also provided as partial -compatibility wrappers to @code{dblquad} and @code{triplequad}: +The recursive algorithm for quadrature presented above is referred to as +@qcode{"iterated"}. A separate 2-D integration method is implemented in the +function @code{quad2d}. This function performs a @qcode{"tiled"} integration +by subdividing the integration domain into rectangular regions and performing +separate integrations over those domains. The domains are further subdivided +in areas requiring refinement to reach the desired numerical accuracy. For +certain functions this method can be faster than the 2-D iteration used in the +other functions above. + +@DOCSTRING(quad2d) + +Finally, the functions @code{integral2} and @code{integral3} are provided +as general 2-D and 3-D integration functions. They will auto-select between +iterated and tiled integration methods and, unlike @code{dblquad} and +@code{triplequad}, will work with non-rectangular integration domains. @DOCSTRING(integral2) @DOCSTRING(integral3) -The above mentioned approach works, but is fairly slow, and that problem -increases exponentially with the dimensionality of the integral. Another -possible solution is to use Orthogonal Collocation as described in the -previous section (@pxref{Orthogonal Collocation}). The integral of a function -@math{f(x,y)} for @math{x} and @math{y} between 0 and 1 can be approximated -using @math{n} points by +The above integrations can be fairly slow, and that problem increases +exponentially with the dimensionality of the integral. Another possible +solution for 2-D integration is to use Orthogonal Collocation as described in +the previous section (@pxref{Orthogonal Collocation}). The integral of a +function @math{f(x,y)} for @math{x} and @math{y} between 0 and 1 can be +approximated using @math{n} points by @tex $$ \int_0^1 \int_0^1 f(x,y) d x d y \approx \sum_{i=1}^n \sum_{j=1}^n q_i q_j f(r_i, r_j), diff -r 9fab3273ff26 -r 2e64bed0bb3a scripts/general/integral2.m --- a/scripts/general/integral2.m Sat Oct 14 18:16:55 2017 -0500 +++ b/scripts/general/integral2.m Sat Oct 14 18:30:51 2017 -0400 @@ -1,4 +1,4 @@ -## Copyright (C) 2017 Nicholas Jankowski +## Copyright (C) 2017 Nicholas Jankowski, David Bateman ## ## This file is part of Octave. ## @@ -19,14 +19,13 @@ ## -*- texinfo -*- ## @deftypefn {} {@var{q} =} integral2 (@var{f}, @var{xa}, @var{xb}, @var{ya}, @var{yb}) ## @deftypefnx {} {@var{q} =} integral2 (@var{f}, @var{xa}, @var{xb}, @var{ya}, @var{yb}, @var{prop}, @var{val}, @dots{}) +## @deftypefnx {} {[@var{q}, @var{err}] =} integral2 (@dots{}) ## ## Numerically evaluate the two-dimensional integral of @var{f} using adaptive ## quadrature over the two-dimensional domain defined by @var{xa}, @var{xb}, -## @var{ya}, @var{yb} (scalars may be finite or infinite). -## -## @code{integral2} is a wrapper for @code{dblquad} intended to provide -## @sc{matlab} compatibility. More control of the numerical integration may -## be achievable by calling the various quadrature functions directly. +## @var{ya}, @var{yb} (scalars may be finite or infinite). Additionally, +## @var{ya} and @var{yb} may be scalar functions of @var{x}, allowing for +## integration over non-rectangular domains. ## ## @var{f} is a function handle, inline function, or string containing the name ## of the function to evaluate. The function @var{f} must be of the form @@ -44,6 +43,19 @@ ## @item RelTol ## Define the relative error tolerance for the quadrature. The default ## value is 1e-6 (1e-4 for single). +## +## @item Method +## Specify the two-dimensional integration method to be used, with valid +## options being @qcode{"auto"} (default), @qcode{"tiled"}, or +## @qcode{"iterated"}. When using @qcode{"auto"}, Octave will choose the +## @qcode{"tiled"} method unless any of the integration limits are infinite. +## +## @item Vectorized +## Enable or disable vectorized integration. A value of @code{false} forces +## Octave to use only scalar inputs when calling the integrand, which enables +## integrands @math{f(x,y)} that have not been vectorized and only accept +## @var{x} and @var{y} as scalars to be used. The default value is +## @code{true}. ## @end table ## ## Adaptive quadrature is used to minimize the estimate of error until the @@ -55,59 +67,67 @@ ## ## @example ## @group -## @var{error} <= max (@var{AbsTol}, @var{RelTol}*|@var{q}|). +## @var{error} <= max (@var{AbsTol}, @var{RelTol}*|@var{q}|) ## @end group ## @end example ## ## @end ifnottex ## -## Known @sc{matlab} incompatibilities: +## @var{err} is an approximate bound on the error in the integral +## @code{abs (@var{q} - @var{I})}, where @var{I} is the exact value of the +## integral. +## +## Example 1 : integrate a rectangular region in x-y plane +## +## @example +## @group +## @var{f} = @@(@var{x},@var{y}) 2*ones (size (@var{x})); +## @var{q} = integral2 (@var{f}, 0, 1, 0, 1) +## @result{} @var{q} = 2 +## @end group +## @end example +## +## The result is a volume, which for this constant-value integrand, is just +## @code{@var{Length} * @var{Width} x @var{Height}}. +## +## Example 2 : integrate a triangular region in x-y plane ## -## @enumerate -## @item -## If tolerances are left unspecified, and any integration limits or waypoints -## are of type @code{single}, then Octave's integral functions automatically -## reduce the default absolute and relative error tolerances as specified -## above. If tighter tolerances are desired they must be specified. -## @sc{matlab} leaves the tighter tolerances appropriate for @code{double} -## inputs in place regardless of the class of the integration limits. -# -## @item -## @code{integral2} currently only integrates functions over rectangular -## domains. Implementing @var{ya} and @var{yb} as functions of @var{x} is a -## planned future improvement. +## @example +## @group +## @var{f} = @@(@var{x},@var{y}) 2*ones (size (@var{x})); +## @var{ymax} = @@(@var{x}) 1 - @var{x}; +## @var{q} = integral2 (@var{f}, 0, 1, 0, @var{ymax}) +## @result{} @var{q} = 1 +## @end group +## @end example +## +## The result is a volume, which for this constant-value integrand, is the +## Triangle Area x Height or +## @code{1/2 * @var{Base} * @var{Width} * @var{Height}}. ## -## @item -## The @qcode{"Method"} property is not yet implemented in Octave due to the -## lack of a @qcode{"tiled"} integrator implementation. All integrals are -## evaluated using an equivalent of the @qcode{"iterated"} method. -## @end enumerate +## Programming Notes: If there are singularities within the integration region +## it is best to split the integral and place the singularities on the +## boundary. +## +## Known @sc{matlab} incompatibility: If tolerances are left unspecified, and +## any integration limits are of type @code{single}, then Octave's integral +## functions automatically reduce the default absolute and relative error +## tolerances as specified above. If tighter tolerances are desired they +## must be specified. @sc{matlab} leaves the tighter tolerances appropriate +## for @code{double} inputs in place regardless of the class of the +## integration limits. ## -## @seealso{integral, integral3, quad, quadgk, quadv, quadl, quadcc, trapz, -## dblquad, triplequad} +## Reference: @nospell{L.F. Shampine}, +## @cite{@sc{matlab} program for quadrature in 2D}, Applied Mathematics and +## Computation, pp. 266--274, Vol 1, 2008. +## +## @seealso{quad2d, dblquad, integral, quad, quadgk, quadv, quadl, quadcc, +## trapz, integral3, triplequad} ## @end deftypefn -function q = integral2 (f, xa, xb, ya, yb, varargin) - ## FIXME: It is possible that a non-rectangular domain could be handled by - ## overlaying the integrand with a boolean mask function such that - ## the integration occurs over a rectangle, but regions outside the - ## desired domain contribute zero to the integral. This may be an - ## inefficient but acceptable hack to get around the rectangular - ## domain limit without having to rewrite the integrating function. +function [q, err] = integral2 (f, xa, xb, ya, yb, varargin) - ## FIXME: Implement "Method" property to let the user select between iterated - ## and tiled integration. Tiled integration follows the method of - ## Matlab's quad2d function, currently unimplemented in Octave. - ## Should probably just wait for a quad2d implementation to point the - ## integral2 wrapper to, instead of trying to re-create it here. The - ## following can be added to the help docstring once it is functional: - ## @item Method - ## Specifies the two dimensional integration method to be used, with valid - ## options being @var{"auto"}, @var{"tiled"}, or @var{"iterated"}. - ## @code{integral} will use @var{"auto"} by default, where it will usually - ## choose @var{"tiled"} unless any of the integration limits are infinite. - - if (nargin < 5 || (mod (nargin, 2) == 0)) + if (nargin < 5 || mod (nargin, 2) == 0) print_usage (); endif @@ -115,12 +135,14 @@ print_usage (); endif - if (! (isscalar (xa) && isscalar (xb) && isscalar (ya)) && isscalar (yb)) + if (! (isreal (xa) && isscalar (xa) && isreal (xb) && isscalar (xb))) print_usage (); endif ## Check for single or double limits to set appropriate default tolerance. - issingle = isa ([xa, xb, ya, yb], "single"); + issingle = (isa ([xa, xb], "single") + || (! is_function_handle (ya) && isa (ya, "single")) + || (! is_function_handle (yb) && isa (yb, "single"))); ## Set defaults, update with any specified parameters. if (issingle) @@ -131,91 +153,186 @@ reltol = 1e-6; endif - if (nargin == 5) - ## Pass the simplest case directly to integrator. - ## Let quadcc function handle input checks. - q = dblquad (f, xa, xb, ya, yb, [abstol, reltol], @quadcc); + method = "auto"; + idx = 1; + while (idx < nargin - 5) + prop = varargin{idx++}; + if (! ischar (prop)) + error ("integral2: property PROP must be a string"); + endif + + switch (tolower (prop)) + case "abstol" + abstol = varargin{idx++}; + if (! (isnumeric (abstol) && isscalar (abstol) && abstol >= 0)) + error ("integral2: AbsTol value must be a numeric scalar >= 0"); + endif - else - ## Parse options to determine how to call integrator. - intmethod = []; + case "reltol" + reltol = varargin{idx++}; + if (! (isnumeric (reltol) && isscalar (reltol) && reltol >= 0)) + error ("integral2: RelTol value must be a numeric scalar >= 0"); + endif - idx = 1; - while (idx < nargin - 5) - prop = varargin{idx++}; - if (! ischar (prop)) - error ("integral2: property PROP must be a string"); - endif + case "method" + method = tolower (varargin{idx++}); + if (! any (strcmp (method, {"auto", "iterated", "tiled"}))) + error ("integral2 : unrecognized method '%s'", method); + endif + + case "vectorized" + vectorized = varargin{idx++}; + if (! (isscalar (vectorized) && isreal (vectorized))) + error ('integral2: Vectorized must be a logical value'); + endif + if (! vectorized) + f = @(x, y) arrayfun (f, x, y); + endif - switch (tolower (prop)) - case "abstol" - abstol = varargin{idx++}; - if (! (isnumeric (abstol) && isscalar (abstol) && abstol >= 0)) - error ("integral2: AbsTol value must be a numeric scalar >= 0"); - endif + otherwise + error ("integral2: unknown property '%s'", prop); + + endswitch + endwhile + + if (strcmp (method, "auto")) + if (isinf (xa) || isinf (xb) + || (! is_function_handle (ya) && isinf (ya)) + || (! is_function_handle (yb) && isinf (yb))) + method = "iterated"; + else + method = "tiled"; + endif + endif - case "reltol" - reltol = varargin{idx++}; - if (! (isnumeric (reltol) && isscalar (reltol) && reltol >= 0)) - error ("integral2: RelTol value must be a numeric scalar >= 0"); - endif + ## check upper and lower bounds of y + if (! is_function_handle (ya)) + if (! (isreal (ya) && isscalar (ya))) + error ("integral2: YA must be a real scalar or a function"); + endif + ya = @(x) ya * ones (rows (x), columns (x)); + endif + if (! is_function_handle (yb)) + if (! (isreal (yb) && isscalar (yb))) + error ("integral2: YB must be a real scalar or a function"); + endif + yb = @(x) yb * ones (rows (x), columns (x)); + endif - case "method" - intmethod = varargin{idx++}; - warning (["integral2: Only 'iterated' method implemented. ", ... - "Method property ignored."]); - otherwise - error ("integral2: unknown property '%s'", prop); - endswitch - endwhile + if (strcmp (method, "iterated")) + q = outer_iterated (f, xa, xb, ya, yb, abstol, reltol); - q = dblquad (f, xa, xb, ya, yb, [abstol, reltol], @quadcc); - + if (nargout == 2) + warning ('integral2: "iterated" method can not return estimated error'); + err = 0; + endif + else + [q, err] = quad2d (f, xa, xb, ya, yb, "AbsTol", abstol, "RelTol", reltol); endif endfunction +function q = outer_iterated (f, xa, xb, ya, yb, abstol, reltol) + finner_iter = @(x) inner_iterated (x, f, ya, yb, abstol, reltol); + q = quadcc (finner_iter, xa, xb, [abstol, reltol]); +endfunction -%!test +function q = inner_iterated (x, f, ya, yb, abstol, reltol) + q = zeros (size (x)); + for i = 1 : length (x) + q(i) = quadcc (@(y) f(x(i), y), ya(x(i)), yb(x(i)), [abstol, reltol]); + endfor +endfunction + + +## method tests +%!shared f %! f = @(x, y) x .* y; -%! assert (integral2 (f, 0, 1, 0, 1), 0.25, 1e-10); +%!assert (integral2 (f, 0, 1, 0, 1), 0.25, 1e-10) +%!assert (integral2 (f, 0, 1, 0, 1, "method", "tiled"), 0.25, 1e-10) +%!assert (integral2 (f, 0, 1, 0, 1, "method", "iterated"), 0.25, 1e-10) +%!assert (integral2 (f, 0, 1, 0, 1, "method", "auto"), 0.25, 1e-10) + +## vectorized = false test %!test +%! f = @(x, y) x * y; +%! assert (integral2 (f, 0, 1, 0, 1, "vectorized", false), 0.25, 1e-10); + +## tolerance tests +%!shared f %! f = @(x, y) 9 * x.^2 + 15 * y.^2; -%! assert (integral2 (f, 0, 5, -5, 0, "AbsTol", 1e-9), 5000, 1e-9); -%! assert (integral2 (f, 0, 5, -5, 0, "RelTol", 1e-6), 5000, -1e-6); -%! assert (integral2 (f, 0, 5, -5, 0, "RelTol", 1e-6, "AbsTol", 1e-9), -%! 5000, 1e-9); + +%!assert (integral2 (f, 0, 5, -5, 0, "AbsTol", 1e-9), 5000, 1e-9) +%!assert (integral2 (f, 0, 5, -5, 0, "RelTol", 1e-5), 5000, -1e-5) +%!assert (integral2 (f, 0, 5, -5, 0, "RelTol", 1e-6, "AbsTol", 1e-9), +%! 5000, 1e-9) ## tests from dblquad %!assert (integral2 (@(x, y) 1 ./ (x+y), 0, 1, 0, 1, "AbsTol", 1e-7), %! 2*log (2), 1e-7) -%!assert (integral2 (@(x, y) 1 ./ (x+y), 0, 1, 0, 1, "RelTol", 1e-6), +%!assert (integral2 (@(x, y) 1 ./ (x+y), 0, 1, 0, 1, "RelTol", 1e-5), +%! 2*log (2), -1e-5) +%!assert (integral2 (@(x, y) 1 ./ (x+y), 0, 1, 0, 1, "AbsTol", 1e-8, +%! "RelTol", 1e-6), %! 2*log (2), -1e-6) -%!assert (integral2 (@(x, y) 1 ./ (x+y), 0, 1, 0, 1, "AbsTol", 1e-8, -%! "RelTol", 1e-6), 2*log (2), -1e-6) %!assert (integral2 (@(x, y) exp (-x.^2 - y.^2) , -1, 1, -1, 1), %! pi * erf (1).^2, 1e-10) %!assert (integral2 (@plus, 1, 2, 3, 4), 5, 1e-10) +%!assert (integral2 (@(x,y) 1 ./ (x + y), 0, 1, 0, @(x) 1 - x), 1, -1e-6) + +## tests from dblquad w/method specified +%!assert (integral2 (@(x, y) 1 ./ (x+y), 0, 1, 0, 1, +%! "AbsTol", 1e-7, "method", "iterated"), +%! 2*log (2), 1e-7) +%!assert (integral2 (@(x, y) 1 ./ (x+y), 0, 1, 0, 1, +%! "RelTol", 1e-5, "method", "iterated"), +%! 2*log (2), -1e-5) +%!assert (integral2 (@(x, y) 1 ./ (x+y), 0, 1, 0, 1, +%! "AbsTol", 1e-8, "RelTol", 1e-6, "method", "iterated"), +%! 2*log (2), -1e-6) +%!assert (integral2 (@(x, y) exp (-x.^2 - y.^2) , -1, 1, -1, 1, +%! "method", "iterated"), +%! pi * erf (1).^2, 1e-10) + +%!assert (integral2 (@plus, 1, 2, 3, 4, "method", "iterated"), 5, 1e-10) +%!assert (integral2 (@(x,y) 1 ./ (x + y), 0, 1, 0, @(x) 1 - x, +%! "method", "iterated"), +%! 1, -1e-6) ## Test input validation -%!error integral2 -%!error integral2 (0, 1 ,2 ,3 ,4) +%!error integral2 () %!error integral2 (@plus) %!error integral2 (@plus, 1) %!error integral2 (@plus, 1, 2) %!error integral2 (@plus, 1, 2, 3) -%!error integral2 (@plus, 1, 2, 3, [4 5]) -%!error integral2 (@plus, 1, 2, 3, "test") %!error integral2 (@plus, 1, 2, 3, 4, "foo") +%!error integral2 (0, 1, 2, 3, 4) # f must be function handle +%!error integral2 (@plus, 1i, 2, 3, 4) # real limits +%!error integral2 (@plus, 1, 2i, 3, 4) # real limits +%!error integral2 (@plus, [1 1], 2, 3, 4) # scalar limits +%!error integral2 (@plus, 1, [2 2], 3, 4) # scalar limits +%!error integral2 (@plus,1,2,3,4,99, "bar") +%!error +%! integral2 (@plus,1,2,3,4, "AbsTol", "foo"); +%!error +%! integral2 (@plus, 1, 2, 3, 4, "AbsTol", [1, 2]); +%!error integral2 (@plus,1,2,3,4, "AbsTol", -1) +%!error +%! integral2 (@plus, 1, 2, 3, 4, "RelTol", "foo"); +%!error +%! integral2 (@plus, 1, 2, 3, 4, "RelTol", [1, 2]); +%!error integral2 (@plus,1,2,3,4, "RelTol", -1) +%!error integral2 (@plus,1,2,3,4, "method", "foo") +%!error +%! integral2 (@plus,1,2,3,4, "Vectorized", [0 1]); +%!error +%! integral2 (@plus,1,2,3,4, "Vectorized", {true}); %!error integral2 (@plus, 1, 2, 3, 4, "foo", "bar") -%!error integral2 (@plus, 1, 2, 3, 4, 99, "bar") -%!error integral2 (@plus, 1, 2, 3, 4, "AbsTol", "foo") -%!error integral2 (@plus, 1, 2, 3, 4, "AbsTol", [1, 2]) -%!error integral2 (@plus, 1, 2, 3, 4, "AbsTol", -1) -%!error integral2 (@plus, 1, 2, 3, 4, "RelTol", "foo") -%!error integral2 (@plus, 1, 2, 3, 4, "RelTol", [1, 2]) -%!error integral2 (@plus, 1, 2, 3, 4, "RelTol", -1) -%!warning -%! q = integral2 (@plus, 0, 1, 0, 1, "Method", "tiled"); +%!error integral2 (@plus, 1, 2, 3i, 4) +%!error integral2 (@plus, 1, 2, [3 3], 4) +%!error integral2 (@plus, 1, 2, 3, 4i) +%!error integral2 (@plus, 1, 2, 3, [4 4]) +%!warning <"iterated" method can not return estimated error> +%! [q, err] = integral2 (@plus, 0, 0, 0, 0, "method", "iterated"); diff -r 9fab3273ff26 -r 2e64bed0bb3a scripts/general/integral3.m --- a/scripts/general/integral3.m Sat Oct 14 18:16:55 2017 -0500 +++ b/scripts/general/integral3.m Sat Oct 14 18:30:51 2017 -0400 @@ -1,4 +1,4 @@ -## Copyright (C) 2017 Nicholas Jankowski +## Copyright (C) 2017 David Bateman ## ## This file is part of Octave. ## @@ -23,17 +23,15 @@ ## Numerically evaluate the three-dimensional integral of @var{f} using ## adaptive quadrature over the three-dimensional domain defined by ## @var{xa}, @var{xb}, @var{ya}, @var{yb}, @var{za}, @var{zb} (scalars may -## be finite or infinite). -## -## @code{integral3} is a wrapper for @code{triplequad} intended to provide -## @sc{matlab} compatibility. More control of the numerical integration may be -## achievable by calling the various quadrature functions directly. +## be finite or infinite). Additionally, @var{ya} and @var{yb} may be +## scalar functions of @var{x} and @var{za}, and @var{zb} maybe be scalar +## functions of @var{x} and @var{y}, allowing for integration over +## non-rectangular domains. ## ## @var{f} is a function handle, inline function, or string containing the name ## of the function to evaluate. The function @var{f} must be of the form -## @math{w = f(x,y,z)} where either @var{x} or @var{y} is a vector and the -## remaining inputs are scalars. @var{f} should return a vector of the same -## length and orientation as the vector input @var{x} or @var{y}. +## @math{z = f(x,y)} where @var{x} is a vector and @var{y} is a scalar. It +## should return a vector of the same length and orientation as @var{x}. ## ## Additional optional parameters can be specified using ## @qcode{"@var{property}", @var{value}} pairs. Valid properties are: @@ -46,6 +44,19 @@ ## @item RelTol ## Define the relative error tolerance for the quadrature. The default ## value is 1e-6 (1e-4 for single). +## +## @item Method +## Specify the two-dimensional integration method to be used, with valid +## options being @qcode{"auto"} (default), @qcode{"tiled"}, or +## @qcode{"iterated"}. When using @qcode{"auto"}, Octave will choose the +## @qcode{"tiled"} method unless any of the integration limits are infinite. +## +## @item Vectorized +## Enable or disable vectorized integration. A value of @code{false} forces +## Octave to use only scalar inputs when calling the integrand, which enables +## integrands @math{f(x,y)} that have not been vectorized and only accept +## @var{x} and @var{y} as scalars to be used. The default value is +## @code{true}. ## @end table ## ## Adaptive quadrature is used to minimize the estimate of error until the @@ -57,61 +68,67 @@ ## ## @example ## @group -## @var{error} <= max (@var{AbsTol}, @var{RelTol}*|@var{q}|). +## @var{error} <= max (@var{AbsTol}, @var{RelTol}*|@var{q}|) ## @end group ## @end example ## ## @end ifnottex ## -## Known @sc{matlab} incompatibilities: +## @var{err} is an approximate bound on the error in the integral +## @code{abs (@var{q} - @var{I})}, where @var{I} is the exact value of the +## integral. +## +## Example 1 : integrate over a rectangular volume ## -## @enumerate -## @item -## If tolerances are left unspecified, and any integration limits or waypoints -## are of type @code{single}, then Octave's integral functions automatically -## reduce the default absolute and relative error tolerances as specified -## above. If tighter tolerances are desired they must be specified. -## @sc{matlab} leaves the tighter tolerances appropriate for @code{double} -## inputs in place regardless of the class of the integration limits. +## @example +## @group +## @var{f} = @@(@var{x},@var{y},@var{z}) ones (size (@var{x})); +## @var{q} = integral3 (@var{f}, 0, 1, 0, 1, 0, 1) +## @result{} @var{q} = 1 +## @end group +## @end example +## +## For this constant-value integrand, the result is a volume which is just +## @code{@var{Length} * @var{Width} x @var{Height}}. +## +## Example 2 : integrate over a spherical volume ## -## @item -## @code{integral3} currently only integrates functions over rectangular -## volumes. Implementing @var{ya} and @var{yb} as functions of @var{x}, and -## @var{za} and @var{zb} as functions of (@var{x,y}) is a planned future -## improvement. +## @example +## @group +## @var{f} = @@(@var{x},@var{y}) ones (size (@var{x})); +## @var{ymax} = @@(@var{x}) sqrt (1 - @var{x}.^2); +## @var{zmax} = @@(@var{x}) sqrt (1 - @var{x}.^2 - @var{y}.^2); +## @var{q} = integral3 (@var{f}, 0, 1, 0, @var{ymax}) +## @result{} @var{q} = 0.52360 +## @end group +## @end example +## +## For this constant-value integrand, the result is a volume which is 1/8th +## of a unit sphere or @code{1/8 * 4/3 * pi}. ## -## @item -## The @qcode{"Method"} property is not yet implemented in Octave due to the -## lack of a @qcode{"tiled"} integrator implementation. All integrals are -## evaluated using an equivalent of the @qcode{"iterated"} method. -## @end enumerate +## Programming Notes: If there are singularities within the integration region +## it is best to split the integral and place the singularities on the +## boundary. ## -## @seealso{integral, integral2, quad, quadgk, quadv, quadl, quadcc, trapz, -## dblquad, triplequad} +## Known @sc{matlab} incompatibility: If tolerances are left unspecified, and +## any integration limits are of type @code{single}, then Octave's integral +## functions automatically reduce the default absolute and relative error +## tolerances as specified above. If tighter tolerances are desired they +## must be specified. @sc{matlab} leaves the tighter tolerances appropriate +## for @code{double} inputs in place regardless of the class of the +## integration limits. +## +## Reference: @nospell{L.F. Shampine}, +## @cite{@sc{matlab} program for quadrature in 2D}, Applied Mathematics and +## Computation, pp. 266--274, Vol 1, 2008. +## +## @seealso{triplequad, integral, quad, quadgk, quadv, quadl, +## quadcc, trapz, integral2, quad2d, dblquad} ## @end deftypefn function q = integral3 (f, xa, xb, ya, yb, za, zb, varargin) - ## FIXME: it is possible that a non-rectangular domain could be handled by - ## overlaying the integrand with a boolean mask function such that - ## the integration occurs over a rectangle but regions outside the - ## desired domain contribute zero to the integral. This may be an - ## inefficient but acceptable hack to get around the rectangular domain - ## limit without having to rewrite the integrating function. - ## FIXME: implement 'method' property to let the user select between iterated - ## and tiled integration. Tiled integration follows the method of - ## matlab's quad2d function, currently unimplemented in Octave. Should - ## probably just wait for a quad2d implementation to point the - ## integral3 wrapper to instead of trying to recreate it here. The - ## following can be added to the help docstring once it is functional: - ## @item Method - ## Specifies the underlying 2D integration method to be used on the y and z - ## dimensions, with valid options being @var{"auto"}, @var{"tiled"}, or - ## @var{"iterated"}. @code{integral3} will use @var{"auto"} by default, where - ## it will usually choose @var{"tiled"} unless any of the integration limits - ## are infinite. - - if (nargin < 7 || (mod (nargin, 2) == 0)) + if (nargin < 7 || mod (nargin, 2) == 0) print_usage (); endif @@ -119,14 +136,16 @@ print_usage (); endif - if (! (isscalar (xa) && isscalar (xb) - && isscalar (ya) && isscalar (yb) - && isscalar (za) && isscalar (zb))) + if (! (isreal (xa) && isscalar (xa) && isreal (xb) && isscalar (xb))) print_usage (); endif ## Check for single or double limits to set appropriate default tolerance. - issingle = isa ([xa, xb, ya, yb, za, zb], "single"); + issingle = (isa ([xa, xb], "single") + || (! is_function_handle (ya) && isa (ya, "single")) + || (! is_function_handle (yb) && isa (yb, "single")) + || (! is_function_handle (za) && isa (za, "single")) + || (! is_function_handle (zb) && isa (zb, "single"))); ## Set defaults, update with any specified parameters. if (issingle) @@ -137,81 +156,187 @@ reltol = 1e-6; endif - if (nargin == 7) - ## Pass the simplest case directly to integrator. - ## Let quadcc function handle input checks. - q = triplequad (f, xa, xb, ya, yb, za, zb, [abstol, reltol], @quadcc); + method = "auto"; + vectorized = true; + idx = 1; + while (idx < nargin - 7) + prop = varargin{idx++}; + if (! ischar (prop)) + error ("integral3: property PROP must be a string"); + endif + + switch (tolower (prop)) + case "abstol" + abstol = varargin{idx++}; + if (! (isnumeric (abstol) && isscalar (abstol) && abstol >= 0)) + error ("integral3: AbsTol value must be a numeric scalar >= 0"); + endif - else - ## Parse options to determine how to call integrator. - intmethod = []; + case "reltol" + reltol = varargin{idx++}; + if (! (isnumeric (reltol) && isscalar (reltol) && reltol >= 0)) + error ("integral3: RelTol value must be a numeric scalar >= 0"); + endif + + case "method" + method = tolower (varargin{idx++}); + if (! any (strcmp (method, {"auto", "iterated", "tiled"}))) + error ("integral3 : unrecognized method '%s'", method); + endif - idx = 1; - while (idx < nargin - 7) - prop = varargin{idx++}; - if (! ischar (prop)) - error ("integral3: property PROP must be a string"); - endif + case "vectorized" + vectorized = varargin{idx++}; + if (! (isscalar (vectorized) && isreal (vectorized))) + error ('integral3: Vectorized must be a logical value'); + endif + + otherwise + error ("integral3: unknown property '%s'", prop); + + endswitch + endwhile - switch (tolower (prop)) - case "abstol" - abstol = varargin{idx++}; - if (! (isnumeric (abstol) && isscalar (abstol) && abstol >= 0)) - error ("integral3: AbsTol value must be a numeric scalar >= 0"); - endif - - case "reltol" - reltol = varargin{idx++}; - if (! (isnumeric (reltol) && isscalar (reltol) && reltol >= 0)) - error ("integral2: RelTol value must be a numeric scalar >= 0"); - endif + if (strcmp (method, "auto")) + if (isinf (xa) || isinf (xb) + || (! is_function_handle (ya) && isinf (ya)) + || (! is_function_handle (yb) && isinf (yb)) + || (! is_function_handle (za) && isinf (za)) + || (! is_function_handle (zb) && isinf (zb))) + method = "iterated"; + else + method = "tiled"; + endif + endif - case "method" - intmethod = varargin{idx++}; - warning (["integral3: Only 'iterated' method implemented. ", ... - "Method property ignored."]); - otherwise - error ("integral3: unknown property '%s'", prop); - endswitch - endwhile + ## check upper and lower bounds of y + if (! is_function_handle (ya)) + if (! (isreal (ya) && isscalar (ya))) + error ("integral3: YA must be a real scalar or a function"); + endif + ya = @(x) ya * ones (size (x)); + endif + if (! is_function_handle (yb)) + if (! (isreal (yb) && isscalar (yb))) + error ("integral3: YB must be a real scalar or a function"); + endif + yb = @(x) yb * ones (size (x)); + endif - q = triplequad (f, xa, xb, ya, yb, za, zb, [abstol, reltol], @quadcc); + ## check upper and lower bounds of z + if (! is_function_handle (za)) + if (! (isreal (za) && isscalar (za))) + error ("integral3: ZA must be a real scalar or a function"); + endif + za = @(x, y) za * ones (size(y)); + endif + if (! is_function_handle (zb)) + if (! (isreal (zb) && isscalar (zb))) + error ("integral3: ZB must be a real scalar or a function") + endif + zb = @(x, y) zb * ones (size (y)); + endif - endif + finner = @(x) inner (x, f, ya, yb, za, zb, vectorized, method, abstol, reltol); + q = quadcc (finner, xa, xb, [abstol, reltol]); endfunction +function q = inner (x, f, ya, yb, za, zb, vectorized, method, abstol, reltol) + q = zeros (size (x)); + for i = 1 : length (x) + za2 = @(y) za(x(i), y); + zb2 = @(y) zb(x(i), y); + f2 = @(y, z) f(x(i), y, z); + if (! vectorized) + f2 = @(y, z) arrayfun (f2, y, z); + endif + if (strcmp (method, "iterated")) + finner_iter = @(y) inner_iterated (y, f2, za2, zb2, abstol, reltol); + q(i) = quadcc (finner_iter, ya(x(i)), yb(x(i)), [abstol, reltol]); + else + q(i) = quad2d (f2, ya(x(i)), yb(x(i)), za2, zb2, + "AbsTol", abstol, "RelTol", reltol); + endif + endfor +endfunction -%!test -%! f = @(x, y, z) x.*y.*z; -%! assert (integral3 (f, 0, 1, 0, 1, 0, 1), 0.125, 1e-10); +function q = inner_iterated (y, f2, za2, zb2, abstol, reltol) + q = zeros (size (y)); + for i = 1 : length (y) + q(i) = quadcc (@(z) f2(y(i), z), za2(y(i)), zb2(y(i)), [abstol, reltol]); + endfor +endfunction + +## method tests +%!shared f +%! f = @(x, y, z) x .* y .* z; + +%!assert (integral3 (f, 0, 1, 0, 1, 0, 1), 0.125, 1e-10); +%!assert (integral3 (f, 0, 1, 0, 1, 0, 1, "method", "tiled"), 0.125, 1e-10); +%!assert (integral3 (f, 0, 1, 0, 1, 0, 1, "method", "iterated"), 0.125, 1e-10); +%!assert (integral3 (f, 0, 1, 0, 1, 0, 1, "method", "auto"), 0.125, 1e-10); + +## vectorized = false test %!test -%! f = @(x,y,z) y.*sin(x) + z.*cos(x); -%! assert (integral3 (f, 0, pi, 0, 1, -1, 1), 2, 1e-10); +%! f = @(x, y, z) x * y * z; +%! assert (integral3 (f, 0, 1, 0, 1, 0, 1, "vectorized", false), 0.125, 1e-10); + +## tolerance tests +%!shared f +%! f = @(x, y, z) 2 * x.^2 + 3 * y.^2 + 4 * z.^2; -## tests from triplequad -%! assert (integral3 (@(x,y,z) exp (-x.^2 - y.^2 - z.^2) , -1, 1, -1, 1, -1, -%! 1), pi^(3/2) * erf (1).^3, 1e-10); +%!assert (integral3 (f, 0, 5, -5, 0, 0, 5, "AbsTol", 1e-9), 9375, 1e-9) +%!assert (integral3 (f, 0, 5, -5, 0, 0, 5, "RelTol", 1e-5), 9375, -1e-5) +%!assert (integral3 (f, 0, 5, -5, 0, 0, 5, "RelTol", 1e-6, "AbsTol", 1e-9), +%! 9375, 1e-9) + +## non-rectangular region +## This test is too slow with "iterated" method +%!assert (integral3 (@(x,y,z) 1 ./ (x + y + z), 0, 1, 0, @(x) 1 - x, 0, +%! @(x, y) 1 - x - y, "method", "tiled"), 0.25, 1e-6) ## Test input validation -%!error integral3 (0, 1 ,2 ,3 ,4, 5, 6) +%!error integral3 %!error integral3 (@plus) %!error integral3 (@plus, 1) %!error integral3 (@plus, 1, 2) %!error integral3 (@plus, 1, 2, 3) %!error integral3 (@plus, 1, 2, 3, 4) %!error integral3 (@plus, 1, 2, 3, 4, 5) -%!error integral3 (@plus, 1, 2, 3, 4, 5, [6 7]) -%!error integral3 (@plus, 1, 2, 3, 4, 5, "test") %!error integral3 (@plus, 1, 2, 3, 4, 5, 6, "foo") -%!error integral3 (@plus, 1, 2, 3, 4, 5, 6, "foo", "bar") -%!error integral3 (@plus, 1, 2, 3, 4, 5, 6, NA, "bar") -%!error integral3 (@plus, 1, 2, 3, 4, 5, 6, "AbsTol", "foo") -%!error integral3 (@plus, 1, 2, 3, 4, 5, 6, "AbsTol", [1, 2]) -%!error integral3 (@plus, 1, 2, 3, 4, 5, 6, "AbsTol", -1) -%!error integral3 (@plus, 1, 2, 3, 4, 5, 6, "RelTol", "foo") -%!error integral3 (@plus, 1, 2, 3, 4, 5, 6, "RelTol", [1, 2]) -%!error integral3 (@plus, 1, 2, 3, 4, 5, 6, "RelTol", -1) -%!warning -%! q = integral3 (@plus, 0, 1, 0, 1, 0, 1, "Method", "tiled"); +%!error integral3 (0, 1, 2, 3, 4, 5, 6) # f must be a function handle +%!error integral3 (@plus, 1i, 2, 3, 4, 5, 6) # real limits +%!error integral3 (@plus, 1, 2i, 3, 4, 5, 6) # real limits +%!error integral3 (@plus, [1 1], 2, 3, 4, 5, 6) # scalar limits +%!error integral3 (@plus, 1, [2 2], 3, 4, 5, 6) # scalar limits +%!error +%! integral3 (@plus, 1, 2, 3, 4, 5, 6, 99, "bar"); +%!error +%! integral3 (@plus, 1, 2, 3, 4, 5, 6, "AbsTol", "foo"); +%!error +%! integral3 (@plus, 1, 2, 3, 4, 5, 6, "AbsTol", [1, 2]); +%!error +%! integral3 (@plus, 1, 2, 3, 4, 5, 6, "AbsTol", -1); +%!error +%! integral3 (@plus, 1, 2, 3, 4, 5, 6, "RelTol", "foo"); +%!error +%! integral3 (@plus, 1, 2, 3, 4, 5, 6, "RelTol", [1, 2]); +%!error +%! integral3 (@plus, 1, 2, 3, 4, 5, 6, "RelTol", -1); +%!error +%! integral3 (@plus,1,2,3,4,5,6, "method", "foo"); +%!error +%! integral3 (@plus,1,2,3,4,5,6, "Vectorized", [0 1]); +%!error +%! integral3 (@plus,1,2,3,4,5,6, "Vectorized", {true}); +%!error +%! integral3 (@plus, 1, 2, 3, 4, 6, 6, "foo", "bar"); +%!error integral3 (@plus, 1, 2, 3i, 4, 5, 6) +%!error integral3 (@plus, 1, 2, [3 3], 4, 5, 6) +%!error integral3 (@plus, 1, 2, 3, 4i, 5, 6) +%!error integral3 (@plus, 1, 2, 3, [4 4], 5, 6) +%!error integral3 (@plus, 1, 2, 3, 4, 5i, 6) +%!error integral3 (@plus, 1, 2, 3, 4, [5 5], 6) +%!error integral3 (@plus, 1, 2, 3, 4, 5, 6i) +%!error integral3 (@plus, 1, 2, 3, 4, 5, [6 6]) diff -r 9fab3273ff26 -r 2e64bed0bb3a scripts/general/module.mk --- a/scripts/general/module.mk Sat Oct 14 18:16:55 2017 -0500 +++ b/scripts/general/module.mk Sat Oct 14 18:30:51 2017 -0400 @@ -66,6 +66,7 @@ %reldir%/postpad.m \ %reldir%/prepad.m \ %reldir%/publish.m \ + %reldir%/quad2d.m \ %reldir%/quadgk.m \ %reldir%/quadl.m \ %reldir%/quadv.m \ diff -r 9fab3273ff26 -r 2e64bed0bb3a scripts/general/quad2d.m --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/scripts/general/quad2d.m Sat Oct 14 18:30:51 2017 -0400 @@ -0,0 +1,476 @@ +## Copyright (C) 2017 David Bateman +## +## This file is part of Octave. +## +## Octave is free software; you can redistribute it and/or modify it +## under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 3 of the License, or +## (at your option) any later version. +## +## Octave is distributed in the hope that it will be useful, but +## WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with Octave; see the file COPYING. If not, see +## . + +## -*- texinfo -*- +## @deftypefn {} {@var{q} =} quad2d (@var{f}, @var{xa}, @var{xb}, @var{ya}, @var{yb}) +## @deftypefnx {} {@var{q} =} quad2d (@var{f}, @var{xa}, @var{xb}, @var{ya}, @var{yb}, @var{prop}, @var{val}, @dots{}) +## @deftypefnx {} {[@var{q}, @var{err}, @var{iter}] =} quad2d (@dots{}) +## +## Numerically evaluate the two-dimensional integral of @var{f} using adaptive +## quadrature over the two-dimensional domain defined by @var{xa}, @var{xb}, +## @var{ya}, @var{yb} using tiled integration. Additionally, @var{ya} and +## @var{yb} may be scalar functions of @var{x}, allowing for the integration +## over non-rectangular domains. +## +## @var{f} is a function handle, inline function, or string containing the name +## of the function to evaluate. The function @var{f} must be of the form +## @math{z = f(x,y)} where @var{x} is a vector and @var{y} is a scalar. It +## should return a vector of the same length and orientation as @var{x}. +## +## Additional optional parameters can be specified using +## @qcode{"@var{property}", @var{value}} pairs. Valid properties are: +## +## @table @code +## @item AbsTol +## Define the absolute error tolerance for the quadrature. The default +## value is 1e-10 (1e-5 for single). +## +## @item RelTol +## Define the relative error tolerance for the quadrature. The default +## value is 1e-6 (1e-4 for single). +## +## @item MaxFunEvals +## The maximum number of function calls to the vectorized function @var{f}. +## The default value is 5000. +## +## @item Singular +## Enable/disable transforms to weaken singularities on the edge of the +## integration domain. The default value is @var{true}. +## +## @item Vectorized +## Option to disable vectorized integration, forcing Octave to use only scalar +## inputs when calling the integrand. The default value is @var{false}. +## +## @item FailurePlot +## If @code{quad2d} fails to converge to the desired error tolerance before +## MaxFunEvals is reached, a plot of the areas that still need refinement +## is created. The default value is @var{false}. +## @end table +## +## Adaptive quadrature is used to minimize the estimate of error until the +## following is satisfied: +## @tex +## $$error \leq \max \left( AbsTol, RelTol\cdot\vert q\vert \right)$$ +## @end tex +## @ifnottex +## +## @example +## @group +## @var{error} <= max (@var{AbsTol}, @var{RelTol}*|@var{q}|) +## @end group +## @end example +## +## @end ifnottex +## +## The optional output @var{err} is an approximate bound on the error in the +## integral @code{abs (@var{q} - @var{I})}, where @var{I} is the exact value +## of the integral. The optional output @var{iter} is the number of vectorized +## function calls to the function @var{f} that were used. +## +## Example 1 : integrate a rectangular region in x-y plane +## +## @example +## @group +## @var{f} = @@(@var{x},@var{y}) 2*ones (size (@var{x})); +## @var{q} = quad2d (@var{f}, 0, 1, 0, 1) +## @result{} @var{q} = 2 +## @end group +## @end example +## +## The result is a volume, which for this constant-value integrand, is just +## @code{@var{Length} * @var{Width} x @var{Height}}. +## +## Example 2 : integrate a triangular region in x-y plane +## +## @example +## @group +## @var{f} = @@(@var{x},@var{y}) 2*ones (size (@var{x})); +## @var{ymax} = @@(@var{x}) 1 - @var{x}; +## @var{q} = quad2d (@var{f}, 0, 1, 0, @var{ymax}) +## @result{} @var{q} = 1 +## @end group +## @end example +## +## The result is a volume, which for this constant-value integrand, is the +## Triangle Area x Height or +## @code{1/2 * @var{Base} * @var{Width} * @var{Height}}. +## +## Programming Notes: If there are singularities within the integration region +## it is best to split the integral and place the singularities on the +## boundary. +## +## Known @sc{matlab} incompatibility: If tolerances are left unspecified, and +## any integration limits are of type @code{single}, then Octave's integral +## functions automatically reduce the default absolute and relative error +## tolerances as specified above. If tighter tolerances are desired they +## must be specified. @sc{matlab} leaves the tighter tolerances appropriate +## for @code{double} inputs in place regardless of the class of the +## integration limits. +## +## Reference: @nospell{L.F. Shampine}, +## @cite{@sc{matlab} program for quadrature in 2D}, Applied Mathematics and +## Computation, pp. 266--274, Vol 1, 2008. +## +## @seealso{integral2, dblquad, integral, quad, quadgk, quadv, quadl, quadcc, +## trapz, integral3, triplequad} +## @end deftypefn + +function [q, err, iter] = quad2d (f, xa, xb, ya, yb, varargin) + + if (nargin < 5 || mod (nargin, 2) == 0) + print_usage (); + endif + + if (ischar (f)) + ## Convert function given as a string to a function handle + f = @(x) feval (f, x); + elseif (! is_function_handle (f)) + print_usage (); + endif + + if (! (isreal (xa) && isscalar (xa) && isreal (xb) && isscalar (xb))) + print_usage (); + endif + + ## Check for single or double limits to set appropriate default tolerance. + issingle = (isa ([xa, xb], "single") + || (! is_function_handle (ya) && isa (ya, "single")) + || (! is_function_handle (yb) && isa (yb, "single"))); + + ## Set defaults, update with any specified parameters. + if (issingle) + abstol = 1e-5; + reltol = 1e-4; + else + abstol = 1e-10; + reltol = 1e-6; + endif + + vectorized = true; + singular = true; + idx = 1; + maxiter = 5000; + failureplot = false; + + while (idx < nargin - 5) + prop = varargin{idx++}; + if (! ischar (prop)) + error ("quad2d: property PROP must be a string"); + endif + + switch (tolower (prop)) + case "abstol" + abstol = varargin{idx++}; + if (! (isnumeric (abstol) && isscalar (abstol) && abstol >= 0)) + error ("quad2d: AbsTol value must be a numeric scalar >= 0"); + endif + + case "reltol" + reltol = varargin{idx++}; + if (! (isnumeric (reltol) && isscalar (reltol) && reltol >= 0)) + error ("quad2d: RelTol value must be a numeric scalar >= 0"); + endif + + case "maxfunevals" + maxiter = varargin{idx++}; + if (! (isnumeric (maxiter) && isscalar (maxiter) + && fix (maxiter) == maxiter && maxiter >= 1)) + error ("quad2d: MaxFunEvals value must be a scalar integer >= 1"); + endif + + case "singular" + singular = varargin{idx++}; + if (! (isscalar (singular) && isreal (singular))) + error ("quad2d: Singular must be a logical value"); + endif + + case "vectorized" + vectorized = varargin{idx++}; + if (! (isscalar (vectorized) && isreal (vectorized))) + error ("quad2d: Vectorized must be a logical value"); + endif + + case "failureplot" + failureplot = varargin{idx++}; + if (! (isscalar (failureplot) && isreal (failureplot))) + error ("quad2d: FailurePlot must be a logical value"); + endif + + otherwise + error ("quad2d: unknown property '%s'", prop); + + endswitch + endwhile + + if (! vectorized) + f = @(x, y) arrayfun (f, x, y); + endif + + ## check upper and lower bounds of y + if (! is_function_handle (ya)) + if (! (isreal (ya) && isscalar (ya))) + error ("quad2d: YA must be a real scalar or a function"); + endif + ya = @(x) ya * ones(rows (x), columns (x)); + endif + if (! is_function_handle (yb)) + if (! (isreal (yb) && isscalar (yb))) + error ("quad2d: YB must be a real scalar or a function"); + endif + yb = @(x) yb * ones(rows (x), columns (x)); + endif + + iter = 0; + qaccept = 0; + qerraccept = 0; + + if (singular) + ## Shampine suggests using the singularity weakening transform + ## suggested by Havie. + ## \int_a^b f(x) dx = \int_0^pi f (g(t)) (dx / dt) dt + ## where + ## g(t) = ((a - b) * cos(t) + (a + b)) / 2 + ## dx = - (a - b) * sin(t) / 2 dt + ## Now our integral is + ## \int_a^b \int_0^1 f(x,y) dydx + ## as we already subsitute for "y", so + ## gx(tx) = ((a - b) * cos(tx) + (a + b)) / 2 + ## gy(ty) = (1 - cos(ty)) / 2 + ## dydx = (b - a) * sin(tx) * sin(ty) / 4 dtydtx + + xtrans = @(tx) ((xa - xb) .* cos (tx) + (xa + xb)) ./ 2; + ytrans = @(ty) (1 - cos (ty)) ./ 2; + ztrans = @(tx, ty) (xb - xa) .* sin(tx) .* sin(ty) ./ 4; + area = pi ^ 2; + + ## Initialize tile list + tilelist(1) = struct ("xa", 0, "xb", pi, "ya", 0, "yb", pi, ... + "q", 0, "qerr", Inf); + else + xtrans = @(tx) tx; + ytrans = @(ty) ty; + ztrans = @(tx, ty) 1; + area = (xb - xa); + + ## Initialize tile list + tilelist(1) = struct ("xa", xa, "xb", xb, "ya", 0, "yb", 1, ... + "q", 0, "qerr", Inf); + endif + + while (length (tilelist) > 0 && iter < maxiter) + ## Get tile with the largest error + [~, idx] = max ([tilelist.qerr]); + tile = tilelist(idx); + tilelist(idx) = []; + + ## Subdivide the tile into 4 subtiles + iter += 4; + tiles(4) = struct ("xa", tile.xa, "xb", tile.xa + (tile.xb - tile.xa) / 2, + "ya", tile.ya, "yb", tile.ya + (tile.yb - tile.ya) / 2, + "q", 0, "qerr", 0); + tiles(3) = struct ("xa", tile.xa, "xb", tile.xa + (tile.xb - tile.xa) / 2, + "ya", tile.ya + (tile.yb - tile.ya) / 2, "yb", tile.yb, + "q", 0, "qerr", 0); + tiles(2) = struct ("xa", tile.xa + (tile.xb - tile.xa) / 2, "xb", tile.xb, + "ya", tile.ya, "yb", tile.ya + (tile.yb - tile.ya) / 2, + "q", 0, "qerr", 0); + tiles(1) = struct ("xa", tile.xa + (tile.xb - tile.xa) / 2, "xb", tile.xb, + "ya", tile.ya + (tile.yb - tile.ya) / 2, "yb", tile.yb, + "q", 0, "qerr", 0); + + ## Perform the quadrature of 4 subtiles + for i = 1:4 + [tiles(i).q, tiles(i).qerr] = ... + tensorproduct (f, ya, yb, tiles(i), xtrans, ytrans, ztrans, singular); + endfor + + q = qaccept + sum ([[tilelist.q], tiles.q]); + err = qerraccept + sum ([[tilelist.qerr], tiles.qerr]); + tol = max (abstol, reltol .* abs (q)); + + ## Shampine suggests taking a margin of a factor of 8 for + ## the local tolerance. That, and the fact that we are subdividing + ## into 4 tiles, means we divide by 32 at this point. + localtol = tol * ([tile.xb] - [tile.xa]) * ([tile.yb] - [tile.ya]) ... + / area / 32; + + ## If global tolerance is met, return. + if (err < tol) + break; + endif + + ## Accept the tiles meeting the tolerance, and add the others back to + ## the list of tiles to treat. + idx = find ([tiles.qerr] < localtol); + qaccept += sum ([tiles(idx).q]); + qerraccept += sum ([tiles(idx).qerr]); + tiles(idx) = []; + tilelist = [tilelist, tiles]; + endwhile + + ## Verify convergence + if (iter >= maxiter) + if (err > max (abstol, reltol .* abs (q))) + warning (["quad2d: " ... + "Maximum number of sub-tiles reached without convergence"]); + else + warning (["quad2d: " ... + "Maximum number of sub-tiles reached, accuracy may be low"]); + endif + if (failureplot) + newplot (); + title ("quad2d : Areas needing refinement"); + for tile = tilelist + xaa = xtrans(tile.xa); + xbb = xtrans(tile.xb); + y1 = ya(xaa) + ytrans(tile.ya) * (yb(xaa) - ya(xaa)); + y2 = ya(xaa) + ytrans(tile.yb) * (yb(xaa) - ya(xaa)); + y3 = ya(xbb) + ytrans(tile.yb) * (yb(xbb) - ya(xbb)); + y4 = ya(xbb) + ytrans(tile.ya) * (yb(xbb) - ya(xbb)); + patch ([xaa, xaa, xbb, xbb, xaa], [y1, y2, y3, y4, y1], "b"); + endfor + endif + endif + +endfunction + +function [q, qerr] = tensorproduct (f, ya, yb, tile, xtrans, ytrans, ztrans, singular) + ## The Shampine TwoD paper proposes using a G3,K7 rule in a tensor product. + ## I couldn't find a tabulated abscissas and weights of a G3,K7 rule publicly + ## available, so use a G7,K15 rule from Octave's implementation of quadgk. + + persistent abscissa = [-0.9914553711208126e+00, -0.9491079123427585e+00, ... + -0.8648644233597691e+00, -0.7415311855993944e+00, ... + -0.5860872354676911e+00, -0.4058451513773972e+00, ... + -0.2077849550078985e+00, 0.0000000000000000e+00, ... + 0.2077849550078985e+00, 0.4058451513773972e+00, ... + 0.5860872354676911e+00, 0.7415311855993944e+00, ... + 0.8648644233597691e+00, 0.9491079123427585e+00, ... + 0.9914553711208126e+00]; + + persistent weights15 = [0.2293532201052922e-01, 0.6309209262997855e-01, ... + 0.1047900103222502e+00, 0.1406532597155259e+00, ... + 0.1690047266392679e+00, 0.1903505780647854e+00, ... + 0.2044329400752989e+00, 0.2094821410847278e+00, ... + 0.2044329400752989e+00, 0.1903505780647854e+00, ... + 0.1690047266392679e+00, 0.1406532597155259e+00, ... + 0.1047900103222502e+00, 0.6309209262997855e-01, ... + 0.2293532201052922e-01]; + + persistent weights7 = [0.0, ... + 0.1294849661688697e+00, 0.0, ... + 0.2797053914892767e+00, 0.0, ... + 0.3818300505051889e+00, 0.0, ... + 0.4179591836734694e+00, 0.0, ... + 0.3818300505051889e+00, 0.0, ... + 0.2797053914892767e+00, 0.0, ... + 0.1294849661688697e+00, 0.0]; + + xaa = tile.xa; + xbb = tile.xb; + yaa = tile.ya; + ybb = tile.yb; + + tx = ((xbb - xaa) * abscissa + xaa + xbb) / 2; + x = xtrans(tx); + ty = (abscissa' * (ybb - yaa) + yaa + ybb) / 2; + y = ones (15, 1) * ya(x) + ytrans(ty) * (yb(x) - ya(x)); + + xhalfwidth = (xbb - xaa ) / 2; + yhalfwidth = ones (15, 1) * (yb(x) - ya(x)) .* (ybb - yaa) ./ 2; + + x = ones (15, 1) * x; + tx = ones (15,1) * tx; + ty = ty * ones (1, 15); + + z = yhalfwidth .* f (x, y) .* ztrans(tx, ty) .* xhalfwidth; + q = weights15 * (weights15 * z)'; + qerr = abs (weights7 * (weights7 * z)' - q); +endfunction + + +%!shared f +%! f = @(x, y) x .* y; +%!assert (quad2d (f, 0, 1, 0, 1), 0.25, 1e-10) + +%!test +%! f = @(x, y) 9 * x.^2 + 15 * y.^2; +%!assert (quad2d (f, 0, 5, -5, 0, "AbsTol", 1e-9), 5000, 1e-9) +%!assert (quad2d (f, 0, 5, -5, 0, "RelTol", 1e-6), 5000, -1e-6) +%!assert (quad2d (f, 0, 5, -5, 0, "RelTol", 1e-6, "AbsTol", 1e-9), 5000, 1e-9) + +## tests from dblquad +%!test +%! f = @(x, y) 1 ./ (x+y); +%!assert (quad2d (f, 0, 1, 0, 1, "AbsTol", 1e-7), 2*log (2), 1e-7) +%!assert (quad2d (f, 0, 1, 0, 1, "RelTol", 1e-5), 2*log (2), -1e-5) +%!assert (quad2d (f, 0, 1, 0, 1, "AbsTol", 1e-8, "RelTol", 1e-6), +%! 2*log (2), -1e-6) +%!assert (quad2d (f, 0, 1, 0, @(x) 1 - x), 1, -1e-6) +%!assert (quad2d (f, 0, 1, 0, @(x) 1 - x, "Singular", true), 1, -1e-6) + +%!assert (quad2d (@(x, y) exp (-x.^2 - y.^2) , -1, 1, -1, 1), +%! pi * erf (1).^2, 1e-10) + +%!assert (quad2d (@plus, 1, 2, 3, 4), 5, 1e-10) + +## Test input validation +%!error quad2d () +%!error quad2d (@plus) +%!error quad2d (@plus, 1) +%!error quad2d (@plus, 1, 2) +%!error quad2d (@plus, 1, 2, 3) +%!error quad2d (@plus, 1, 2, 3, 4, "foo") +%!error quad2d (0, 1, 2, 3, 4) # f must be function handle +%!error quad2d (@plus, 1i, 2, 3, 4) # real limits +%!error quad2d (@plus, 1, 2i, 3, 4) # real limits +%!error quad2d (@plus, [1 1], 2, 3, 4) # scalar limits +%!error quad2d (@plus, 1, [2 2], 3, 4) # scalar limits +%!error quad2d (@plus, 1, 2, 3, 4, 99, "bar") +%!error quad2d (@plus, 1, 2, 3, 4, "AbsTol", "foo") +%!error quad2d (@plus, 1, 2, 3, 4, "AbsTol", [1, 2]) +%!error quad2d (@plus, 1, 2, 3, 4, "AbsTol", -1) +%!error quad2d (@plus, 1, 2, 3, 4, "RelTol", "foo") +%!error quad2d (@plus, 1, 2, 3, 4, "RelTol", [1, 2]) +%!error quad2d (@plus, 1, 2, 3, 4, "RelTol", -1) +%!error +%! quad2d (@plus,1,2,3,4, "MaxFunEvals", {1}); +%!error +%! quad2d (@plus,1,2,3,4, "MaxFunEvals", [1 1]); +%!error +%! quad2d (@plus,1,2,3,4, "MaxFunEvals", 1.5); +%!error +%! quad2d (@plus,1,2,3,4, "MaxFunEvals", -1); +%!error +%! quad2d (@plus,1,2,3,4, "Singular", [0 1]); +%!error +%! quad2d (@plus,1,2,3,4, "Singular", {true}); +%!error +%! quad2d (@plus,1,2,3,4, "Vectorized", [0 1]); +%!error +%! quad2d (@plus,1,2,3,4, "Vectorized", {true}); +%!error +%! quad2d (@plus,1,2,3,4, "FailurePlot", [0 1]); +%!error +%! quad2d (@plus,1,2,3,4, "FailurePlot", {true}); +%!error quad2d (@plus, 1, 2, 3, 4, "foo", "bar") +%!error quad2d (@plus, 1, 2, 3i, 4) +%!error quad2d (@plus, 1, 2, [3 3], 4) +%!error quad2d (@plus, 1, 2, 3, 4i) +%!error quad2d (@plus, 1, 2, 3, [4 4]) +%!warning quad2d (@plus, 1, 2, 3, 4, "MaxFunEvals", 1); diff -r 9fab3273ff26 -r 2e64bed0bb3a scripts/help/__unimplemented__.m --- a/scripts/help/__unimplemented__.m Sat Oct 14 18:16:55 2017 -0500 +++ b/scripts/help/__unimplemented__.m Sat Oct 14 18:30:51 2017 -0400 @@ -66,7 +66,7 @@ case "matlabrc" txt = ["matlabrc is not implemented. ", ... - 'Octave uses the file ".octaverc" instead.']; + "Octave uses the file '.octaverc' instead."]; case {"ode113", "ode15i", "ode15s", "ode23s", "ode23t", "ode23tb"} txt = ["Octave provides lsode and ode45 for solving differential equations. ", ... @@ -78,9 +78,6 @@ txt = ["startup is not implemented. ", ... 'Octave uses the file ".octaverc" instead.']; - case "quad2d" - txt = ["quad2d is not implemented. Consider using dblquad."]; - case {"xlsread", "xlsfinfo", "xlswrite", "wk1read", "wk1finfo", "wk1write"} txt = ["Functions for spreadsheet style I/O ", ... "(.xls .xlsx .sxc .ods .dbf .wk1 etc.) " , ... @@ -843,11 +840,10 @@ }; endfunction - %!test %! str = __unimplemented__ ("no_name_function"); %! assert (isempty (str)); -%! str = __unimplemented__ ("quad2d"); -%! assert (str(1:51), "quad2d is not implemented. Consider using dblquad."); +%! str = __unimplemented__ ("matlabrc"); +%! assert (str(1:71), "matlabrc is not implemented. Octave uses the file '.octaverc' instead."); %! str = __unimplemented__ ("MException"); %! assert (str(1:58), "the 'MException' function is not yet implemented in Octave");