Skip to main content
replaced http://mathematica.stackexchange.com/ with https://mathematica.stackexchange.com/
Source Link

where chebRule[data___] is the structure returned by NIntegrate`InitializeIntegrationRule and region is a NIntegrate`IntegrationRegion[] that contains data (including the integrand) for computing the integral over the region. (An NIntegrate`IntegrationRegion[] is also the argument #1 in the option IntegrationMonitor, an undocumented option which has been discussed in these Q&Ain these Q&A.) An IntegrationRegion has many methodsmany methods. We will need two:

The computation of the Chebyshev series follows Boyd, which I first encountered herehere. Finding the integral from a Chebyshev series is well-known and easy to derive.

where chebRule[data___] is the structure returned by NIntegrate`InitializeIntegrationRule and region is a NIntegrate`IntegrationRegion[] that contains data (including the integrand) for computing the integral over the region. (An NIntegrate`IntegrationRegion[] is also the argument #1 in the option IntegrationMonitor, an undocumented option which has been discussed in these Q&A.) An IntegrationRegion has many methods. We will need two:

The computation of the Chebyshev series follows Boyd, which I first encountered here. Finding the integral from a Chebyshev series is well-known and easy to derive.

where chebRule[data___] is the structure returned by NIntegrate`InitializeIntegrationRule and region is a NIntegrate`IntegrationRegion[] that contains data (including the integrand) for computing the integral over the region. (An NIntegrate`IntegrationRegion[] is also the argument #1 in the option IntegrationMonitor, an undocumented option which has been discussed in these Q&A.) An IntegrationRegion has many methods. We will need two:

The computation of the Chebyshev series follows Boyd, which I first encountered here. Finding the integral from a Chebyshev series is well-known and easy to derive.

Editorial proofreading
Source Link
Michael E2
  • 245k
  • 18
  • 351
  • 775

There is a level above this. Recall that in adaptive integration NIntegrate subdivides regions (intervals in 1D), and applies the integration rule to each subregion. If the error is too great in a subregion, itNIntegrate divides it again. When NIntegrate applies a GeneralRule to a subregion of integration, the result returned to NIntegrate has the form

where chebRule[data___] is the structure returned by NIntegrate`InitializeIntegrationRule and region is a NIntegrate`IntegrationRegion[] that contains data (including the integrand) for computing the integral over the region. An ``NIntegrate(An IntegrationRegion[]NIntegrate`IntegrationRegion[] is also the argument #1 in the option IntegrationMonitor, an undocumented option which has been discussed IntegrationMonitorin these Q&A.) hasAn IntegrationRegion has many methods. We will need two:

Here1. Here you can see the recursive subdivision and the convergence error estimates. If you follow carefully, you can see how the error and the axis determine the subdivisions. (There are threefour iterations; they can be grouped as the first printed rule, and then groups of two; the nextlast two pairs show the subdivision of the first two pairs, andin reversed order according to their error estimates, 1.93 x 10^-6 vs. 3.27 * 10^-6. The errors that IntegrationMonitor sows are the last foursums at each step of the errors over the current subintervals.)

A2. A relatively innocuous integrand is

We compare it with similar calls to the rules "GaussKronrod"``, ``"ClenshawCurtisRule""GaussKronrod", "ClenshawCurtisRule", except for the last{9, 17} setting, which they do not support. By For comparison, the exact value to 16 digits is

and the default method "MultidimensionalRule" takes 2.38428 seconds, 8325 function evaluations and has an error of 3.75823*10^-10.

First of all, the error for chebRule is closer to the goal, which should be around 10^-8 for this integral. The function f1 evaluates quickly so one can see that chebRule appears to be slower per function evaluation than "ClenshawCurtisRule", but it seems also to use fewer function calls. The larger error and fewer calls are related to the difference in the error estimators of the two methods. If the integrand is expensive to calculate, then chebRule may have an advantage over "ClenshawCurtisRule".

3. An example where chebRule can beat "ClenshawCurtisRule" is shown below. One thing to realize is that one does not take advantage of the convergence rate of the Chebyshev series if a small value for "Points" is used (this is true for the Clenshaw-Curtis rule, too). Another is that if we do not use MachinePrecision, then we cannot take advantage of current hardware that optimizes matrix multiplication, and "ClenshawCurtisRule" loses some of its advantage.

exact = Integrate[Cos[x]^2 Cos[10 x]^2, {x, 0, 50}];
NIntegrate[Cos[x]^2 Cos[10 x]^2, {x, 0, 50}, 
   Method -> {chebRule, "Points" -> 1 + 2^6}, 
   WorkingPrecision -> 32] - exact // AbsoluteTiming
NIntegrate[Cos[x]^2 Cos[10 x]^2, {x, 0, 50}, 
   Method -> {"ClenshawCurtisRule", "Points" -> 1 + 2^6}, 
   WorkingPrecision -> 32] - exact // AbsoluteTiming
(*
  {0.137261, -2.*10^-30}
  {0.317895, 0.*10^-31}
*)

One can improve chebRule quite a bit by writing one's own "chebPoints" routine that calculates just the abscissas (and not the weights and error weights).

The error weights in a GeneralRule are typically the difference of the weights of two related integration rules which is known to approximate (or at least bound) the error. In my case, I wanted to compute the error in a different way, basically for exploring my own interests and not to create a better integrator. From Anton's examples and a little spelunking, I seem to have figured how to do this. Since it is an answer to this question and it extends the (currently only) other answer, I thought I should share it. I doubt my code is bullet-proof. It can perform quite well; it can also do just okay; and I haven't tested enough pathological cases to find one where it breaks, but I haven't been too cruel to itthat is not my main interest. Hopefully, eitherit will be accepted as an example of how such a integration rule can be incorporated in NIntegrate[].

The rule belowchebRule is closely related to the Clenshaw-Curtis rule, which itself follows the form of a GeneralRule. Instead of the usual error estimate, I wanted to estimate the error from the Chebyshev series of integrand over the integration region. If the function is analytic in a (complex) neighborhood of the interval of integration, then the coefficients converge to zero geometrically and one can estimate the error of approximation from the tail of such a truncated series. (See, for instance, Boyd, Chebyshev and Fourier Spectral Methods, Dover, New York, 2001, ch. 2). Since one can construct a series in which any number of coefficients are zero, this is not a fool-proof way to estimate the error. Except for (nearly) even or odd functions, which have series in which every other coefficient is (nearly) zero, functions in practice tend to have only a few (finite number of) terms that do not behave.

There is a level above this. Recall that in adaptive integration NIntegrate subdivides regions (intervals in 1D), and applies the integration rule to each subregion. If the error is too great in a subregion, it divides again. When NIntegrate applies a GeneralRule to a subregion of integration, the result returned to NIntegrate has the form

where chebRule[data___] is the structure returned by NIntegrate`InitializeIntegrationRule and region is a NIntegrate`IntegrationRegion[] that contains data (including the integrand) for computing the integral over the region. An ``NIntegrateIntegrationRegion[] is the argument #1 in the option IntegrationMonitor has many methods. We will need two:

Here you can see the recursive subdivision and the convergence error estimates. If you follow carefully, you can see how the error and the axis determine the subdivisions. (There are three iterations; they can be grouped as the first printed rule, the next two, and the last four.)

A relatively innocuous integrand is

We compare it with similar calls to the rules "GaussKronrod"``, ``"ClenshawCurtisRule", except for the last. By comparison the exact value to 16 digits is

and the default "MultidimensionalRule" takes 2.38428 seconds, 8325 function evaluations and has an error of 3.75823*10^-10.

First of all, the error is closer to the goal, which be around 10^-8 for this integral. The function f1 evaluates quickly so one can see that chebRule appears to be slower per function evaluation than "ClenshawCurtisRule", but it seems also to use fewer function calls. The larger error and fewer calls are related to the difference in the error estimators of the two methods. If the integrand is expensive to calculate, then chebRule may have an advantage.

The error weights in a GeneralRule are typically the difference of the weights of two related integration rules which is known to approximate (or at least bound) the error. In my case, I wanted to compute the error in a different way, basically for exploring my own interests and not to create a better integrator. From Anton's examples and a little spelunking, I seem to have figured how to do this. Since it is an answer to this question and it extends the (currently only) other answer, I thought I should share it. I doubt my code is bullet-proof. It can perform quite well; it can also do just okay; and I haven't tested enough pathological cases to find one where it breaks, but I haven't been too cruel to it, either.

The rule below is closely related to the Clenshaw-Curtis rule, which itself follows the form of a GeneralRule. Instead of the usual error estimate, I wanted to estimate the error from the Chebyshev series of integrand over the integration region. If the function is analytic in a (complex) neighborhood of the interval of integration, then the coefficients converge to zero geometrically and one can estimate the error of approximation from the tail of such a truncated series. (See, for instance, Boyd, Chebyshev and Fourier Spectral Methods, Dover, New York, 2001, ch. 2). Since one can construct a series in which any number of coefficients are zero, this is not a fool-proof way to estimate the error. Except for (nearly) even or odd functions, which have series in which every other coefficient is (nearly) zero, functions in practice tend to have only a few (finite number of) terms that do not behave.

There is a level above this. Recall that in adaptive integration NIntegrate subdivides regions (intervals in 1D), and applies the integration rule to each subregion. If the error is too great in a subregion, NIntegrate divides it again. When NIntegrate applies a GeneralRule to a subregion of integration, the result returned to NIntegrate has the form

where chebRule[data___] is the structure returned by NIntegrate`InitializeIntegrationRule and region is a NIntegrate`IntegrationRegion[] that contains data (including the integrand) for computing the integral over the region. (An NIntegrate`IntegrationRegion[] is also the argument #1 in the option IntegrationMonitor, an undocumented option which has been discussed in these Q&A.) An IntegrationRegion has many methods. We will need two:

1. Here you can see the recursive subdivision and the convergence error estimates. If you follow carefully, you can see how the error and the axis determine the subdivisions. (There are four iterations; they can be grouped as the first printed rule and then groups of two; the last two pairs show the subdivision of the first two pairs, in reversed order according to their error estimates, 1.93 x 10^-6 vs. 3.27 * 10^-6. The errors that IntegrationMonitor sows are the sums at each step of the errors over the current subintervals.)

2. A relatively innocuous integrand is

We compare it with similar calls to the rules "GaussKronrod", "ClenshawCurtisRule", except for the {9, 17} setting, which they do not support. For comparison, the exact value to 16 digits is

and the default method "MultidimensionalRule" takes 2.38428 seconds, 8325 function evaluations and has an error of 3.75823*10^-10.

First of all, the error for chebRule is closer to the goal, which should be around 10^-8 for this integral. The function f1 evaluates quickly so one can see that chebRule appears to be slower per function evaluation than "ClenshawCurtisRule", but it seems also to use fewer function calls. The larger error and fewer calls are related to the difference in the error estimators of the two methods. If the integrand is expensive to calculate, then chebRule may have an advantage over "ClenshawCurtisRule".

3. An example where chebRule can beat "ClenshawCurtisRule" is shown below. One thing to realize is that one does not take advantage of the convergence rate of the Chebyshev series if a small value for "Points" is used (this is true for the Clenshaw-Curtis rule, too). Another is that if we do not use MachinePrecision, then we cannot take advantage of current hardware that optimizes matrix multiplication, and "ClenshawCurtisRule" loses some of its advantage.

exact = Integrate[Cos[x]^2 Cos[10 x]^2, {x, 0, 50}];
NIntegrate[Cos[x]^2 Cos[10 x]^2, {x, 0, 50}, 
   Method -> {chebRule, "Points" -> 1 + 2^6}, 
   WorkingPrecision -> 32] - exact // AbsoluteTiming
NIntegrate[Cos[x]^2 Cos[10 x]^2, {x, 0, 50}, 
   Method -> {"ClenshawCurtisRule", "Points" -> 1 + 2^6}, 
   WorkingPrecision -> 32] - exact // AbsoluteTiming
(*
  {0.137261, -2.*10^-30}
  {0.317895, 0.*10^-31}
*)

One can improve chebRule quite a bit by writing one's own "chebPoints" routine that calculates just the abscissas (and not the weights and error weights).

The error weights in a GeneralRule are typically the difference of the weights of two related integration rules which is known to approximate (or at least bound) the error. In my case, I wanted to compute the error in a different way, basically for exploring my own interests and not to create a better integrator. From Anton's examples and a little spelunking, I seem to have figured how to do this. Since it is an answer to this question and it extends the (currently only) other answer, I thought I should share it. I doubt my code is bullet-proof. It can perform quite well; it can also do just okay; and I haven't tested enough pathological cases to find one where it breaks, but that is not my main interest. Hopefully, it will be accepted as an example of how such a integration rule can be incorporated in NIntegrate[].

The rule chebRule is closely related to the Clenshaw-Curtis rule, which itself follows the form of a GeneralRule. Instead of the usual error estimate, I wanted to estimate the error from the Chebyshev series of integrand over the integration region. If the function is analytic in a (complex) neighborhood of the interval of integration, then the coefficients converge to zero geometrically and one can estimate the error of approximation from the tail of such a truncated series. (See, for instance, Boyd, Chebyshev and Fourier Spectral Methods, Dover, New York, 2001, ch. 2). Since one can construct a series in which any number of coefficients are zero, this is not a fool-proof way to estimate the error. Except for (nearly) even or odd functions, which have series in which every other coefficient is (nearly) zero, functions in practice tend to have only a few (finite number of) terms that do not behave.

Post Undeleted by Michael E2
added 3197 characters in body
Source Link
Michael E2
  • 245k
  • 18
  • 351
  • 775

Here is another type of integration rule. It has a customized error estimator, incorporating which was my main interest. In fact, the rule below computes an integral of arbitrary dimension, using its own internal formula for the integral as well.

A rule of the type NIintegrate`GeneralRule[{abscissas, weights, errweights}] computes computes the integral and error estimates via dot products in more or less the following following way:

There is a level above this. Recall that in adaptive integration NIntegrate subdivides subdivides regions (intervals in 1D), and applies the integration rule to each subregion subregion. If the error is too great in a subregion, it divides again. When    NIntegrate applies a GeneralRule to a subregion of integration, the result returned returned to NIntegrate has the form

A GeneralRule is always 1D, so the axis is always 1. In multidimensional rules rules the number will indicate along which axis the region is to be split in half half, if the error is too great. Ultimately, the triple above is what an integration integration rule needs to return when it is called by NIntegrate.

A rule, let's call it chebRule as we will below, can be called by NIntegrate in in at least two ways, one of which I have figured out. The call has the form

where chebRule[data___] is the structure returned by    NIntegrate`InitializeIntegrationRule and region is a    NIntegrate`IntegrationRegion[] that contains data (including the integrand) for for computing the integral over the region. An ``NIntegrateIntegrationRegion[] is is the argument #1 in the option    IntegrationMonitor has has many methods methods. We We will need two:

which give the boundaries of the region and a function for evaluating the integrand integrand, respectively. You evaluate the function on the rule's sampling points points taken from the interval {0, 1}. The method takes care of the rescaling for for you. All All you need to do inside    chebRule[data___]["ApproximateIntegral"[region]] is calculate the three things above above, the integral, error, and, if implementing a multidimensional rule, which axis axis to subdivide next.

The initialization code processes the options, of which currently there is only one one, and gets the Clenshaw-Curtis sampling points for use in the rule. The The only option option is "Points", which works like the    "ClenshawCurtisRule" option option and generates 2 n +- 1 points for a setting of n. Unlike    "ClenshawCurtisRule", it will take a list of point settings for multidimensional multidimensional integrals.

ClearAll[chebRule];

Options[chebRule] = {"Points" -> Automatic};
chebRuleProperties = Part[Options[chebRule], All, 1];
chebRule::dim = "The value of option `` should be a list of length ``";

(* initialization of rule *)
chebRule /: NIntegrate`InitializeIntegrationRule[
   chebRule, nfs_, ranges_, ruleOpts_, allOpts_] := 

 Module[{absc, points, minpts = 3, dim, wprec, t},
  
  wprec = WorkingPrecision /. allOpts;
  t = NIntegrate`GetMethodOptionValues[chebRule, chebRuleProperties, ruleOpts];
  If[t === $Failed, Return[$Failed]];
  {points} = t;
  points = points /. Automatic -> 5;
  dim = Length@ranges;
  If[! AllTrue[Flatten@{points}, # >= minpts &],
   Message[NIntegrate::mintmin, First@FilterRules[ruleOpts, "Points"], minpts];
   Return[$Failed]
   ];
  
  (* "Points" -> list of length dim *)
  If[ListQ@points && Length@points != dim,
   Message[chebRule::dim, First@FilterRules[ruleOpts, "Points"], dim];
   Return[$Failed]
   ];
  (* absc = list of ascissas for each dimension *)
  If[ListQ@points,
   absc = First@NIntegrate`ClenshawCurtisRuleData[#, wprec] & /@ points,
   absc = First@NIntegrate`ClenshawCurtisRuleData[points, wprec];
   absc = Table[absc, {dim}]
   ];
  
  chebRule[absc]
  ]

Here is the integration code. First there is the error estimator. It comes from from the convergence theory for Chebyshev series (see Boyd, reference below). What What really matters is that it computes a number that estimates the error of the quadrature quadrature to pass back to NIntegrate. The code below is used to come up with an an error for each axis (from the iterated integral), which is then used in the main main integrator below to determine how the subregion should be divided next.

(* call: self["localError"[coeff]] --
 * estimates error of Chebyshev series `coeff`,
 * provided truncated series is long enough *)
chebRule[_]["localError"[coeff_]]chebLE := Compile[{{coeff, _Real, 1}},
   With[{e2 = Total[Abs@coeff[[{-2, ;;]]]-1}]]], e4 = Total[Abs@coeff[[{-4, ;;]]]-3, -2, -1}]]]},
    With[{r2 = e2/e4},
     If[Length@coeff > 8 && e4 != 0 && r2 < 1,
      1.21 e2^2e2*r2/e4(1 - Sqrt[r2]), (* extrapolate error *)
      e2]                        (* use last two terms as upper bound *)
   ];  ]]
   ];
