diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e0e499306..fc81554d6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,20 +38,15 @@ jobs: - name: Gusto unit-tests run: | . /home/firedrake/firedrake/bin/activate - python $(which firedrake-clean) + which firedrake-clean python -m pytest -n 12 -v unit-tests - name: Gusto integration-tests run: | . /home/firedrake/firedrake/bin/activate - python $(which firedrake-clean) + firedrake-clean python -m pytest -n 12 -v integration-tests - name: Gusto examples run: | . /home/firedrake/firedrake/bin/activate - python $(which firedrake-clean) + firedrake-clean python -m pytest -n 12 -v examples - - name: Lint - if: ${{ always() }} - run: | - . /home/firedrake/firedrake/bin/activate - make lint diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 000000000..e6bec0821 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,57 @@ +name: Check docs build cleanly + +on: + # Run on pushes to master + push: + branches: + - master + # And all pull requests + pull_request: + +concurrency: + # Cancels jobs running if new commits are pushed + group: > + ${{ github.workflow }}- + ${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + build_docs: + name: Run doc build + # The type of runner that the job will run on + runs-on: ubuntu-latest + # The docker container to use. + container: + image: firedrakeproject/firedrake-docdeps:latest + options: --user root + volumes: + - ${{ github.workspace }}:/home/firedrake/output + # Steps represent a sequence of tasks that will be executed as + # part of the jobs + steps: + - uses: actions/checkout@v3 + - name: Install checkedout Gusto + run: | + . /home/firedrake/firedrake/bin/activate + python -m pip install -e . + - name: Install Read the Docs theme + run: | + . /home/firedrake/firedrake/bin/activate + python -m pip install sphinx_rtd_theme + - name: Check documentation links + if: ${{ github.ref == 'refs/heads/master' }} + run: | + . /home/firedrake/firedrake/bin/activate + cd docs + make linkcheck + - name: Build docs + run: | + . /home/firedrake/firedrake/bin/activate + cd docs + make html + - name: Upload artifact + uses: actions/upload-pages-artifact@v1 + with: + name: github-pages + path: /__w/gusto/gusto/docs/build/html + retention-days: 1 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 000000000..8c29106e4 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,40 @@ +name: Run lint + +on: + # Push to master or PR + push: + branches: + - master + pull_request: + +jobs: + linter: + name: "Run linter" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Setup python + uses: actions/setup-python@v4 + with: + python-version: 3.11 + - name: Setup flake8 annotations + uses: rbialon/flake8-annotations@v1 + - name: Install linter + run: | + pip install flake8 pylint + - name: Lint codebase + run: | + make lint GITHUB_ACTIONS_FORMATTING=1 + actionlint: + name: "Lint Github actions YAML files" + # There's a way to add error formatting so GH actions adds messages to code, + # but I can't work out the right number of quotes to get it to work + # https://github.com/rhysd/actionlint/blob/main/docs/usage.md + # #example-error-annotation-on-github-actions + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Check workflow files + uses: docker://rhysd/actionlint:latest + with: + args: -color diff --git a/Makefile b/Makefile index f077972cb..4ffd2902f 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,22 @@ +# Adds file annotations to Github Actions (only useful on CI) +GITHUB_ACTIONS_FORMATTING=0 +ifeq ($(GITHUB_ACTIONS_FORMATTING), 1) + FLAKE8_FORMAT=--format='::error file=%(path)s,line=%(row)d,col=%(col)d,title=%(code)s::%(path)s:%(row)d:%(col)d: %(code)s %(text)s' +else + FLAKE8_FORMAT= +endif + lint: @echo " Linting gusto codebase" - @python3 -m flake8 gusto + @python3 -m flake8 $(FLAKE8_FORMAT) gusto @echo " Linting gusto examples" - @python3 -m flake8 examples + @python3 -m flake8 $(FLAKE8_FORMAT) examples @echo " Linting gusto unit-tests" - @python3 -m flake8 unit-tests + @python3 -m flake8 $(FLAKE8_FORMAT) unit-tests @echo " Linting gusto integration-tests" - @python3 -m flake8 integration-tests + @python3 -m flake8 $(FLAKE8_FORMAT) integration-tests @echo " Linting gusto plotting scripts" - @python3 -m flake8 plotting + @python3 -m flake8 $(FLAKE8_FORMAT) plotting test: @echo " Running all tests" @@ -24,4 +32,4 @@ integration_test: example: @echo " Running all examples" - @python3 -m pytest examples $(PYTEST_ARGS) \ No newline at end of file + @python3 -m pytest examples $(PYTEST_ARGS) diff --git a/docs/source/conf.py b/docs/source/conf.py index 642702049..28a7e16a4 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -254,5 +254,5 @@ 'pyop2': ('https://op2.github.io/PyOP2', None), 'ufl': ('https://fenics.readthedocs.io/projects/ufl/en/latest/', None), 'h5py': ('http://docs.h5py.org/en/latest/', None), - 'python':('https://docs.python.org/2.7/', None), + 'python':('https://docs.python.org/', None), } diff --git a/gusto/__init__.py b/gusto/__init__.py index c932d6811..d1a210f01 100644 --- a/gusto/__init__.py +++ b/gusto/__init__.py @@ -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 diff --git a/gusto/numerical_integrator.py b/gusto/numerical_integrator.py new file mode 100644 index 000000000..d04ab2f2d --- /dev/null +++ b/gusto/numerical_integrator.py @@ -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) diff --git a/unit-tests/test_numerical_integrator.py b/unit-tests/test_numerical_integrator.py new file mode 100644 index 000000000..53d0b1131 --- /dev/null +++ b/unit-tests/test_numerical_integrator.py @@ -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'