Uploaded image for project: 'Commons Math'
  1. Commons Math
  2. MATH-1558

MidpointIntegrator doesn't implement the midpoint method correctly

Rank to TopRank to BottomAttach filesAttach ScreenshotBulk Copy AttachmentsBulk Move AttachmentsAdd voteVotersWatch issueWatchersCreate sub-taskConvert to sub-taskLinkCloneLabelsUpdate Comment AuthorReplace String in CommentUpdate Comment VisibilityDelete Comments
    XMLWordPrintableJSON

Details

    • Bug
    • Status: Open
    • Major
    • Resolution: Unresolved
    • 3.6.1
    • None
    • None

    Description

      Hi all,

      I've been reading through the implementation of MidpointIntegrator, and I've discovered an issue with the algorithm as implemented.

      The midpoint method generates an estimate for the definite integral of f between a and b by

      • subdividing (a, b) into n intervals
      • estimating the area of each interval as a rectangle (b-a)/n wide and as tall as the midpoint of the interval - ie, ((b-a)/n) * f((a + b) / 2)
      • adding up all estimates.

      The MidpointIntegrator implementation here sticks to n = powers of 2 for this reason, stated in the comments:

       The interval is divided equally into 2^n sections rather than an arbitrary m sections because this configuration can best utilize the already computed values.

       

      Here is the problem: the midpoint method can't reuse values if you split an interval in half. It can only reuse previous values if you split the interval into 3; ie, use 3^n sections, not 2^n.

      What's actually implemented in `MidpointIntegrator` is a left Riemann sum without the leftmost point. Or, identically, a right Riemann sum without the rightmost point: https://en.wikipedia.org/wiki/Riemann_sum

      This matters because the error of a (left, right) Riemann sum is proportional to 1/n, while the error of the midpoint method is proportional to 1/n^2... a big difference.

      Explanation

      The idea behind the midpoint method's point reuse is that if you triple the number of integral slices, one of the midpoints with n slices will overlap with the n/3 slice evaluation:

      n = 1 |--------x--------|
      n = 3 |--x--|--x--|--x–-|
      

      You can incrementally update the n=1 estimate by

      • sampling the points at (1/6) and (5/6) of the way across the n=1 interval
      • adding them to the n=1 estimate
      • scaling this sum down by 3, to scale down the widths of the rectangles

      For values of n != 1, the same trick applies... just sample the 1/6, 5/6 point for every slice in the n/3 evaluation.

      What happens when you try and scale from n => 2n? The old function evaluations all fall on the boundaries between the new cells, and can't be reused:

      n = 1 |-------x-------|
      n = 2 |---x---|---x---|
      n = 4 |-x-|-x-|-x-|-x-|
      

      The implementation of "stage" in MidpointIntegrator is combining them like this:

      n = 1 |-------x-------|
      n = 2 |---x---x---x---|
      n = 4 |-x-x-x-x-x-x-x-|
      

      which is actually:

      • tripling the number of integration slices at each step, not doubling, and
      • moving the function evaluation points out of the midpoint.

      In fact, what "stage" is actually doing is calculating a left Riemann sum, just without the left-most point.

      Here are the evaluation points for a left Riemann sum for comparison:

      n = 2 x-------x--------
      n = 4 x---x---x---x----
      n = 8 x-x-x-x-x-x-x-x--
      

      Here's the code from "stage" implementing this:

      // number of new points in this stage... 2^n points at this stage, so we
      // have 2^n-1 points.
      final long np = 1L << (n - 1);
      double sum = 0;
      
      // spacing between adjacent new points
      final double spacing = diffMaxMin / np;
      
      // the first new point}}}}
      double x = min + 0.5 * spacing;}}}}
      for (long i = 0; i < np; i++) {}}}}
          sum += computeObjectiveValue(x);
          x += spacing;}}}}
      }
      // add the new sum to previously calculated result
      return 0.5 * (previousStageResult + sum * spacing);
      

      Suggested Resolution 

      To keep the exact same results, I think the only solution is to remove the incorrect incremental "stage"; or convert it to the algorithm that implements the correct incremental increase by 3, and then don't call it by default.

      Everything does work wonderfully if you expand n by a factor of 3 each time. Press discusses this trick in Numerical Recipes, section 4.4 (p136): http://phys.uri.edu/nigh/NumRec/bookfpdf/f4-4.pdf and notes that the savings you get still make it more efficient than NOT reusing function evaluations and implementing a correct scale-by-2 each time.

      Happy to provide more information if I can be helpful!

       

      Attachments

        Activity

          This comment will be Viewable by All Users Viewable by All Users
          Cancel

          People

            Unassigned Unassigned
            sritchie Sam Ritchie

            Dates

              Created:
              Updated:

              Time Tracking

                Estimated:
                Original Estimate - Not Specified
                Not Specified
                Remaining:
                Remaining Estimate - 0h
                0h
                Logged:
                Time Spent - 50m
                50m

                Slack

                  Issue deployment