|
Peano
|
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
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.
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:
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.
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.
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.
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.