(* callcalled many times, MachinePrecision OK(?), hence compiled *)
chebRule[_]["localError"[coeff_]] := chebLE[coeff];

(* self["globalError"[error[[n]],Times@@widths[[;;n]],n]] -- 
 * estimates "global" error of integrating along axis `n`
 * by integrating local error bound along axis `(n-1)`
 * and multiplying by the other dimensions (`volume`) *)
chebRule[absc_]["globalError"[errors_chebGE = Compile[{{errors, volume__Real, dim_]]1}, :={prevabsc, 
 _Real, 1}, Module[{error =volume, errors_Real}},
   If[dim >Module[{error, 1ints},
    (* integrate errors along next axis up (trapezoidal rule) *)
    error = Partition[Flatten[error]Partition[Flatten[errors], Length@absc[[dim - 1]]];Length@prevabsc];
    errorints = volume * 
      error.(PadRight[#Join[{First@#}, Length@absc[[dimRest@# -+ 1]]Most@#, 0.{Last@#}] + &@
        Differences@prevabsc);
   PadLeft[#, Length@absc[[dim0.5 -volume*Max@ints 1]],(*
 0.] &@ Differences@absc[[dim -max 1]]integral will be the error bound *);
    error]
 = Max@error/2, ];
chebRule[absc_]["globalError"[errors_, volume_, 1]] :=  volume*Max[errors];
(* topcompiling axis:won't volumehelp ==much, lengthexcept ofmaybe intervalin high dimensional integrals *)
  chebRule[absc_]["globalError"[errors_, volume_, erroraxis_]] := volume*Max[error]
   chebGE[errors, ]
absc[[axis - 1]], ];volume];

The computation of the Chebyshev series follows Boyd, which I first encountered    here. Finding Finding the integral from a Chebyshev series is    well-known and and easy to derive.

chebRule[absc_]["Abscissas"[]] := absc;

(* Subroutines for computing and integrating Chebyshev series *)
chebRule[_]["series"[vals_, width_]] := Module[{coeff},
   coeff =
    Sqrt[2/(Length@vals - 1)]*FourierDCT[Reverse@vals, 1]/width;
   coeff[[{1, -1}]] /= 2;
   coeff
   ];
chebRule[_]["integrate"[coeff_, width_]] :=
  Map[     (* integration rule for a Chebyshev series *)
   Total[width/(1 - Range[0, Length@# - 1, 2]^2)*#[[1 ;; ;; 2]]] &,
   coeff,
   {-2}];


(* 
 * Main integrator  
 *)
(self : chebRule[___])["ApproximateIntegral"[region_]] := 
  Module[{absc, ranges, dim, fvals, coeff, intintegral, widths, error, naxis},
   
   absc = self@ "Abscissas"[];self@"Abscissas"[];
   ranges = region@ "Boundaries";region@"Boundaries";
   dim = Length@ ranges;Length@ranges;
   widths = Flatten[Differences /@ ranges];
   error = Table[{}, {dim}];
   
   fvals =  (* evaluate integrand *)
    Outer[region["EvaluateTransformedIntegrand"[{##}]] &, 
     Sequence @@ absc];
   
   naxis = dim; (* convenient iterator with Nest[] *)
   intintegral = Nest[
     (* replacereplaces each {-2}bottom level list of function/integral values by its integral *)
     Function[{vals},
      (* first*first compute Chebyshev series and approximation error *)
      coeff = Map[
        (* compute cosine transformsMap[self["series"[#, onwidths[[axis]]]] bottom&, levelvals, *){-2}];
   
      (coeff =* Sqrt[2/(Length@#estimate -the 1)]errors * FourierDCT[Reverse@#, 1]/widths[[n]]; )
          coeff[[{1, -1}]]error[[axis]] /= 2;
       (* first of error[[n]]the =Chebyshev {error[[n]],approximation self["localError"[coeff]]};*)
          coeff)Map[self["localError"[#]] &,
        valscoeff, {-2}];
 
      (*error[[axis]] estimate= an error(* next for the whole level (axis n) *)
      error[[n]] = self["globalError"[error[[n]]self["globalError"[error[[axis]], Times @@ widths[[;; n]]axis]], n]];
         axis]];

      (* caveat: decrement n axis  one step too early *)
      naxis--;
      (* integration rule for a Chebyshev series *)
      Map[
       Total[widths[[n + 1]]/(1 - Range[0, Length@# - 1Map[self["integrate"[#, 2]^2) #[[1 ;;widths[[axis ;;+ 2]]]1]]]] &,
       coeff,
       {-2}]
      ],
     fvals,
     dim];

   (* define  dbPrintaxis = Print  for verbose outputFirst@Ordering[error, *)-1];
   dbPrint[ranges -> {intintegral, error, First@Ordering[error, -1]axis}];
   error =  (* convert from MachinePrecision to working precision *)
   {int, Max@error, First@Ordering[errorSetPrecision[Max@error, -1]}region@"WorkingPrecision"];
  (* {integral, error, axis *)}
   ];

##Examples (TBD)

Here you can see the recursive subdivision and the convergence error estimates. If you follow carefully, you can see how the error and the axis determine the subdivisions subdivisions. (There are three iterations; they can be grouped as the first printed printed rule, the next two, and the last four.)

Block[{dbPrint = Print},
 Reap@NIntegrate[Cos[7 x] Sin[5 y], {x, 0, 2}, {y, 0, 3},
   Method -> {chebRule, "Points" -> 9}, 
   IntegrationMonitor :> ((Sow@Total@Through[#1@"Error"]) &)]
 ]
First@% - Integrate[Cos[7 x] Sin[5 y], {x, 0, 2}, {y, 0, 3}]

Mathematica graphicsMathematica graphics

MoreA relatively innocuous integrand is

f1 = Sin[2 x] Sin[y^3] Sin[x y];

We will integrate it with calls of the form

NIntegrate[f1, {x, 0, 2}, {y, 0, 3}, Method -> ...]

with Method settings like

Method -> chebRule
Method -> {chebRule, "Points" -> 9}
Method -> {chebRule, "Points" -> 17}
Method -> {chebRule, "Points" -> {9, 17}}

We compare it with similar calls to comethe rules "GaussKronrod"``, ``"ClenshawCurtisRule", except for the last. By comparison the exact value to 16 digits is

exact = 0.1998439750348762

and the default "MultidimensionalRule" takes 2.38428 seconds, 8325 function evaluations and has an error of 3.75823*10^-10.

Mathematica graphics
Comparison of methods (code at end)

First of all, the error is closer to the goal, which be around 10^-8 for this integral. The function f1 evaluates quickly so one can see that chebRule appears to be slower per function evaluation than "ClenshawCurtisRule", but it seems also to use fewer function calls. The larger error and fewer calls are related to the difference in the error estimators of the two methods. If the integrand is expensive to calculate, then chebRule may have an advantage.

The error weights in a GeneralRule are typically the difference of the weights of of two related integration rules which is known to approximate (or at least bound bound) the error. In In my case, I wanted to compute the error in a different way, basically basically for exploring my own interests and not to create a better integrator. From From Anton's examples and a little spelunking, I seem to have figured how to do this this. Since it is an answer to this question and it extends the (currently only only) other answer, I thought I should share it. I doubt my code is bullet bullet-proof. It can perform quite well; it can also do just okay; and I haven't haven't tested enough pathological cases to find one where it breaks, but I haven't haven't been too cruel to it, either.

The rule below is closely related to the Clenshaw-Curtis rule, which itself follows follows the form of a GeneralRule. Instead of the usual error estimate, I wanted wanted to estimate the error from the Chebyshev series of integrand over the integration integration region. If the function is analytic in a (complex) neighborhood of the the interval of integration, then the coefficients converge to zero geometrically geometrically and one can estimate the error of approximation from the tail of such such a truncated series.    (See, for instance, Boyd, Chebyshev and Fourier Spectral Spectral Methods, Dover, New York, 2001 2001, ch. 2). Since one can construct construct a series in which any number of coefficients are zero, this is not a fool fool-proof way to estimate the error. Except for (nearly) even or odd functions functions, which have series in which every other coefficient is (nearly) zero, functions functions in practice tend to have only a few (finite number of) terms that do not not behave.

##Extra code

Code for the comparison of methods:

f1 = Sin[2 x] Sin[y^3] Sin[x y];
exact = 0.19984397503487617461507951200812381171`16.;

Needs["Integration`NIntegrateUtilities`"];
ClearAll[ni];
ni[{"ClenshawCurtisRule" | "GaussKronrod", "Points" -> {__}}] := "";
ni[meth_] := {"IntegralEstimate", "IntegralEstimate" - exact,
      "Evaluations", "Timing"} /.
     NIntegrateProfile@NIntegrate[f1, {x, 0, 2}, {y, 0, 3},
       Method -> meth] /.
    Plus[InputForm[a_?NumericQ], b_?NumericQ] :> a + b // Column;

$rules = {Automatic; "GaussKronrod", "ClenshawCurtisRule", chebRule};
tbl = Table[ni[{rule, pts}],
   {pts, {"Points" -> Automatic, "Points" -> 9, "Points" -> 17,
     "Points" -> {9, 17}}},
   {rule, $rules}];
Join[{{"Points\\Method", SpanFromLeft, Sequence @@ $rules}},
  Transpose@
   Join[{{"Auto.", 9, 17, {9, 17}},
     Table[Column[{"Est", "Error", "Evals", "Time"}], {4}]},
    Transpose[tbl]]
  ] // Grid[#, Dividers -> All, BaseStyle -> Smaller] &

Here is another type of integration rule. It has a customized error estimator, which was my main interest. In fact, the rule below computes an integral of arbitrary dimension, using its own internal formula for the integral as well.

A rule of the type NIintegrate`GeneralRule[{abscissas, weights, errweights}] computes the integral and error estimates via dot products in more or less the following way:

There is a level above this. Recall that in adaptive integration NIntegrate subdivides regions (intervals in 1D), and applies the integration rule to each subregion. If the error is too great in a subregion, it divides again. When  NIntegrate applies a GeneralRule to a subregion of integration, the result returned to NIntegrate has the form

A GeneralRule is always 1D, so the axis is always 1. In multidimensional rules the number will indicate along which axis the region is to be split in half, if the error is too great. Ultimately, the triple above is what an integration rule needs to return when it is called by NIntegrate.

A rule, let's call it chebRule as we will below, can be called by NIntegrate in at least two ways, one of which I have figured out. The call has the form

where chebRule[data___] is the structure returned by  NIntegrate`InitializeIntegrationRule and region is a  NIntegrate`IntegrationRegion[] that contains data (including the integrand) for computing the integral over the region. An ``NIntegrateIntegrationRegion[] is the argument #1 in the option  IntegrationMonitor has many methods. We will need two:

which give the boundaries of the region and a function for evaluating the integrand, respectively. You evaluate the function on the rule's sampling points taken from the interval {0, 1}. The method takes care of the rescaling for you. All you need to do inside  chebRule[data___]["ApproximateIntegral"[region]] is calculate the three things above, the integral, error, and, if implementing a multidimensional rule, which axis to subdivide next.

The initialization code processes the options, of which currently there is only one, and gets the Clenshaw-Curtis sampling points for use in the rule. The only option is "Points", which works like the  "ClenshawCurtisRule" option and generates 2 n + 1 points for a setting of n. Unlike  "ClenshawCurtisRule", it will take a list of point settings for multidimensional integrals.

ClearAll[chebRule];

Options[chebRule] = {"Points" -> Automatic};
chebRuleProperties = Part[Options[chebRule], All, 1];
chebRule::dim = "The value of option `` should be a list of length ``";

(* initialization of rule *)
chebRule /: NIntegrate`InitializeIntegrationRule[
   chebRule, nfs_, ranges_, ruleOpts_, allOpts_] := 

 Module[{absc, points, minpts = 3, dim, wprec, t},
  
  wprec = WorkingPrecision /. allOpts;
  t = NIntegrate`GetMethodOptionValues[chebRule, chebRuleProperties, ruleOpts];
  If[t === $Failed, Return[$Failed]];
  {points} = t;
  points = points /. Automatic -> 5;
  dim = Length@ranges;
  If[! AllTrue[Flatten@{points}, # >= minpts &],
   Message[NIntegrate::mintmin, First@FilterRules[ruleOpts, "Points"], minpts];
   Return[$Failed]
   ];
  
  (* "Points" -> list of length dim *)
  If[ListQ@points && Length@points != dim,
   Message[chebRule::dim, First@FilterRules[ruleOpts, "Points"], dim];
   Return[$Failed]
   ];

  If[ListQ@points,
   absc = First@NIntegrate`ClenshawCurtisRuleData[#, wprec] & /@ points,
   absc = First@NIntegrate`ClenshawCurtisRuleData[points, wprec];
   absc = Table[absc, {dim}]
   ];
  
  chebRule[absc]
  ]

Here is the integration code. First there is the error estimator. It comes from the convergence theory for Chebyshev series (see Boyd, reference below). What really matters is that it computes a number that estimates the error of the quadrature to pass back to NIntegrate. The code below is used to come up with an error for each axis (from the iterated integral), which is then used in the main integrator below to determine how the subregion should be divided next.

(* call: self["localError"[coeff]] --
 * estimates error of Chebyshev series `coeff`,
 * provided truncated series is long enough *)
chebRule[_]["localError"[coeff_]] := 
  With[{e2 = Total[Abs@coeff[[-2 ;;]]], e4 = Total[Abs@coeff[[-4 ;;]]]},
   If[Length@coeff > 8 && e4 != 0,
    1.21 e2^2/e4, (* extrapolate error *)
    e2]           (* use last two as upper bound *)
   ];

(* call: self["globalError"[error[[n]],Times@@widths[[;;n]],n]] -- 
 * estimates "global" error of integrating along axis `n`
 * by integrating local error bound along axis `(n-1)`
 * and multiplying by the other dimensions (`volume`) *)
chebRule[absc_]["globalError"[errors_, volume_, dim_]] := 
   Module[{error = errors},
   If[dim > 1,
    (* integrate errors along next axis up (trapezoidal rule) *)
    error = Partition[Flatten[error], Length@absc[[dim - 1]]];
    error = volume * 
      error.(PadRight[#, Length@absc[[dim - 1]], 0.] + 
           PadLeft[#, Length@absc[[dim - 1]], 0.] &@ Differences@absc[[dim - 1]]);
    error = Max@error/2,
    (* top axis: volume == length of interval *)
    error = volume*Max[error]
    ]
   ];

The computation of the Chebyshev series follows Boyd, which I first encountered  here. Finding the integral from a Chebyshev series is  well-known and easy to derive.

chebRule[absc_]["Abscissas"[]] := absc;

(* 
 * Main integrator
 *)
(self : chebRule[___])["ApproximateIntegral"[region_]] := 
  Module[{absc, ranges, dim, fvals, coeff, int, widths, error, n},
   
   absc = self@ "Abscissas"[];
   ranges = region@ "Boundaries";
   dim = Length@ ranges;
   widths = Flatten[Differences /@ ranges];
   error = Table[{}, {dim}];
   
   fvals = Outer[region["EvaluateTransformedIntegrand"[{##}]] &, 
     Sequence @@ absc];
   
   n = dim; (* convenient iterator with Nest[] *)
   int = Nest[
     (* replace each {-2} level list of function/integral values by its integral *)
     Function[{vals},
      (* first compute Chebyshev series and approximation error *)
      coeff = Map[
        (* compute cosine transforms on bottom level *)
        (coeff = Sqrt[2/(Length@# - 1)] * FourierDCT[Reverse@#, 1]/widths[[n]]; 
          coeff[[{1, -1}]] /= 2;
          error[[n]] = {error[[n]], self["localError"[coeff]]};
          coeff) &,
        vals, {-2}];
 
      (* estimate an error for the whole level (axis n) *)
      error[[n]] = self["globalError"[error[[n]], Times @@ widths[[;; n]], n]];
      
      (* caveat: decrement n one step too early *)
      n--;
      (* integration rule for a Chebyshev series *)
      Map[
       Total[widths[[n + 1]]/(1 - Range[0, Length@# - 1, 2]^2) #[[1 ;; ;; 2]]] &,
       coeff,
       {-2}]
      ],
     fvals,
     dim];

   (* define  dbPrint = Print  for verbose output *)
   dbPrint[ranges -> {int, error, First@Ordering[error, -1]}]; 
   {int, Max@error, First@Ordering[error, -1]}  (* integral, error, axis *)
   ];

##Examples (TBD)

Here you can see the recursive subdivision and the convergence error estimates. If you follow carefully, you can see how the error and the axis determine the subdivisions. (There are three iterations; they can be grouped as the first printed rule, the next two, and the last four.)

Block[{dbPrint = Print},
 Reap@NIntegrate[Cos[7 x] Sin[5 y], {x, 0, 2}, {y, 0, 3},
   Method -> {chebRule, "Points" -> 9}, 
   IntegrationMonitor :> ((Sow@Total@Through[#1@"Error"]) &)]
 ]
First@% - Integrate[Cos[7 x] Sin[5 y], {x, 0, 2}, {y, 0, 3}]

Mathematica graphics

More to come...

The error weights in a GeneralRule are typically the difference of the weights of two related integration rules which is known to approximate (or at least bound) the error. In my case, I wanted to compute the error in a different way, basically for exploring my own interests and not to create a better integrator. From Anton's examples and a little spelunking, I seem to have figured how to do this. Since it is an answer to this question and it extends the (currently only) other answer, I thought I should share it. I doubt my code is bullet-proof. It can perform quite well; it can also do just okay; and I haven't tested enough pathological cases to find one where it breaks, but I haven't been too cruel to it, either.

The rule below is closely related to the Clenshaw-Curtis rule, which itself follows the form of a GeneralRule. Instead of the usual error estimate, I wanted to estimate the error from the Chebyshev series of integrand over the integration region. If the function is analytic in a (complex) neighborhood of the interval of integration, then the coefficients converge to zero geometrically and one can estimate the error of approximation from the tail of such a truncated series.  (See, for instance, Boyd, Chebyshev and Fourier Spectral Methods, Dover, New York, 2001, ch. 2). Since one can construct a series in which any number of coefficients are zero, this is not a fool-proof way to estimate the error. Except for (nearly) even or odd functions, which have series in which every other coefficient is (nearly) zero, functions in practice tend to have only a few (finite number of) terms that do not behave.

Here is another type of integration rule. It has a customized error estimator, incorporating which was my main interest. In fact, the rule below computes an integral of arbitrary dimension, using its own internal formula for the integral as well.

A rule of the type NIintegrate`GeneralRule[{abscissas, weights, errweights}] computes the integral and error estimates via dot products in more or less the following way:

There is a level above this. Recall that in adaptive integration NIntegrate subdivides regions (intervals in 1D), and applies the integration rule to each subregion. If the error is too great in a subregion, it divides again. When  NIntegrate applies a GeneralRule to a subregion of integration, the result returned to NIntegrate has the form

A GeneralRule is always 1D, so the axis is always 1. In multidimensional rules the number will indicate along which axis the region is to be split in half, if the error is too great. Ultimately, the triple above is what an integration rule needs to return when it is called by NIntegrate.

A rule, let's call it chebRule as we will below, can be called by NIntegrate in at least two ways, one of which I have figured out. The call has the form

where chebRule[data___] is the structure returned by  NIntegrate`InitializeIntegrationRule and region is a  NIntegrate`IntegrationRegion[] that contains data (including the integrand) for computing the integral over the region. An ``NIntegrateIntegrationRegion[] is the argument #1 in the option  IntegrationMonitor has many methods. We will need two:

which give the boundaries of the region and a function for evaluating the integrand, respectively. You evaluate the function on the rule's sampling points taken from the interval {0, 1}. The method takes care of the rescaling for you. All you need to do inside  chebRule[data___]["ApproximateIntegral"[region]] is calculate the three things above, the integral, error, and, if implementing a multidimensional rule, which axis to subdivide next.

The initialization code processes the options, of which currently there is only one, and gets the Clenshaw-Curtis sampling points for use in the rule. The only option is "Points", which works like the  "ClenshawCurtisRule" option and generates 2 n - 1 points for a setting of n. Unlike  "ClenshawCurtisRule", it will take a list of point settings for multidimensional integrals.

ClearAll[chebRule];

Options[chebRule] = {"Points" -> Automatic};
chebRuleProperties = Part[Options[chebRule], All, 1];
chebRule::dim = "The value of option `` should be a list of length ``";

(* initialization of rule *)
chebRule /: NIntegrate`InitializeIntegrationRule[
   chebRule, nfs_, ranges_, ruleOpts_, allOpts_] :=

 Module[{absc, points, minpts = 3, dim, wprec, t},

  wprec = WorkingPrecision /. allOpts;
  t = NIntegrate`GetMethodOptionValues[chebRule, chebRuleProperties, ruleOpts];
  If[t === $Failed, Return[$Failed]];
  {points} = t;
  points = points /. Automatic -> 5;
  dim = Length@ranges;
  If[! AllTrue[Flatten@{points}, # >= minpts &],
   Message[NIntegrate::mintmin, First@FilterRules[ruleOpts, "Points"], minpts];
   Return[$Failed]
   ];

  (* "Points" -> list of length dim *)
  If[ListQ@points && Length@points != dim,
   Message[chebRule::dim, First@FilterRules[ruleOpts, "Points"], dim];
   Return[$Failed]
   ];
  (* absc = list of ascissas for each dimension *)
  If[ListQ@points,
   absc = First@NIntegrate`ClenshawCurtisRuleData[#, wprec] & /@ points,
   absc = First@NIntegrate`ClenshawCurtisRuleData[points, wprec];
   absc = Table[absc, {dim}]
   ];

  chebRule[absc]
  ]

Here is the integration code. First there is the error estimator. It comes from the convergence theory for Chebyshev series (see Boyd, reference below). What really matters is that it computes a number that estimates the error of the quadrature to pass back to NIntegrate. The code below is used to come up with an error for each axis (from the iterated integral), which is then used in the main integrator below to determine how the subregion should be divided next.

(* self["localError"[coeff]] --
 * estimates error of Chebyshev series `coeff`,
 * provided truncated series is long enough *)
chebLE = Compile[{{coeff, _Real, 1}},
   With[{e2 = Total[Abs@coeff[[{-2, -1}]]], e4 = Total[Abs@coeff[[{-4, -3, -2, -1}]]]},
    With[{r2 = e2/e4},
     If[Length@coeff > 8 && e4 != 0 && r2 < 1,
      1.21 e2*r2/(1 - Sqrt[r2]), (* extrapolate error *)
      e2]                        (* use last two terms as upper bound *)
     ]]
   ];
(* called many times, MachinePrecision OK(?), hence compiled *)
chebRule[_]["localError"[coeff_]] := chebLE[coeff];

(* self["globalError"[error[[n]],Times@@widths[[;;n]],n]] --
 * estimates "global" error of integrating along axis `n`
 * by integrating local error bound along axis `(n-1)`
 * and multiplying by the other dimensions (`volume`) *)
chebGE = Compile[{{errors, _Real, 1}, {prevabsc, _Real, 1}, {volume, _Real}},
   Module[{error, ints},
    (* integrate errors along next axis up (trapezoidal rule) *)
    error = Partition[Flatten[errors], Length@prevabsc];
    ints =
     error.(Join[{First@#}, Rest@# + Most@#, {Last@#}] &@
        Differences@prevabsc);
    0.5 volume*Max@ints (*
    max integral will be the error bound *)
    ]
   ];
chebRule[absc_]["globalError"[errors_, volume_, 1]] :=  volume*Max[errors];
(* compiling won't help much, except maybe in high dimensional integrals *)
chebRule[absc_]["globalError"[errors_, volume_, axis_]] :=
  chebGE[errors, absc[[axis - 1]], volume];

The computation of the Chebyshev series follows Boyd, which I first encountered  here. Finding the integral from a Chebyshev series is  well-known and easy to derive.

chebRule[absc_]["Abscissas"[]] := absc;

(* Subroutines for computing and integrating Chebyshev series *)
chebRule[_]["series"[vals_, width_]] := Module[{coeff},
   coeff =
    Sqrt[2/(Length@vals - 1)]*FourierDCT[Reverse@vals, 1]/width;
   coeff[[{1, -1}]] /= 2;
   coeff
   ];
chebRule[_]["integrate"[coeff_, width_]] :=
  Map[     (* integration rule for a Chebyshev series *)
   Total[width/(1 - Range[0, Length@# - 1, 2]^2)*#[[1 ;; ;; 2]]] &,
   coeff,
   {-2}];


(* 
 * Main integrator  
 *)
(self : chebRule[___])["ApproximateIntegral"[region_]] :=
  Module[{absc, ranges, dim, fvals, coeff, integral, widths, error, axis},

   absc = self@"Abscissas"[];
   ranges = region@"Boundaries";
   dim = Length@ranges;
   widths = Flatten[Differences /@ ranges];
   error = Table[{}, {dim}];

   fvals =  (* evaluate integrand *)
    Outer[region["EvaluateTransformedIntegrand"[{##}]] &,
     Sequence @@ absc];

   axis = dim; (* convenient iterator with Nest[] *)
   integral = Nest[
     (* replaces each bottom level list of function/integral values by its integral *)
     Function[{vals},
      (*first compute Chebyshev series *)
      coeff = Map[self["series"[#, widths[[axis]]]] &, vals, {-2}];
 
      (* estimate the errors *)
      error[[axis]] =  (* first of the Chebyshev approximation *)
          Map[self["localError"[#]] &, coeff, {-2}];
      error[[axis]] =  (* next for the whole level (axis n) *)
       self["globalError"[error[[axis]], Times @@ widths[[;; axis]],
         axis]];

      (* caveat: decrement  axis  one step too early *)
      axis--;
      Map[self["integrate"[#, widths[[axis + 1]]]] &, coeff, {-2}]
      ],
     fvals,
     dim];

   axis = First@Ordering[error, -1];
   dbPrint[ranges -> {integral, error, axis}];
   error =  (* convert from MachinePrecision to working precision *)
     SetPrecision[Max@error, region@"WorkingPrecision"];
   {integral, error, axis}
   ];

##Examples

Here you can see the recursive subdivision and the convergence error estimates. If you follow carefully, you can see how the error and the axis determine the subdivisions. (There are three iterations; they can be grouped as the first printed rule, the next two, and the last four.)

Block[{dbPrint = Print},
 Reap@NIntegrate[Cos[7 x] Sin[5 y], {x, 0, 2}, {y, 0, 3},
   Method -> {chebRule, "Points" -> 9},
   IntegrationMonitor :> ((Sow@Total@Through[#1@"Error"]) &)]
 ]
First@% - Integrate[Cos[7 x] Sin[5 y], {x, 0, 2}, {y, 0, 3}]

Mathematica graphics

A relatively innocuous integrand is

f1 = Sin[2 x] Sin[y^3] Sin[x y];

We will integrate it with calls of the form

NIntegrate[f1, {x, 0, 2}, {y, 0, 3}, Method -> ...]

with Method settings like

Method -> chebRule
Method -> {chebRule, "Points" -> 9}
Method -> {chebRule, "Points" -> 17}
Method -> {chebRule, "Points" -> {9, 17}}

We compare it with similar calls to the rules "GaussKronrod"``, ``"ClenshawCurtisRule", except for the last. By comparison the exact value to 16 digits is

exact = 0.1998439750348762

and the default "MultidimensionalRule" takes 2.38428 seconds, 8325 function evaluations and has an error of 3.75823*10^-10.

Mathematica graphics
Comparison of methods (code at end)

First of all, the error is closer to the goal, which be around 10^-8 for this integral. The function f1 evaluates quickly so one can see that chebRule appears to be slower per function evaluation than "ClenshawCurtisRule", but it seems also to use fewer function calls. The larger error and fewer calls are related to the difference in the error estimators of the two methods. If the integrand is expensive to calculate, then chebRule may have an advantage.

The error weights in a GeneralRule are typically the difference of the weights of two related integration rules which is known to approximate (or at least bound) the error. In my case, I wanted to compute the error in a different way, basically for exploring my own interests and not to create a better integrator. From Anton's examples and a little spelunking, I seem to have figured how to do this. Since it is an answer to this question and it extends the (currently only) other answer, I thought I should share it. I doubt my code is bullet-proof. It can perform quite well; it can also do just okay; and I haven't tested enough pathological cases to find one where it breaks, but I haven't been too cruel to it, either.

The rule below is closely related to the Clenshaw-Curtis rule, which itself follows the form of a GeneralRule. Instead of the usual error estimate, I wanted to estimate the error from the Chebyshev series of integrand over the integration region. If the function is analytic in a (complex) neighborhood of the interval of integration, then the coefficients converge to zero geometrically and one can estimate the error of approximation from the tail of such a truncated series.  (See, for instance, Boyd, Chebyshev and Fourier Spectral Methods, Dover, New York, 2001, ch. 2). Since one can construct a series in which any number of coefficients are zero, this is not a fool-proof way to estimate the error. Except for (nearly) even or odd functions, which have series in which every other coefficient is (nearly) zero, functions in practice tend to have only a few (finite number of) terms that do not behave.

##Extra code

Code for the comparison of methods:

f1 = Sin[2 x] Sin[y^3] Sin[x y];
exact = 0.19984397503487617461507951200812381171`16.;

Needs["Integration`NIntegrateUtilities`"];
ClearAll[ni];
ni[{"ClenshawCurtisRule" | "GaussKronrod", "Points" -> {__}}] := "";
ni[meth_] := {"IntegralEstimate", "IntegralEstimate" - exact,
      "Evaluations", "Timing"} /.
     NIntegrateProfile@NIntegrate[f1, {x, 0, 2}, {y, 0, 3},
       Method -> meth] /.
    Plus[InputForm[a_?NumericQ], b_?NumericQ] :> a + b // Column;

$rules = {Automatic; "GaussKronrod", "ClenshawCurtisRule", chebRule};
tbl = Table[ni[{rule, pts}],
   {pts, {"Points" -> Automatic, "Points" -> 9, "Points" -> 17,
     "Points" -> {9, 17}}},
   {rule, $rules}];
Join[{{"Points\\Method", SpanFromLeft, Sequence @@ $rules}},
  Transpose@
   Join[{{"Auto.", 9, 17, {9, 17}},
     Table[Column[{"Est", "Error", "Evals", "Time"}], {4}]},
    Transpose[tbl]]
  ] // Grid[#, Dividers -> All, BaseStyle -> Smaller] &
Post Deleted by Michael E2
Source Link
Michael E2
  • 245k
  • 18
  • 351
  • 775
Loading