Contributing code
^^^^^^^^^^^^^^^^^
Structure of the package
""""""""""""""""""""""""
The functionality of the :mod:`pde` package is split into multiple sub-packages.
The domain, together with its symmetries, periodicities, and discretizations, is
described by classes defined in :mod:`~pde.grids`.
Discretized fields are represented by classes in :mod:`~pde.fields`, which have
methods for differential operators with various boundary conditions collected
in :mod:`~pde.grids.boundaries`.
The actual pdes are collected in :mod:`~pde.pdes` and the respective solvers
are defined in :mod:`~pde.solvers`.
The actual numerical computations are done in backends, which are implemented in
:mod:`~pde.backends`.
Extending functionality
"""""""""""""""""""""""
All code is build on a modular basis, making it easy to introduce new classes
that integrate with the rest of the package. For instance, it is simple to
define a new partial differential equation by subclassing
:class:`~pde.pdes.base.PDEBase`.
Alternatively, PDEs can be defined by specifying their evolution rates using
mathematical expressions by creating instances of the class
:class:`~pde.pdes.pde.PDE`.
Moreover, new grids can be introduced by subclassing
:class:`~pde.grids.base.GridBase`.
The actual calculations are done by backends, which offer an interface for doing the
calculation details. These backends are defined in :mod:`~pde.backends` and allow
accessing their details independently. For instance, the numba-accelerated operators
can be used without any other part of the package by calling
the method :meth:`~pde.backends.numba.backend.NumbaBackend.make_operator` on the
:meth:`~pde.backends.numba.numba_backend` object.
Moreover, new operators can be associated with grids by registering them using
:meth:`numba_backend.register_operator`.
For instance, to create a new operator for the cylindrical grid one needs to
define a factory function that creates the operator. This factory function takes
an instance of :class:`~pde.grids.boundaries.axes.BoundariesList` as an argument and
returns a function that takes as an argument the actual data array for the grid.
Note that the grid itself is an attribute of
:class:`~pde.grids.boundaries.axes.BoundariesList`.
This operator would be registered with the grid by calling
:code:`numba_backend.register_operator(CylindricalSymGrid, "operator", make_operator)`,
where the arguments denote the grid class, the name of the operator, and the factory
function, respectively.
Design choices
""""""""""""""
The data layout of field classes (subclasses of
:class:`~pde.fields.base.FieldBase`) was chosen to allow for a simple
decomposition of different fields and tensor components. Consequently, the data
is laid out in memory such that spatial indices are last. For instance, the data
of a vector field ``field`` defined on a 2d Cartesian grid will have three
dimensions and can be accessed as ``field.data[vector_component, x, y]``,
where ``vector_component`` is either 0 or 1.
Coding style
""""""""""""
The coding style is enforced using `ruff `_, based on the
styles suggest by `isort `_ and
`black `_. Moreover, we use `Google Style docstrings
`_,
which might be best `learned by example
`_.
The documentation, including the docstrings, are written using `reStructuredText
`_, with examples in the
following `cheatsheet
`_.
To ensure the integrity of the code, we also try to provide many test functions,
contained in the separate sub-folder :file:`tests`.
These tests can be ran using scripts in the :file:`scripts` subfolder in the root
folder.
This folder also contain a script :file:`tests_types.sh`, which uses :mod:`mypy`
to check the consistency of the python type annotations.
We use these type annotations for additional documentation and they have also
already been useful for finding some bugs.
Finally, we have pre-commit hooks, which you should install using `pre-commit install`.
We also have some conventions that should make the package more consistent and
thus easier to use. For instance, we try to use ``properties`` instead of getter
and setter methods as often as possible.
Because we use :mod:`numba` or :mod:`torch` to speed up computations, we need to pass
around (compiled) functions regularly. The names of the methods and functions that make
such functions, i.e. that return callables, should start with 'make_*' where the
wildcard should describe the purpose of the function being created.
Running unit tests
""""""""""""""""""
The :mod:`pde` package contains several unit tests, collected in the :file:`tests`
folder in the project root. These tests ensure that basic functions work as expected,
in particular when code is changed in future versions. To run all tests, there are a
few convenience scripts in the root directory :file:`scripts`. The most basic script is
:file:`tests_run.sh`, which uses :mod:`pytest` to run the tests. Clearly, the python
package :mod:`pytest` needs to be installed. There are also additional scripts that for
instance run tests in parallel (needs the python package :mod:`pytest-xdist` installed),
measure test coverage (needs package :mod:`pytest-cov` installed), and make simple
performance measurements. Moreover, there is a script :file:`test_types.sh`, which uses
:mod:`mypy` to check the consistency of the python type annotations and there is a
script :file:`format_code.sh`, which formats the code automatically to adhere to our style.
Before committing a change to the code repository, it is good practice to run the tests,
check the type annotations, and the coding style with the scripts described above.