Peano
Loading...
Searching...
No Matches
PDETerm.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
5
6class PDETerm:
7 """!
8
9 Represents one named, point-wise PDE operator that a solver exposes for
10 the user to implement: a C++ method name, its parameter list, and an
11 implementation. flux(), eigenvalues(), the nonconservative product,
12 algebraic/point sources, boundary conditions, ... all have this same
13 shape (see python/exahype2/solvers/fv/riemann/kernels.py for how they
14 are actually declared in the generated abstract solver header).
15
16 Well-known terms should get their own PDETerm subclass which pins down
17 name and signature and adds any term-specific behaviour - @see Flux for
18 a fully worked example. Use the plain PDETerm constructor directly only
19 for a new, solver-specific term that does not warrant its own subclass
20 (yet).
21
22 A term's implementation is a C++ code snippet that becomes the generated
23 method's body verbatim - including the empty string, which generates a
24 method that does nothing (a no-op, not "contributes zero": it does not
25 zero out the output for you) - or None (the default), which leaves the
26 method pure virtual for you to implement by hand in your solver
27 subclass instead of generating it. A term you don't want at all is
28 simply never passed to solver.add_pde_term() in the first place.
29
30 implementation must not be None when stateless is True: the constructor
31 raises ValueError in that case, since a stateless term's generated
32 method is static, and static methods cannot be pure virtual - there
33 would be no declaration left to signal "implement this by hand".
34
35 PDETerm only models *what* the term is (name, signature, implementation).
36 It deliberately knows nothing about Jinja2 or the code generator: turning
37 a solver's collection of terms into the generated abstract solver header
38 remains the code generator's job, exactly as it is today.
39
40 @see exahype2.api.Solver.add_pde_term()
41
42 """
43
44 NAME: str
45 SIGNATURE: str
46
48 self,
49 name: str,
50 implementation: str = None,
51 stateless: bool = False,
52 ):
53 """!
54
55 name: str
56 The C++ method name, e.g. "flux" or "eigenvalues". Used by
57 exahype2.api.Solver to key its collection of terms, i.e. has to be
58 unique per solver.
59
60 implementation: str
61 A C++ snippet that becomes the method's body verbatim (the empty
62 string is a valid, generated no-op body), or None (the default) to
63 leave the method pure virtual for you to implement by hand. Must
64 not be None if stateless is True: the generated method is then
65 static, and static methods cannot be pure virtual, so there would
66 be nothing left to tell you to implement it by hand.
67
68 """
69 if stateless and implementation is None:
70 raise ValueError(
71 f"PDETerm {name!r} is stateless, so its generated method is static - "
72 "static methods cannot be pure virtual, so implementation must not be "
73 "None (pass a C++ snippet, or the empty string for a no-op body)."
74 )
75 self.name = name
76 self.implementation = implementation
77 self.stateless = stateless
78
79 def __repr__(self) -> str:
80 return f"{self.__class__.__name__}(name={self.name!r}, implementation={self.implementation!r})"
81
82 def __str__(self) -> str:
83 return f"{self.name}: {self.implementation}"
84
85 class Generator:
86 """!
87
88 Turns a PDETerm into the C++ text for its generated method: exactly
89 one method - the static, GPU-offloadable overload if the term is
90 stateless, otherwise the virtual instance method. Never both from
91 the same term; @see exahype2.api.Solver.add_pde_term() for how to
92 get both variants of the same conceptual operation.
93
94 If term.implementation is None, the method is left for the user to
95 implement by hand: pure virtual (" = 0") for the non-stateless case,
96 or - since static methods cannot be pure virtual - simply declared
97 without a matching out-of-class definition for the stateless case.
98 Otherwise the method is generated non-overridable ("final") with
99 term.implementation as its body.
100
101 Embedded into PDETerm (reachable as term.generator) so that a term
102 subclass can supply its own Generator - @see Flux.Generator - and
103 term.generator resolves to the right one automatically, since it is
104 looked up on the term's actual class rather than hardcoded to
105 PDETerm.Generator.
106
107 """
108
109 def __init__(self, term: "PDETerm"):
110 self.term = term
111
112 @property
113 def signature(self) -> str:
114 """!
115 The term's C++ parameter list. Looked up as a SIGNATURE attribute
116 on the owning term (e.g. Flux.SIGNATURE); a plain, custom PDETerm
117 has to set term.SIGNATURE itself before it can generate code.
118 """
119 signature = getattr(self.term, "SIGNATURE", None)
120 if signature is None:
121 raise AttributeError(
122 f"{self.term!r} (or its class) has no SIGNATURE to generate code from; "
123 "either subclass PDETerm and provide one (see Flux), or set "
124 "term.SIGNATURE explicitly."
125 )
126 return signature
127
129 """!
130 static GPU-offloadable declaration, or the empty string if the
131 term isn't stateless. exahype2::Solver::Offloadable is a dummy
132 tag argument that exists precisely because C++ cannot overload on
133 staticness alone - it is what lets this static method coexist
134 with the same-named, non-static, no-Offloadable-argument method
135 that registering the same term name again without stateless
136 would produce.
137 """
138 if not self.term.stateless:
139 return ""
140 return (
141 "#if defined(GPUOffloadingOMP)\n"
142 "#pragma omp declare target\n"
143 "#endif\n"
144 f"static GPUCallableMethod void {self.term.name}(\n"
145 f" {self.signature},\n"
146 " Offloadable\n"
147 ");\n"
148 "#if defined(GPUOffloadingOMP)\n"
149 "#pragma omp end declare target\n"
150 "#endif\n"
151 )
152
153 def generate_declaration(self) -> str:
154 """!
155 Declaration text for the abstract solver header: the static,
156 GPU-offloadable overload if this term is stateless, otherwise
157 the virtual instance method - never both from one term. Pure
158 virtual (" = 0") if term.implementation is None, "final"
159 otherwise; the static, stateless overload is unaffected by this
160 distinction (static methods cannot be pure virtual) - @see
161 generate_out_of_class_definitions for how it is left for the
162 user to implement by hand instead.
163 """
164 if self.term.stateless:
166 body_suffix = " = 0" if self.term.implementation is None else " final"
167 return (
168 f"virtual void {self.term.name}(\n"
169 f" {self.signature}\n"
170 f"){body_suffix};\n"
171 )
172
173 def generate_definition(self) -> str:
174 """!
175 Body of the generated method: the term's implementation verbatim.
176 Only meaningful when term.implementation is not None - @see
177 generate_out_of_class_definitions, which never calls this
178 otherwise.
179 """
180 return self.term.implementation
181
182 def generate_out_of_class_definitions(self, qualified_class_name: str) -> str:
183 """!
184 Out-of-class method body for the solver's .cpp file: the static,
185 GPU-offloadable overload if this term is stateless, otherwise
186 the virtual instance method - mirrors generate_declaration(),
187 never both from one term. The empty string if
188 term.implementation is None: the user implements that method by
189 hand instead, whether as an override of the pure virtual
190 instance method or, for a stateless term, as an ordinary static
191 method matching the declaration (name hiding, not override -
192 static methods cannot be virtual).
193
194 qualified_class_name: str
195 Fully qualified class name the definition is attached to,
196 e.g. "examples::MySolver::AbstractMySolver".
197 """
198 if self.term.implementation is None:
199 return ""
200 body = self.generate_definition()
201 if self.term.stateless:
202 return (
203 "#if defined(GPUOffloadingOMP)\n"
204 "#pragma omp declare target\n"
205 "#endif\n"
206 f"GPUCallableMethod void {qualified_class_name}::{self.term.name}(\n"
207 f" {self.signature},\n"
208 " Offloadable\n"
209 f") {{\n {body}\n}}\n"
210 "#if defined(GPUOffloadingOMP)\n"
211 "#pragma omp end declare target\n"
212 "#endif\n"
213 )
214 return (
215 f"void {qualified_class_name}::{self.term.name}(\n"
216 f" {self.signature}\n"
217 f") {{\n {body}\n}}\n"
218 )
219
221 """!
222 Override declaration for the concrete solver class that the
223 Python API generates when the user hasn't provided their own
224 implementation of this term - the empty string if the term
225 isn't left abstract (term.implementation is not None; a
226 stateless term's method is static and thus never overridable
227 in the first place, but that combination cannot occur anyway,
228 @see PDETerm.__init__).
229 """
230 if self.term.implementation is not None or self.term.stateless:
231 return ""
232 return (
233 f"virtual void {self.term.name}(\n"
234 f" {self.signature}\n"
235 ") override;\n"
236 )
237
238 def generate_stub_definition(self, qualified_class_name: str) -> str:
239 """!
240 Out-of-class body for generate_stub_declaration(): a trace-in
241 (with x, and normal too if the term's signature has one), a
242 logWarning noting the method is unimplemented, a
243 "// @todo implement" comment, and logTraceOut - or the empty
244 string under the same condition as generate_stub_declaration().
245
246 qualified_class_name: str
247 Fully qualified class name the definition is attached to,
248 e.g. "examples::MySolver::MySolver".
249 """
250 if self.term.implementation is not None or self.term.stateless:
251 return ""
252 method_name = f"{self.term.name}(...)"
253 log_trace_in = (
254 f'logTraceInWith2Arguments("{method_name}", x, normal);'
255 if "normal" in self.signature
256 else f'logTraceInWith1Argument("{method_name}", x);'
257 )
258 return (
259 f"void {qualified_class_name}::{self.term.name}(\n"
260 f" {self.signature}\n"
261 f") {{\n"
262 f" {log_trace_in}\n"
263 f' logWarning("{method_name}", "not implemented yet");\n'
264 f" // @todo implement\n"
265 f' logTraceOut("{method_name}");\n'
266 f"}}\n"
267 )
268
269 @property
270 def generator(self) -> "PDETerm.Generator":
271 """!
272 Code generator for this term: an instance of this term's own
273 (possibly subclass-specific) Generator. @see PDETerm.Generator
274 """
275 return self.__class__.Generator(self)
Turns a PDETerm into the C++ text for its generated method: exactly one method - the static,...
Definition PDETerm.py:85
__init__(self, "PDETerm" term)
Definition PDETerm.py:109
str generate_declaration(self)
Declaration text for the abstract solver header: the static, GPU-offloadable overload if this term is...
Definition PDETerm.py:153
str generate_definition(self)
Body of the generated method: the term's implementation verbatim.
Definition PDETerm.py:173
str generate_out_of_class_definitions(self, str qualified_class_name)
Out-of-class method body for the solver's .cpp file: the static, GPU-offloadable overload if this ter...
Definition PDETerm.py:182
str _generate_static_offloadable_declaration(self)
static GPU-offloadable declaration, or the empty string if the term isn't stateless.
Definition PDETerm.py:128
str generate_stub_declaration(self)
Override declaration for the concrete solver class that the Python API generates when the user hasn't...
Definition PDETerm.py:220
str signature(self)
The term's C++ parameter list.
Definition PDETerm.py:113
str generate_stub_definition(self, str qualified_class_name)
Out-of-class body for generate_stub_declaration(): a trace-in (with x, and normal too if the term's s...
Definition PDETerm.py:238
Represents one named, point-wise PDE operator that a solver exposes for the user to implement: a C++ ...
Definition PDETerm.py:6
__init__(self, str name, str implementation=None, bool stateless=False)
name: str The C++ method name, e.g.
Definition PDETerm.py:52
"PDETerm.Generator" generator(self)
Code generator for this term: an instance of this term's own (possibly subclass-specific) Generator.
Definition PDETerm.py:270