Peano
Loading...
Searching...
No Matches
Adding a numerical scheme

A numerical scheme is technically a generator which takes a solver description and produces a task graph of activities.

Therefore, it is typically implemented through a single method

def construct_output(self, exahype2_project, peano_project, time_step):
...

where time_step is the task graph of the surrounding project's time stepping phase. Implementing a numerical scheme therefore means populating this graph with peano4.api.taskgraph.Task instances and the dependencies between them.

Task graph structurex

Below is a simple example for such a task graph - the time step task graph exahype2.api.fv.RusanovWithExplicitEuler builds for a Finite Volume solver, as rendered by Graph.visualise():

The task graph describes what happens during a time step, and in which order. It represents the logical activities that have to be completed per time step. The code below sets this graph up:

reconstruction_task = peano4.api.taskgraph.Task(name=f"{self._solver.name}_Reconstruct and apply BCs", entity=peano4.api.taskgraph.Task.GridEntity.CELL)
update_cell_task = peano4.api.taskgraph.Task(name=f"{self._solver.name}_Update cell", entity=peano4.api.taskgraph.Task.GridEntity.CELL)
project_on_faces_task = peano4.api.taskgraph.Task(name=f"{self._solver.name}_Project on faces", entity=peano4.api.taskgraph.Task.GridEntity.CELL)
compute_cfl_condition_task = peano4.api.taskgraph.Task(name=f"{self._solver.name}_Compute global CFL condition", entity=peano4.api.taskgraph.Task.GridEntity.CELL)
update_cell_task.add_dependency(reconstruction_task)
project_on_faces_task.add_dependency(update_cell_task)
compute_cfl_condition_task.add_dependency(update_cell_task)
time_step.add_task(reconstruction_task)
time_step.add_task(update_cell_task)
time_step.add_task(project_on_faces_task)
time_step.add_task(compute_cfl_condition_task)
The task graph abstraction allows users to formalise their solver steps over small task graphs which ...
Definition __init__.py:1

The CFL condition has a task of its own: The update task's action set (exahype2.api.fv.actionsets.UpdateCell) stores the maximum eigenvalue of every patch it updates in the cell. The task "Compute global CFL condition" (exahype2.api.fv.actionsets.ComputeGlobalCFLCondition) then reduces these values and derives the next global time step size from them. It only depends on the update, i.e. it can run in parallel to the projection onto the faces.

Every Task is tagged with the kind of mesh entity it works on (Task.GridEntity.VERTEX/FACE/CELL) and the tasks it depends on. A new numerical scheme typically follows the same pattern: create one Task per algorithmic sub-step, wire up add_dependency() calls to express the required order, and add every task to the phase's Graph via add_task(). While developing a scheme, Graph.visualise() renders the resulting graph with matplotlib to sanity-check the dependency structure.

Schemes with several stages

A scheme with several stages per time step, such as a Runge-Kutta scheme, adds one chain of tasks per stage to the same graph. The fourth-order Finite Differences exahype2.api.rkfd4.CentralDifferencesWithRungeKutta.CentralDifferencesWithRungeKutta are an example: each stage reconstructs its input with a halo, evaluates the right-hand side, and projects the input of the next stage onto the faces, from where the next stage's reconstruction reads it. The scheme does not have to say that each stage needs a mesh traversal of its own. It follows from the data the tasks use: a task which reads face data another task writes cannot run in the same traversal, and the task graph compiler hence starts a new one (peano4.api.taskgraph.compiler.FuseSteps).

Tasks within one graph need unique names, so a scheme which adds the same kind of task once per stage has to number them. The Runge-Kutta tools which do not depend on the discretisation in space, i.e. the Butcher tableaus and the linear combination of the estimates, reside in exahype2.api.rungekutta, so further Runge-Kutta schemes can reuse them.

Adding the actual computations

The actual computations are now attached to these tasks: The tasks represent a logical order, but each then has an action set which does the actual calculations. While the tasks define the order, the action sets define what is to be computed.

From hereon

The task graph created by this solver will be taken by the project and will be mangled with all the other task graphs of solvers that are tied to the project. After all, ExaHyPE supports the composition of many solvers. This concatenated super graph is then handed over to a task graph compiler in Peano.

Depending on what compiler is chosen, the task graph might be linearised into many single grid sweeps, where each sweep runs only one task from the graph. However, there are also compiler variants which quite aggressively parallelise over a task graph, fuse individual steps, or even map some of the action sets into real tasks in a TBB or OpenMP sense of their own.

How this all works is technically speaking not a properly of ExaHyPE, but of Peano. Therefore, all the documentation of these aspects is tied to peano4.api.taskgraph, while the bigger picture is provided by Peano 4.

Best practices and conventions

  • Keep the task graph as atomic as possible. That is, don't create tasks that host many action sets. Only group the action sets together that really need to be executed in a single task and have a similar access pattern to the mesh. For example, it is not a good idea to combine a global (time step or eigenvalue) reduction with a cell update task. Keep tasks separate and leave it to the task compiler to optimise things.