2

I've written some code that's meant to integrate a function numerically using the trapezoidal rule. It works, but the answer it produces has a wrong sign. Why might that be?

The code is:

integration :: (Double -> Double) -> Double -> Double -> Double
integration f a b = h * (f a + f b + partial_sum)
    where 
        h = (b - a) / 1000 
        most_parts  = map f (points (1000-1) h) 
        partial_sum = sum most_parts

points  :: Double -> Double -> [Double]
points x1 x2 
    | x1 <= 0 = []
    | otherwise = (x1*x2) : points (x1-1) x2

Trapezoidal rule

The code is probably inelegant, but I'm only a student of Haskell and would like to deal with the current problem first and coding style matters after that.

Chris Stryczynski
  • 30,145
  • 48
  • 175
  • 286
Chiffa
  • 1,486
  • 2
  • 19
  • 38
  • 3
    Clearing up the coding style problems will make it easier to see what's going on and to test pieces in isolation. – dfeuer Oct 06 '15 at 19:43
  • True. I should learn to write good code and test it. But that'd probably come later. – Chiffa Oct 06 '15 at 19:46
  • 1
    Why do you put `x1*x2` in the `points` list? That doesn't make sense to me. You could just use a range comprehension there, though float ranges are a bit... weird. See [this post of mine](http://stackoverflow.com/questions/7290438/haskell-ranges-and-floats/7296160#7296160) (I used the very example of trapezoidal integration there, to demonstrate why the strange behaviour of floats makes some sense). – leftaroundabout Oct 06 '15 at 19:51
  • 1
    Hint: missing a factor of 2. Let there be 3 points. You should have `(b-a)/4*(f_1+2f_2+f_3). Compare with your solution. – karakfa Oct 06 '15 at 19:53
  • @karakfa, that's true, but not really my fault: I used a Wikipedia article in another language, which gives a different formula. Also, I doubt that's what affects the signs. – Chiffa Oct 06 '15 at 19:58
  • @leftaroundabout: I'm just trying to make a list of points that a function'll later work with. – Chiffa Oct 06 '15 at 20:04
  • Actually, the formula I'm using gives results that are closer to actual values than the one from the english article. – Chiffa Oct 06 '15 at 20:08
  • 1
    you points seems off - for example the first point will be `999 * (b-a) / 1000` which is about `(b-a)` and the last point will be just `0` but shouln't those be from `a` to `b`? (aka: you missed a `+ a` there) - assuming the rest is ok then you will integrate from `0` to `b-a` instead of from `a` to `b`this way! – Random Dev Oct 06 '15 at 20:11
  • The order of points doesn't affect anything, if I understand the whole process right; it's the sum of their values that matters, and it doesn't depend on the order of points. – Chiffa Oct 06 '15 at 20:38
  • I am not talking about the order - see my answer (which produces reasonable results for basic functions - so chances are good it's not totally wrong ;) ) – Random Dev Oct 06 '15 at 20:40

2 Answers2

5

Note: This answer is written in literate Haskell. Save it with .lhs as extension and load it in GHCi to test the solution.

Finding the culprit

First of all, let's take a look at integration. In its current form, it contains only summation of function values f x. Even though the factors aren't correct at the moment, the overall approach is fine: you evaluate f at the grid points. However, we can use the following function to verify that there's something wrong:

ghci> integration (\x -> if x >= 10 then 1 else (-1)) 10 15
-4.985

Wait a second. x isn't even negative in [10,15]. This suggests that you use the wrong grid points.

Grid points revisited

Even though you've linked the article, let's have a look at an exemplary use of the trapezoidal rule (public domain, original file by Oleg Alexandrov):

trapezoidal rule

Although this doesn't use a uniform grid, let's suppose that the 6 grid points are equidistant with grid distance h = (b - a) / 5. What are the x coordinates of those points?

x_0 = a + 0 * h (== a)
x_1 = a + 1 * h
x_2 = a + 2 * h
x_3 = a + 3 * h
x_4 = a + 4 * h
x_5 = a + 5 * h (== b)

If we use set a = 10 and b = 15 (and therefore h = 1), we should end up with [10, 11, 12, 13, 14, 15]. Let's check your points. In this case, you would use points 5 1 and end up with [5,4,3,2,1].

And there's the error. points doesn't respect the boundary. We can easily fix this by using pointsWithOffset:

> points  :: Double -> Double -> [Double]
> points x1 x2 
>     | x1 <= 0 = []
>     | otherwise = (x1*x2) : points (x1-1) x2
>
> pointsWithOffset :: Double -> Double -> Double -> [Double]
> pointsWithOffset x1 x2 offset = map (+offset) (points x1 x2)

That way, we can still use your current points definition to generate grid points from x1 to 0 (almost). If we use integration with pointsWithOffset, we end up with

integration :: (Double -> Double) -> Double -> Double -> Double
integration f a b = h * (f a + f b + partial_sum)
    where 
        h = (b - a) / 1000 
        most_parts  = map f (pointsWithOffset (1000-1) h a)  
        partial_sum = sum most_parts

Tying up loose ends

However, this doesn't take into account that you use all inner points twice in the trapezoid rule. If we add the factors, we end up with

> integration :: (Double -> Double) -> Double -> Double -> Double
> integration f a b = 
>      h / 2 * (f a + f b + 2 * partial_sum)
>    --    ^^^              ^^^
>    where 
>        h = (b - a) / 1000 
>        most_parts  = map f (pointsWithOffset (1000-1) h a)  
>        partial_sum = sum most_parts

Which yields the correct value for our test function above.

Exercise

Your current version only supports 1000 grid points. Add an Int argument so that one can change the number of grid points:

integration :: Int -> (Double -> Double) -> Double -> Double -> Double
integration n f a b = -- ...

Furthermore, try to write points in different ways, for example go from a to b, use takeWhile and iterate, or even a list comprehension.

Zeta
  • 103,620
  • 13
  • 194
  • 236
  • A good technique for avoiding weird fence-post issues when counting in floating point is `map (arbitraryTransformation . fromIntegral)`. Of course, this all changes if you use variable-width intervals. – dfeuer Oct 06 '15 at 21:16
  • That is a great answer. I regret being able to give it only 1 point. Also, should I look into literal Haskell. As a student, I don't yet know what that is. – Chiffa Oct 06 '15 at 21:17
  • @Chiffa: You know how you usually use `--` or `{- ... -}` for comments? *Literate* programming basically flips everything around. Everything is interpreted as a comment, unless you use special syntax (in this case `> ...`, which is also called Bird style). – Zeta Oct 06 '15 at 21:21
  • My function is a result of an exercise. Surely a real-life, well-designed function should have an argument for the number of points. As for `iterate`, I'm yet to learn about it. – Chiffa Oct 06 '15 at 21:21
  • @Chiffa: Remark: I use literate Haskell only on StackOverflow, so that one can copy and paste the whole answer into an editor/file. I usually don't use it for my own projects. – Zeta Oct 06 '15 at 21:24
2

Yes it indeed was the points plus you had some factors wrong (the inner points are multiplied by 2) - this is the fixed version of your code:

integration :: (Double -> Double) -> Double -> Double -> Double
integration f a b = h * (f a + f b + innerSum) / 2
    where 
        h = (b - a) / 1000 
        innerPts  = map ((2*) . f . (a+)) (points (1000-1) h) 
        innerSum = sum innerPts

points  :: Double -> Double -> [Double]
points i x 
    | i <= 0 = []
    | otherwise = (i*x) : points (i-1) x

which gives sensible approximations (to 1000 points):

λ> integration (const 2) 1 2
2.0
λ> integration id 1 2
1.5

λ> integration (\x -> x*x) 1 2
2.3333334999999975

λ> 7/3
2.3333333333333335
Random Dev
  • 51,810
  • 9
  • 92
  • 119
  • 1
    Your `points` looks the same. I think both are shady because they count with `Double`, but I don't think the order of the point list matters for this algorithm. – dfeuer Oct 06 '15 at 20:40
  • @dfeuer: He moved `(+a)` into `integration`. – Zeta Oct 06 '15 at 20:40
  • yes ... it was easier (aka I was to lazy to add another argument to points) – Random Dev Oct 06 '15 at 20:41
  • it's really not about any order - the only thing I might have f***ed up is that maybe I choose the wrong end of the intervals here - I'm really sleepy and I would not do it this way (points) but this really should be close to the wiki article (it's not that difficult) – Random Dev Oct 06 '15 at 20:43
  • 1
    @Chiffa: Well, this is an overkill, but you could create random polynomials, integrate them, and check whether your `integration` differs too much from the correct result. – Zeta Oct 06 '15 at 21:03
  • The major aim for me is to learn to write code that would create such polynomials for me and then test other code with it. But thanks for the idea. – Chiffa Oct 06 '15 at 21:16