Skip to content

Commit

Permalink
Merge pull request #391 from firedrakeproject/Numericalintegrator
Browse files Browse the repository at this point in the history
PR #391: add numerical integrator
  • Loading branch information
tommbendall authored Jul 27, 2023
2 parents d0eee7d + e8ec956 commit 5beaa9e
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 0 deletions.
1 change: 1 addition & 0 deletions gusto/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from gusto.limiters import * # noqa
from gusto.linear_solvers import * # noqa
from gusto.meshes import * # noqa
from gusto.numerical_integrator import * # noqa
from gusto.physics import * # noqa
from gusto.preconditioners import * # noqa
from gusto.recovery import * # noqa
Expand Down
63 changes: 63 additions & 0 deletions gusto/numerical_integrator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import numpy as np


class NumericalIntegral(object):
"""
A class for numerically evaluating and tabulating some 1D integral.
Args:
lower_bound(float): lower bound of integral
upper_bound(float): upper bound of integral
num_points(float): number of points to tabulate integral at
"""
def __init__(self, lower_bound, upper_bound, num_points=500):

# if upper_bound <= lower_bound:
# raise ValueError('lower_bound must be lower than upper_bound')
self.x = np.linspace(lower_bound, upper_bound, num_points)
self.x_double = np.linspace(lower_bound, upper_bound, 2*num_points-1)
self.lower_bound = lower_bound
self.upper_bound = upper_bound
self.num_points = num_points
self.tabulated = False

def tabulate(self, expression):
"""
Tabulate some integral expression using Simpson's rule.
Args:
expression (func): a function representing the integrand to be
evaluated. should take a numpy array as an argument.
"""

self.cumulative = np.zeros_like(self.x)
self.interval_areas = np.zeros(len(self.x)-1)
# Evaluate expression in advance to make use of numpy optimisation
# We evaluate at the tabulation points and the midpoints of the intervals
f = expression(self.x_double)

# Just do Simpson's rule for evaluating area of each interval
self.interval_areas = ((self.x[1:] - self.x[:-1]) / 6.0
* (f[2::2] + 4.0 * f[1::2] + f[:-1:2]))

# Add the interval areas together to create cumulative integral
for i in range(self.num_points - 1):
self.cumulative[i+1] = self.cumulative[i] + self.interval_areas[i]

self.tabulated = True

def evaluate_at(self, points):
"""
Evaluates the integral at some point using linear interpolation.
Args:
points (float or iter) the point value, or array of point values to
evaluate the integral at.
Return:
returns the numerical approximation of the integral from lower
bound to point(s)
"""
# Do linear interpolation from tabulated values
if not self.tabulated:
raise RuntimeError(
'Integral must be tabulated before we can evaluate it at a point')

return np.interp(points, self.x, self.cumulative)
34 changes: 34 additions & 0 deletions unit-tests/test_numerical_integrator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""
Tests the numerical integrator.
"""
from gusto import NumericalIntegral
from numpy import sin, pi
import pytest


def quadratic(x):
return x**2


def sine(x):
return sin(x)


@pytest.mark.parametrize("integrand_name", ["quadratic", "sine"])
def test_numerical_integrator(integrand_name):
if integrand_name == "quadratic":
integrand = quadratic
upperbound = 3
answer = 9
elif integrand_name == "sine":
integrand = sine
upperbound = pi
answer = 2
else:
raise ValueError(f'{integrand_name} integrand not recognised')
numerical_integral = NumericalIntegral(0, upperbound)
numerical_integral.tabulate(integrand)
area = numerical_integral.evaluate_at(upperbound)
err_tol = 1e-10
assert abs(area-answer) < err_tol, \
f'numerical integrator is incorrect for {integrand_name} function'

0 comments on commit 5beaa9e

Please sign in to comment.