Peano
Loading...
Searching...
No Matches
Solver.py
Go to the documentation of this file.
1# This file is part of the ExaHyPE2 project. For conditions of distribution and
2# use, please see the copyright notice at www.peano-framework.org
3from __future__ import annotations
4
5import abc
6import re
7from typing import Callable, Dict, List, Tuple, Union
8
9import peano4.api
11
12from .PDETerm import PDETerm
13
14VariableSpecification = Union[int, Dict[str, int]]
15
16
17class Solver(abc.ABC):
18 """!
19
20 Abstract base class for all ExaHyPE 2 solvers
21
22 Every discretisation ExaHyPE 2 ships (Finite Volumes, ADER-DG, Runge-Kutta
23 Discontinuous Galerkin, Runge-Kutta Finite Differences, ...) is, from the
24 project's point of view, a black box that has to answer a small set of
25 questions: what data does it need on the mesh, which action sets realise
26 its algorithmic steps, and how does it plug into the code generation. This
27 class holds exactly that common contract plus the bits of bookkeeping
28 (name, includes, solver constants, the unknowns/auxiliary_variables
29 variable-shortcut logic) that every concrete solver has historically
30 reimplemented identically. It does not know anything about a concrete
31 discretisation - it neither creates the actual mesh data structures nor
32 the actual action sets, as this remains solver-specific and thus has to be
33 added by all subclasses.
34
35 A concrete solver typically extends the constructor with its own
36 discretisation-specific parameters (patch size, polynomial order, time
37 stepping scheme, ...), calls `Solver.__init__()` for the shared part, and
38 then implements the abstract methods below.
39
40 @see exahype2.api.Project
41
42 """
43
45 """!
46
47 Turns a Solver into actual classes.
48
49 """
50
51 def __init__(self, solver: "Solver"):
52 self._solver = solver
53
54 @abc.abstractmethod
55 def construct_output(self, exahype2_project, peano_project):
56 """!
57 Has to be implemented by subclass
58 """
59
60 class NumericalScheme(abc.ABC):
61 """!
62
63 The concrete discretisation (Finite Volumes, ADER-DG, ...) realised
64 by a Solver. Abstract - concrete solvers have to provide a subclass
65 that implements construct_output().
66
67 """
68
69 def __init__(self, solver: "Solver"):
70 """!
71
72 @param solver: exahype2.api.Solver
73 The solver which the scheme discretises.
74
75 """
76 self._solver = solver
77
78 @abc.abstractmethod
79 def construct_output(self, exahype2_project, peano_project, time_step):
80 """!
81 Has to be implemented by subclass
82
83 time_step: peano4.api.taskgraph.Graph
84 The task graph of the surrounding exahype2_project's time
85 stepping phase, so the numerical scheme can add its own tasks
86 into it.
87 """
88
89 def create_default_mesh_adaptation(self) -> "Solver.MeshAdaptation":
90 """!
91
92 The mesh adaptation a solver uses if the user does not assign
93 one to solver.mesh_adaptation
94
95 @return Solver.MeshAdaptation or None
96 The default returns None, i.e. the solver does not contribute
97 to the mesh adaptation phase.
98
99 """
100 return None
101
102 class GridConstruction(abc.ABC):
103 """!
104
105 How a Solver drives the construction of the mesh it runs on.
106 Abstract - concrete solvers have to provide a subclass that
107 implements construct_output().
108
109 """
110
111 def __init__(self, solver: "Solver"):
112 self._solver = solver
113
114 @abc.abstractmethod
115 def construct_output(self, exahype2_project, peano_project, setup_simulation):
116 """!
117 Has to be implemented by subclass
118
119 setup_simulation: peano4.api.taskgraph.Graph
120 The task graph of the surrounding exahype2_project's setup
121 phase, so the grid construction can add its own tasks into it.
122 """
123
124 class Plotter(abc.ABC):
125 """!
126
127 How a Solver writes out its solution. Abstract - concrete solvers
128 have to provide a subclass that implements construct_output().
129
130 A solver can have an arbitrary number of plotters
131 (@ref exahype2.api.Solver.Solver.add_plotter()), e.g. one which
132 writes VTK files for visualisation and one which writes
133 checkpoints. Each plotter has a name which is unique per solver,
134 and it decides itself when it is active. For this, it gives its
135 solver's C++ class a method
136
137 ~~~~~~~~~~~~~~~~~~~~~~~
138 bool is<Name>Active() const;
139 ~~~~~~~~~~~~~~~~~~~~~~~
140
141 The solver's
142
143 ~~~~~~~~~~~~~~~~~~~~~~~
144 bool wantsToPlot() const;
145 ~~~~~~~~~~~~~~~~~~~~~~~
146
147 holds if any of its plotters is active. The generated main asks all
148 solvers with a plotter (repositories::wantsToPlot()) and runs the
149 plot phase if any of them wants to plot. The task graphs of all
150 plotters of all solvers are added to the one plot graph, which the
151 graph compiler then maps onto grid sweeps. Within the plot phase,
152 every action set of a plotter is guarded by its plotter's
153 is<Name>Active(), so only the active plotters do any work.
154
155 is<Name>Active() must not change throughout the plot phase. Each
156 plotter therefore calls its
157
158 ~~~~~~~~~~~~~~~~~~~~~~~
159 void finish<Name>();
160 ~~~~~~~~~~~~~~~~~~~~~~~
161
162 which moves it on to its next output, in the unprepareTraversal() of
163 its last task, i.e. after all of its tasks have run.
164
165 The class provides this bookkeeping for plotters which write output
166 in regular time intervals (_add_time_interval_bookkeeping_to_solver()).
167
168 """
169
170 def __init__(self, solver: "Solver", name: str = None):
171 """!
172
173 @param solver: exahype2.api.Solver
174 The solver whose solution is written.
175
176 @param name: str or None
177 Name of the plotter, which is unique per solver. It becomes
178 part of the generated C++ method names, i.e. it has to be a
179 valid C++ identifier. If None, the solver derives a name from
180 the plotter's class name once the plotter is added
181 (@ref exahype2.api.Solver.Solver.add_plotter()).
182
183 """
184 self._solver = solver
185 if name is not None and not name.isidentifier():
186 raise ValueError(f"Plotter name {name} is no valid C++ identifier")
187 self.name = name
188
189 @abc.abstractmethod
190 def construct_output(self, exahype2_project, peano_project, plot):
191 """!
192 Has to be implemented by subclass
193
194 plot: peano4.api.taskgraph.Graph
195 The task graph of the surrounding exahype2_project's plotting
196 phase, so the plotter can add its own tasks into it.
197 """
198
199 def output_file_prefix(self) -> str:
200 """!
201
202 Prefix of the files the plotter writes, including the path
203
204 The solver uses it to ensure that no two plotters write into
205 the same files. Plotters which write no files return None.
206
207 """
208 return None
209
211 """!
212
213 C++ predicate which holds if the plotter is active
214
215 Combine the guards of all of the plotter's action sets with it.
216
217 """
218 return (
219 "repositories::"
221 + ".is"
222 + self.name
223 + "Active()"
224 )
225
226 def create_finish_call(self) -> str:
227 """!
228
229 C++ statement which moves the plotter on to its next output
230
231 Insert it into the unprepareTraversal() of the plotter's last
232 task.
233
234 """
235 return (
236 "repositories::"
238 + ".finish"
239 + self.name
240 + "();\n"
241 )
242
244 self, first_time_stamp: float, time_in_between: float
245 ):
246 """!
247
248 Make the plotter active in regular time intervals
249
250 Add the attribute _next<Name>TimeStamp plus the methods
251 is<Name>Active() and finish<Name>() to the solver's C++ class.
252 The plotter is active as soon as the solver's min time stamp has
253 reached _next<Name>TimeStamp. finish<Name>() then increments
254 _next<Name>TimeStamp by time_in_between.
255
256 The solver keeps these members once added, so we add them only
257 once, even if the Peano 4 project is generated multiple times.
258
259 @param first_time_stamp: float
260 Time stamp of the first output.
261
262 @param time_in_between: float
263 Time in between two outputs. Has to be positive.
264
265 """
266 attribute = (
267 "double _next"
268 + self.name
269 + "TimeStamp = "
270 + repr(float(first_time_stamp))
271 + ";"
272 )
273 if attribute in self._solver._abstract_solver_attributes:
274 return
275
278 "bool is" + self.name + "Active() const;",
279 lambda qualified_class_name: "bool "
280 + qualified_class_name
281 + "::is"
282 + self.name
283 + "Active() const {\n"
284 " return tarch::la::greaterEquals(getMinTimeStamp(), _next"
285 + self.name
286 + "TimeStamp);\n"
287 "}\n",
288 )
290 "void finish" + self.name + "();",
291 lambda qualified_class_name: "void "
292 + qualified_class_name
293 + "::finish"
294 + self.name
295 + "() {\n"
296 " if (is" + self.name + "Active()) {\n"
297 " _next"
298 + self.name
299 + "TimeStamp += "
300 + repr(float(time_in_between))
301 + ";\n"
302 " }\n"
303 "}\n",
304 )
305
306 class MeshAdaptation(abc.ABC):
307 """!
308
309 How a Solver adapts its mesh and rebalances its load between two
310 time steps. Abstract - concrete solvers have to provide a subclass
311 that implements construct_output().
312
313 The mesh adaptation is the fourth phase of an ExaHyPE 2 project
314 next to the setup, the time stepping and the plotting. The
315 generated main runs it right after a time step, and only if this
316 time step has asked for a mesh refinement or erase, or if the load
317 balancing has split recently (@ref exahype2::isMeshAdaptationRequired()).
318 The phase is optional: Solvers without a MeshAdaptation do not
319 contribute to it, and the main never runs an empty phase.
320
321 The mesh adaptation does not decide where to refine or erase. The
322 numerical scheme's last task of the time step collects these
323 requests (@ref exahype2.api.blockstructured.DynamicMeshRefinement),
324 and the mesh adaptation realises them.
325
326 """
327
328 def __init__(self, solver: "Solver"):
329 self._solver = solver
330
331 @abc.abstractmethod
332 def construct_output(self, exahype2_project, peano_project, mesh_adaptation):
333 """!
334 Has to be implemented by subclass
335
336 mesh_adaptation: peano4.api.taskgraph.Graph
337 The task graph of the surrounding exahype2_project's mesh
338 adaptation phase, so the mesh adaptation can add its own tasks
339 into it.
340 """
341
342
364 SolutionUpdateTaskTag = "exahype2.api.Solver.SolutionUpdateTaskTag"
365
366
373 IntermediateSolutionUpdateTag = "exahype2.api.Solver.IntermediateSolutionUpdateTag"
374
376 self,
377 name: str,
378 unknowns: VariableSpecification,
379 auxiliary_variables: VariableSpecification,
380 ):
381 """!
382
383 Create solver
384
385 name: str
386 A unique name for the solver. This one is used for all generated
387 classes. The generated C++ object instance also incorporates this
388 name.
389
390 unknowns: int or dict
391 Number of unknowns per mesh entry (Finite Volume voxel, DG degree of
392 freedom, ...). Eventually, unknowns and auxiliary_variables are
393 merged into one big vector, but the solver still has to be able to
394 distinguish them, as only the unknowns are subject to the actual PDE
395 formulation. Can be specified either as a plain int, or as a dict
396 mapping variable name onto multiplicity (e.g.
397 `{"rho": 1, "j": 3}` for a scalar density plus a 3d momentum). If
398 dicts are used, the generated Abstract<SolverName> class hosts a
399 nested struct Variables with one constant per name, so user code
400 can write Q[Variables::j+1] instead of having to remember the
401 offset into the unknowns array by hand.
402
403 auxiliary_variables: int or dict
404 Same semantics as unknowns, but for variables that may be
405 space-dependent yet do not change over time. Their names follow
406 the unknowns' names, as the auxiliary variables follow the
407 unknowns in Q.
408
409 If names are used at all, they have to cover all unknowns and all
410 auxiliary variables, i.e. you cannot pass a dict for one and a
411 non-zero int for the other.
412
413 The variables cannot be changed after construction: the numerical
414 scheme sizes its data structures from them as soon as it is assigned
415 to the solver.
416
417 """
418 self._name = name
419
420 # Offset of each named variable within Q. Empty if no names are used.
421 self._variable_positions: Dict[str, int] = {}
422
423 self._unknowns = self._register_variables(unknowns, 0)
425 auxiliary_variables, self._unknowns
426 )
427
428 named_entries = sum(
429 sum(variables.values())
430 for variables in (unknowns, auxiliary_variables)
431 if isinstance(variables, dict)
432 )
433 if self._variable_positions and named_entries != (
435 ):
436 raise ValueError(
437 f"Solver {name}: the variable names {list(self._variable_positions)} "
438 f"cover only {named_entries} of the {self._unknowns} unknowns and "
439 f"{self._auxiliary_variables} auxiliary variables. If you name "
440 f"variables, name all unknowns and all auxiliary variables, i.e. "
441 f"pass dicts for both (or 0 for the one that is empty)."
442 )
443
446
447 self._pde_terms: Dict[Tuple[str, bool], PDETerm] = {}
448
449 # Raw C++ member declarations/methods to splice into the generated
450 # Abstract<SolverName> class, on top of the PDE terms above -
451 # nothing here by default, @see add_abstract_solver_attribute()/
452 # add_abstract_solver_method().
453 self._abstract_solver_attributes: List[str] = []
454 self._abstract_solver_methods: List[Tuple[str, Callable[[str], str]]] = []
455
456 # Deferred/local imports: these classes all subclass either one of
457 # the Solver.* nested classes above or a class from a sibling
458 # exahype2 subpackage, and some of those in turn import Solver at
459 # their own module top-level. Importing them here too (at Solver.py's
460 # top level) would be a genuine circular import, since this module's
461 # own import of them would then run before class Solver is even
462 # defined.
463 from .SolverToCPPFiles import SolverToCPPFiles
464
466
467 # NumericalScheme has no default realisation - concrete solver
468 # subclasses have to assign their own subclass instance to it,
469 # through the numerical_scheme property below.
470 self._numerical_scheme: "Solver.NumericalScheme" = None
471
472 # GridConstruction has no default realisation - concrete solver
473 # subclasses (or their users) have to assign their own
474 # Solver.GridConstruction subclass instance to it, e.g.
475 # exahype2.api.fv.SetupSimulationOverRegularGrid, through the
476 # grid_construction attribute below.
477 self.grid_construction: "Solver.GridConstruction" = None
478
479 # Plotters have no default realisation - users add their own
480 # Solver.Plotter subclass instances, e.g.
481 # exahype2.api.fv.PlotAsVTKFile, via add_plotter().
482 self._plotters: List["Solver.Plotter"] = []
483
484 # MeshAdaptation is optional. Without one, the solver does not
485 # contribute to the mesh adaptation phase. Users can assign their
486 # own Solver.MeshAdaptation subclass instance to it through the
487 # mesh_adaptation attribute below.
488 self.mesh_adaptation: "Solver.MeshAdaptation" = None
489
490 # Selects the mesh entities on which all action sets of this solver
491 # act: Each action set's guard is the corresponding output of this
492 # selector. By default, the solver works on the finest mesh level
493 # only. Users can assign any other peano4.api.toolbox.GridSelector.
494 self.grid_selector: peano4.api.toolbox.GridSelector = (
496 )
497
498 # The kernel function calls are generated as late as possible, to
499 # ensure that all PDE terms and the numerical scheme have been
500 # specified
501 self.compute_kernel_call: str = None
502
503 def _register_variables(self, variables: VariableSpecification, offset: int) -> int:
504 """!
505
506 Translate a plain int or a name-to-multiplicity dict into a total
507 variable count, and - for the dict case - add each name's position
508 within Q to _variable_positions. Called once for unknowns (offset 0)
509 and once for auxiliary_variables (offset = number of unknowns), so
510 the two end up in one contiguous name/offset table.
511
512 """
513 if isinstance(variables, dict):
514 for variable_name, multiplicity in variables.items():
515 if variable_name in self._variable_positions:
516 raise ValueError(
517 f"Solver {self._name}: variable name {variable_name} is used twice"
518 )
519 self._variable_positions[variable_name] = offset
520 offset += multiplicity
521 return sum(variables.values())
522 elif isinstance(variables, int):
523 return variables
524 else:
525 raise TypeError(
526 "unknowns/auxiliary_variables have to be an int or a dict "
527 "mapping variable name onto multiplicity, but got "
528 f"{type(variables)}"
529 )
530
531 def __str__(self) -> str:
532 return (
533 "Name: " + self._name + "\n"
534 "Type: " + self.__class__.__module__ + "\n"
535 "Unknowns: " + str(self._unknowns) + "\n"
536 "Auxiliary vars: " + str(self._auxiliary_variables)
537 )
538
539 @property
540 def name(self) -> str:
541 """!
542 Unique solver name. Feeds into all generated class and instance names.
543 """
544 return self._name
545
546 @property
547 def numerical_scheme(self) -> "Solver.NumericalScheme":
548 return self._numerical_scheme
549
550 @numerical_scheme.setter
551 def numerical_scheme(self, value: "Solver.NumericalScheme"):
552 if not isinstance(value, Solver.NumericalScheme):
553 raise TypeError(
554 "numerical_scheme has to be a Solver.NumericalScheme, but got "
555 f"{type(value)}"
556 )
557 self._numerical_scheme = value
558
559 @property
560 def plotters(self) -> List["Solver.Plotter"]:
561 """!
562 All plotters of the solver, in the order they have been added.
563 """
564 return list(self._plotters)
565
566 def add_plotter(self, plotter: "Solver.Plotter"):
567 """!
568
569 Add a plotter to the solver
570
571 A solver can have an arbitrary number of plotters, which are
572 independent of each other (@ref exahype2.api.Solver.Solver.Plotter).
573 If the plotter has no name yet, it gets the name of its class,
574 followed by a number if the solver has another plotter of that name
575 already.
576
577 @param plotter: exahype2.api.Solver.Plotter
578 The plotter. Its solver has to be this solver.
579
580 """
581 if not isinstance(plotter, Solver.Plotter):
582 raise TypeError(
583 f"plotter has to be a Solver.Plotter, but got {type(plotter)}"
584 )
585 if plotter._solver is not self:
586 raise ValueError(
587 f"Plotter {type(plotter).__name__} belongs to another solver than {self._name}"
588 )
589 names = [p.name for p in self._plotters]
590 if plotter.name is None:
591 name = type(plotter).__name__
592 counter = 2
593 while name in names:
594 name = type(plotter).__name__ + str(counter)
595 counter += 1
596 plotter.name = name
597 elif plotter.name in names:
598 raise ValueError(
599 f"Solver {self._name} has a plotter named {plotter.name} already"
600 )
601 prefix = plotter.output_file_prefix()
602 if prefix is not None and prefix in [
603 p.output_file_prefix() for p in self._plotters
604 ]:
605 raise ValueError(
606 f"Plotter {plotter.name} of solver {self._name} would write into "
607 f"the files {prefix}* of another plotter. Give it another file "
608 "name or output path."
609 )
610 self._plotters.append(plotter)
611
612 @property
613 def plotter(self) -> "Solver.Plotter":
614 """!
615
616 Shorthand for solvers with at most one plotter
617
618 Reading it yields the solver's only plotter, or None if it has none.
619 Assigning a plotter replaces all plotters of the solver by this one,
620 assigning None removes all plotters. Use add_plotter() to give a
621 solver more than one plotter.
622
623 """
624 if len(self._plotters) > 1:
625 raise RuntimeError(
626 f"Solver {self._name} has {len(self._plotters)} plotters. Use the plotters property."
627 )
628 return self._plotters[0] if self._plotters else None
629
630 @plotter.setter
631 def plotter(self, value: "Solver.Plotter"):
632 self._plotters = []
633 if value is not None:
634 self.add_plotter(value)
635
636 @property
637 def unknowns(self) -> int:
638 return self._unknowns
639
640 @property
641 def auxiliary_variables(self) -> int:
642 return self._auxiliary_variables
643
644 @property
645 def user_solver_includes(self) -> str:
646 """!
647 Add further includes to this property, if your solver requires some
648 additional routines from other header files.
649 """
650 return self._user_solver_includes
651
652 def add_user_solver_includes(self, value: str):
653 """!
654 Add further includes to this property, if your solver requires some
655 additional routines from other header files.
656 """
657 self._user_solver_includes += value
658
659 def _get_default_includes(self) -> str:
660 """!
661 Includes every generated action set can rely on being available,
662 regardless of which action sets a concrete discretisation (e.g.
663 exahype2.api.fv) actually attaches - @see
664 exahype2.api.fv.actionsets.AbstractFVActionSet.get_includes().
665 """
666 return """
667#include "tarch/la/Vector.h"
668
669#include "peano4/utils/Globals.h"
670#include "peano4/utils/Loop.h"
671
672#include "repositories/SolverRepository.h"
673"""
674
675 def set_solver_constants(self, datastring: str):
676 self._solver_constants = datastring
677
678 def add_solver_constants(self, datastring: str):
679 self._solver_constants += datastring + "\n"
680
681 def _unknown_identifier(self) -> str:
682 return self._name + "Q"
683
684 def variable_names_per_entry(self) -> List[str]:
685 """!
686
687 One name per entry of Q, i.e. for the unknowns followed by the
688 auxiliary variables, or the empty list if the variables carry no
689 names. A variable with multiplicity one keeps its name, whereas a
690 variable v with multiplicity k yields the names v[0], ..., v[k-1],
691 i.e. the entries Q[Variables::v+0], ..., Q[Variables::v+k-1].
692 Plotters use these names to label the entries in their output.
693
694 """
695 positions = list(self._variable_positions.items())
696 names = []
697 for i, (variable_name, position) in enumerate(positions):
698 next_position = (
699 positions[i + 1][1]
700 if i + 1 < len(positions)
701 else self._unknowns + self._auxiliary_variables
702 )
703 multiplicity = next_position - position
704 if multiplicity == 1:
705 names.append(variable_name)
706 else:
707 names += [f"{variable_name}[{j}]" for j in range(multiplicity)]
708 return names
709
711 return "instanceOf" + self._name
712
713 @property
714 def pde_terms(self) -> List[PDETerm]:
715 """!
716 All PDE terms (flux, eigenvalues, ...) registered with this solver,
717 in registration order. A given name can appear at most twice - once
718 stateless, once not, @see add_pde_term() - since each generates a
719 different C++ method. Read-only - use add_pde_term() to add one and
720 remove_pde_term() to take one away again.
721 """
722 return list(self._pde_terms.values())
723
724 @property
725 def requires_user_class(self) -> bool:
726 """!
727 True iff at least one PDE term has no implementation. Only then
728 does the user have to provide one, i.e. only then is a
729 <SolverName> class derived from Abstract<SolverName> generated
730 and added to the Makefile (@see exahype2.api.SolverToCPPFiles).
731 Otherwise, Abstract<SolverName> is complete and the solver
732 repository instantiates it directly. Stateless terms always carry
733 an implementation (@see PDETerm), so they never require one.
734 """
735 return any(term.implementation is None for term in self.pde_termspde_terms)
736
737 def get_name_of_solver_class(self) -> str:
738 """!
739 Name of the C++ class the solver repository instantiates:
740 <SolverName> if requires_user_class, Abstract<SolverName> otherwise.
741 """
742 return self._name if self.requires_user_class else "Abstract" + self._name
743
744 def add_pde_term(self, term: PDETerm):
745 """!
746
747 Register a PDE term with this solver, or replace the one already
748 registered under the same name and statelessness. A solver
749 aggregates an arbitrary number of terms - the well-known ones
750 (Flux(), MaxEigenvalue(), ...) as well as new, solver-specific ones
751 users construct themselves via PDETerm(name=..., implementation=...).
752
753 Each term generates exactly one C++ method: the static,
754 GPU-offloadable overload if term.stateless, otherwise the virtual
755 instance method (@see PDETerm.Generator.generate_declaration()),
756 never both. To get both variants of the same conceptual operation
757 (e.g. a stateless flux for GPU kernels and a stateful one for
758 everything else), add the term twice - once with stateless=True,
759 once without - which is why terms are keyed by (name, stateless)
760 rather than by name alone.
761
762 """
763 if not isinstance(term, PDETerm):
764 raise TypeError(f"term has to be a PDETerm, but got {type(term)}")
765 self._pde_terms[(term.name, term.stateless)] = term
766
767 def remove_pde_term(self, name: str, stateless: bool = False):
768 """! Remove a previously registered PDE term by name and statelessness."""
769 del self._pde_terms[(name, stateless)]
770
771 def get_pde_term(self, name: str, stateless: bool = False) -> PDETerm:
772 """! Look up a previously registered PDE term by name and statelessness."""
773 return self._pde_terms[(name, stateless)]
774
775 def add_abstract_solver_attribute(self, declaration: str):
776 """!
777
778 Add a raw C++ member attribute declaration to the generated
779 Abstract<SolverName> class (@see exahype2.api.SolverToCPPFiles),
780 e.g. "double _timeStamp = 0.0;" - use an in-class initialiser if
781 the attribute needs one, since there is no hook into the generated
782 constructor for this.
783
784 Nothing calls this unconditionally: a plain Solver carries no
785 attributes beyond what PDE terms need. It is up to whichever
786 NumericalScheme/GridConstruction/Plotter a solver uses to call
787 this, if it needs solver-level state of its own that genuinely
788 has no home on exahype2::Solver already - e.g.
789 exahype2.api.fv.RusanovWithExplicitEuler deliberately does *not*
790 use this for time stamp/time step size bookkeeping, since
791 ::exahype2::Solver (which every generated Abstract<SolverName>
792 already inherits from) already carries
793 _minTimeStamp/_maxTimeStamp/_minTimeStepSize/_maxTimeStepSize and
794 the updateCell()/startTimeStep()/finishTimeStep() lifecycle to
795 maintain them - @see exahype2.api.fv.actionsets.
796 ComputeGlobalCFLCondition, which drives that
797 lifecycle instead of introducing parallel state via this method.
798
799 """
800 self._abstract_solver_attributes.append(declaration)
801
803 self, declaration: str, definition_generator: Callable[[str], str]
804 ):
805 """!
806
807 Add a raw C++ method to the generated Abstract<SolverName> class:
808 declaration goes into the class body (public section), verbatim.
809 definition_generator is called with the class' fully qualified
810 name once it is known (@see exahype2.api.SolverToCPPFiles,
811 exactly the way PDETerm.Generator.generate_out_of_class_definitions()
812 already is) and has to return the out-of-class definition - a
813 plain string with a "{qualified_class_name}" placeholder is
814 deliberately not supported here, since a C++ method body is full
815 of literal braces of its own; build the string with concatenation
816 instead, e.g.:
817
818 ~~~~~~~~~~~~~~~~~~~~~~~
819 solver.add_abstract_solver_method(
820 "void increaseTimestamp(double dt);",
821 lambda qualified_class_name:
822 "void " + qualified_class_name + "::increaseTimestamp(double dt) {\\n"
823 " _timeStamp += dt;\\n"
824 "}\\n"
825 )
826 ~~~~~~~~~~~~~~~~~~~~~~~
827
828 @see add_abstract_solver_attribute() for the attribute counterpart,
829 and exahype2.api.PDETerm.Generator for the more structured
830 alternative this deliberately is not (that one is for PDE terms a
831 user implements; this one is for solver-level bookkeeping a
832 NumericalScheme/etc. needs that has no equivalent on
833 ::exahype2::Solver already - @see add_abstract_solver_attribute()'s
834 own note on why RusanovWithExplicitEuler does not actually use
835 either method for time stamp/time step size bookkeeping).
836
837 """
838 self._abstract_solver_methods.append((declaration, definition_generator))
839
841 """!
842
843 Add wantsToPlot() to the solver's C++ class: It holds if any of the
844 solver's plotters is active (@ref exahype2.api.Solver.Solver.Plotter).
845 Solvers without plotters do not get the method, as nobody asks them.
846 The method is added only once, even if the Peano 4 project is
847 generated multiple times, and it evaluates the plotters the solver
848 has when the C++ code is written.
849
850 """
851 declaration = "bool wantsToPlot() const;"
852 if not self._plotters or declaration in [
853 existing for existing, _ in self._abstract_solver_methods
854 ]:
855 return
857 declaration,
858 lambda qualified_class_name: "bool "
859 + qualified_class_name
860 + "::wantsToPlot() const {\n"
861 " return "
862 + " or ".join("is" + p.name + "Active()" for p in self._plotters)
863 + ";\n"
864 "}\n",
865 )
866
868 """!
869
870 Override exahype2::Solver::toCheckpointMetaData() in the solver's
871 C++ class such that it also writes the attributes added through
872 add_abstract_solver_attribute(), e.g. the time stamps of the next
873 outputs of the plotters. Only attributes of type double, float or
874 int are written, with their name without leading underscore as key.
875
876 The counterpart override of exahype2::Solver::fromCheckpointMetaData()
877 restores these attributes. Afterwards, it calls finish<Name>() of
878 all plotters: A plotter which has written output at the time of the
879 checkpoint has done so before it has moved on to its next output, as
880 it only moves on at the end of the plot phase
881 (@ref exahype2.api.Solver.Solver.Plotter). The checkpoint hence holds
882 the time stamp of an output which has already been written.
883 finish<Name>() moves such a plotter on, exactly as the original run
884 has done after the checkpoint, so the restarted simulation does not
885 write this output again.
886
887 The overrides evaluate the attributes and plotters the solver has
888 when the C++ code is written, and they are added only once, even if
889 several checkpoints use them or the Peano 4 project is generated
890 multiple times.
891
892 """
893 declaration = "std::string toCheckpointMetaData() const override;"
894 if declaration in [existing for existing, _ in self._abstract_solver_methods]:
895 return
896
897 def checkpointed_attributes():
898 for attribute in self._abstract_solver_attributes:
899 match = re.match(r"\s*(?:double|float|int)\s+(\w+)\s*(=|;)", attribute)
900 if match is not None:
901 yield match.group(1)
902
903 def to_definition(qualified_class_name):
904 result = (
905 "std::string "
906 + qualified_class_name
907 + "::toCheckpointMetaData() const {\n"
908 " return ::exahype2::Solver::toCheckpointMetaData()"
909 )
910 for attribute_name in checkpointed_attributes():
911 result += (
912 '\n + "\\n" + toCheckpointMetaDataEntry("'
913 + attribute_name.lstrip("_")
914 + '", '
915 + attribute_name
916 + ")"
917 )
918 return result + ";\n}\n"
919
920 def from_definition(qualified_class_name):
921 result = (
922 "void "
923 + qualified_class_name
924 + "::fromCheckpointMetaData(const std::string& metaData) {\n"
925 " ::exahype2::Solver::fromCheckpointMetaData(metaData);\n"
926 " double value;\n"
927 )
928 for attribute_name in checkpointed_attributes():
929 result += (
930 ' if (getCheckpointMetaDataEntry(metaData, "'
931 + attribute_name.lstrip("_")
932 + '", value)) {\n'
933 " "
934 + attribute_name
935 + " = static_cast<decltype("
936 + attribute_name
937 + ")>(value);\n"
938 " }\n"
939 )
940 for plotter in self._plotters:
941 result += " finish" + plotter.name + "();\n"
942 return result + "}\n"
943
944 self.add_abstract_solver_method(declaration, to_definition)
946 "void fromCheckpointMetaData(const std::string& metaData) override;",
947 from_definition,
948 )
949
950 # @todo This one is important
951 def create_readme_descriptor(self, domain_offset, domain_size) -> str:
952 """!
953 Return a human-readable description of this solver instance, to be
954 embedded into the generated project's README.
955 """
956 pass
957
959 self,
960 exahype2_project,
961 peano_project,
962 setup_simulation,
963 time_step,
964 plot,
965 mesh_adaptation,
966 ):
967 """!
968 Turn this solver specification into a task graph and create the required C++ kernels/stubs
969
970 peano_project: peano4.api.Project
971 The Peano 4 project that exahype2_project.generate_Peano4_project() is
972 currently assembling. It is a local variable there, not an attribute -
973 merely passed through here, so this solver can add its own artefacts to it.
974
975 setup_simulation, time_step, plot, mesh_adaptation: peano4.api.taskgraph.Graph
976 The task graphs of the four algorithmic phases of the surrounding
977 exahype2_project. They are owned by exahype2_project.generate_Peano4_project()
978 and merely passed through here, so this solver can add its own
979 tasks into them.
980
981 @return peano4.api.taskgraph.Graph
982 """
983 peano4.api.Logger().new_section("Solver " + self._name)
984
985 assert self.grid_construction is not None, (
986 f"Solver {self._name} has no grid_construction set. Assign a "
987 "Solver.GridConstruction subclass instance to grid_construction "
988 "before calling generate_solver_realisation()."
989 )
990 assert self.numerical_scheme is not None, (
991 f"Solver {self._name} has no numerical_scheme set. Assign a "
992 "Solver.NumericalScheme subclass instance to numerical_scheme "
993 "before calling generate_solver_realisation()."
994 )
995
996 # First, as the numerical scheme might ask for the mesh adaptation
997 if self.mesh_adaptation is None:
999 self.numerical_schemenumerical_scheme.create_default_mesh_adaptation()
1000 )
1001
1002 self.grid_construction.construct_output(
1003 exahype2_project, peano_project, setup_simulation
1004 )
1005 self.numerical_schemenumerical_scheme.construct_output(
1006 exahype2_project, peano_project, time_step
1007 )
1008 for plotter in self._plotters:
1009 plotter.construct_output(exahype2_project, peano_project, plot)
1011 if self.mesh_adaptation is not None:
1012 self.mesh_adaptation.construct_output(
1013 exahype2_project, peano_project, mesh_adaptation
1014 )
1015 # Last, so grid construction, numerical scheme, plotter and mesh
1016 # adaptation can add their own members to the solver class first
1017 # (@see add_abstract_solver_attribute()/add_abstract_solver_method()).
1018 self.solver_generator.construct_output(exahype2_project, peano_project)
How a Solver drives the construction of the mesh it runs on.
Definition Solver.py:102
construct_output(self, exahype2_project, peano_project, setup_simulation)
Has to be implemented by subclass.
Definition Solver.py:115
How a Solver adapts its mesh and rebalances its load between two time steps.
Definition Solver.py:306
construct_output(self, exahype2_project, peano_project, mesh_adaptation)
Has to be implemented by subclass.
Definition Solver.py:332
__init__(self, "Solver" solver)
Definition Solver.py:328
The concrete discretisation (Finite Volumes, ADER-DG, ...) realised by a Solver.
Definition Solver.py:60
__init__(self, "Solver" solver)
Definition Solver.py:69
"Solver.MeshAdaptation" create_default_mesh_adaptation(self)
The mesh adaptation a solver uses if the user does not assign one to solver.mesh_adaptation.
Definition Solver.py:89
construct_output(self, exahype2_project, peano_project, time_step)
Has to be implemented by subclass.
Definition Solver.py:79
How a Solver writes out its solution.
Definition Solver.py:124
__init__(self, "Solver" solver, str name=None)
Definition Solver.py:170
str output_file_prefix(self)
Prefix of the files the plotter writes, including the path.
Definition Solver.py:199
_add_time_interval_bookkeeping_to_solver(self, float first_time_stamp, float time_in_between)
Make the plotter active in regular time intervals.
Definition Solver.py:245
str create_is_active_predicate(self)
C++ predicate which holds if the plotter is active.
Definition Solver.py:210
construct_output(self, exahype2_project, peano_project, plot)
Has to be implemented by subclass.
Definition Solver.py:190
str create_finish_call(self)
C++ statement which moves the plotter on to its next output.
Definition Solver.py:226
Turns a Solver into actual classes.
Definition Solver.py:44
__init__(self, "Solver" solver)
Definition Solver.py:51
construct_output(self, exahype2_project, peano_project)
Has to be implemented by subclass.
Definition Solver.py:55
Abstract base class for all ExaHyPE 2 solvers.
Definition Solver.py:17
str _unknown_identifier(self)
Definition Solver.py:681
str get_name_of_global_instance(self)
Definition Solver.py:710
_add_wants_to_plot_to_solver(self)
Add wantsToPlot() to the solver's C++ class: It holds if any of the solver's plotters is active (exah...
Definition Solver.py:840
add_abstract_solver_method(self, str declaration, Callable[[str], str] definition_generator)
Add a raw C++ method to the generated Abstract<SolverName> class: declaration goes into the class bod...
Definition Solver.py:804
generate_solver_realisation(self, exahype2_project, peano_project, setup_simulation, time_step, plot, mesh_adaptation)
Turn this solver specification into a task graph and create the required C++ kernels/stubs.
Definition Solver.py:966
PDETerm get_pde_term(self, str name, bool stateless=False)
Look up a previously registered PDE term by name and statelessness.
Definition Solver.py:771
"Solver.NumericalScheme" numerical_scheme(self)
Definition Solver.py:547
add_abstract_solver_attribute(self, str declaration)
Add a raw C++ member attribute declaration to the generated Abstract<SolverName> class (.
Definition Solver.py:775
_add_checkpoint_meta_data_to_solver(self)
Override exahype2::Solver::toCheckpointMetaData() in the solver's C++ class such that it also writes ...
Definition Solver.py:867
set_solver_constants(self, str datastring)
Definition Solver.py:675
str _get_default_includes(self)
Includes every generated action set can rely on being available, regardless of which action sets a co...
Definition Solver.py:659
add_pde_term(self, PDETerm term)
Register a PDE term with this solver, or replace the one already registered under the same name and s...
Definition Solver.py:744
__init__(self, str name, VariableSpecification unknowns, VariableSpecification auxiliary_variables)
Create solver.
Definition Solver.py:380
add_user_solver_includes(self, str value)
Add further includes to this property, if your solver requires some additional routines from other he...
Definition Solver.py:652
remove_pde_term(self, str name, bool stateless=False)
Remove a previously registered PDE term by name and statelessness.
Definition Solver.py:767
List[str] variable_names_per_entry(self)
One name per entry of Q, i.e.
Definition Solver.py:684
bool requires_user_class(self)
True iff at least one PDE term has no implementation.
Definition Solver.py:725
List[PDETerm] pde_terms(self)
All PDE terms (flux, eigenvalues, ...) registered with this solver, in registration order.
Definition Solver.py:714
add_solver_constants(self, str datastring)
Definition Solver.py:678
_name
Create solver.
Definition Solver.py:418
str create_readme_descriptor(self, domain_offset, domain_size)
Return a human-readable description of this solver instance, to be embedded into the generated projec...
Definition Solver.py:951
add_plotter(self, "Solver.Plotter" plotter)
Add a plotter to the solver.
Definition Solver.py:566
str get_name_of_solver_class(self)
Name of the C++ class the solver repository instantiates: <SolverName> if requires_user_class,...
Definition Solver.py:737
pde_terms
True iff at least one PDE term has no implementation.
Definition Solver.py:735
str user_solver_includes(self)
Add further includes to this property, if your solver requires some additional routines from other he...
Definition Solver.py:645
numerical_scheme(self, "Solver.NumericalScheme" value)
Definition Solver.py:551
str name(self)
Unique solver name.
Definition Solver.py:540
List["Solver.Plotter"] plotters(self)
All plotters of the solver, in the order they have been added.
Definition Solver.py:560
int auxiliary_variables(self)
Definition Solver.py:641
"Solver.Plotter" plotter(self)
Shorthand for solvers with at most one plotter.
Definition Solver.py:613
_plotters
All plotters of the solver, in the order they have been added.
Definition Solver.py:564
int _register_variables(self, VariableSpecification variables, int offset)
Translate a plain int or a name-to-multiplicity dict into a total variable count, and - for the dict ...
Definition Solver.py:503
mesh_adaptation
Turn this solver specification into a task graph and create the required C++ kernels/stubs.
Definition Solver.py:998
Singleton logger for the Peano 4 Python API.
Definition Logger.py:37
Select a subset of the mesh entities.