Peano
Loading...
Searching...
No Matches
AuxiliaryVariables.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 re
6from typing import Dict, List
7
11
12
14 """!
15
16 A set of auxiliary variables of a solver
17
18 Auxiliary variables are quantities such as the sea floor, a mesh
19 distortion or a density which live in every Finite Volume (or Finite
20 Difference point) next to the unknowns, but which are not evolved by the
21 PDE. A solver can have an arbitrary number of such sets, each of them
22 vector-valued, e.g. ten bathymetry entries and another five entries
23 describing a mesh distortion. Each set is held by its own cell data
24 object, i.e. its own patch, rather than by the patch of the unknowns.
25
26 Create a set with a name and a dictionary which maps the names of the
27 parameters onto their multiplicities,
28
29 bathymetry = exahype2.api.AuxiliaryVariables("Bathymetry", {"b": 1})
30
31 and attach it to a solver via exahype2.api.Solver.add_auxiliary_variables().
32 The name is also the name of a struct within the generated abstract
33 solver, so you can write bathymetry[Bathymetry::b] in your C++ code.
34
35 ## Impact on the C++ signatures
36
37 Every PDE term of a solver with auxiliary variables receives, directly
38 behind each Q (and each deltaQ), one pointer per attached set. The
39 pointers come in the order in which the sets have been attached. The
40 pointer is named after the set (first letter in lower case), the pointers
41 behind deltaQ carry the prefix delta. For the set above and the flux, the
42 signature therefore reads
43
44 flux(const double* Q, const double* bathymetry, const Vector& x, int normal, double* F)
45
46 and the signature of the non-conservative product is
47
48 nonconservativeProduct(Q, bathymetry, deltaQ, deltaBathymetry, x, ...)
49
50 Solvers without auxiliary variables keep their original signatures.
51
52 ## Ownership and sharing
53
54 The first solver a set is attached to owns it. Only the owner's
55 initPointWisely() receives the set as writable argument and initialises
56 it. Further solvers can attach the same set object. They share the data,
57 i.e. they do not hold an additional copy, and initPointWisely() sees the
58 set as const.
59
60 @remark Naming
61 These quantities used to be called material parameters. "Material"
62 suggests that they are invariant, but they do not have to be: an
63 auxiliary variable is any quantity that lives next to the unknowns and
64 is not evolved by the PDE terms of the solver. It may well change over
65 time if some other mechanism updates it. The current implementation
66 writes the variables once during the setup, i.e. they are constant in
67 time, but this is a restriction of the implementation, not part of the
68 concept.
69
70 Besides periodic boundaries, the halo layer of the volumes at the
71 domain boundary is not available for auxiliary variables.
72
73 """
74
75 def __init__(self, name: str, variables: Dict[str, int]):
76 """!
77
78 @param name: str
79 Name of the set. Has to be a valid C++ identifier starting with a
80 capital letter. It feeds into the names of the data structures, the
81 name of the struct which hosts the positions of the variables
82 within the set, and - with a lower-case first letter - into the
83 name of the arguments in the PDE term signatures.
84
85 @param variables: dict
86 Maps the name of each parameter onto its multiplicity.
87
88 """
89 if not re.fullmatch(r"[A-Z][A-Za-z0-9_]*", name):
90 raise ValueError(
91 f"The name of a set of auxiliary variables has to be a valid C++ identifier starting with a capital letter, but got {name!r}"
92 )
93 if not isinstance(variables, dict) or not variables:
94 raise TypeError(
95 "variables has to be a non-empty dict which maps variable names onto multiplicities"
96 )
97 self._name = name
98 self._variables: Dict[str, int] = dict(variables)
99 self._positions: Dict[str, int] = {}
100 offset = 0
101 for variable_name, multiplicity in self._variables.items():
102 if multiplicity < 1:
103 raise ValueError(
104 f"Auxiliary variable {variable_name} of {name} has multiplicity {multiplicity}"
105 )
106 self._positions[variable_name] = offset
107 offset += multiplicity
108 self._size = offset
109 self._owner = None
110
111 # Created by create_data_structures()
115 self._patch_size = None
116 self._overlap = None
117 self._added_to = set()
118
119 @property
120 def name(self) -> str:
121 return self._name
122
123 @property
124 def size(self) -> int:
125 """! Total number of entries of the set, i.e. the sum of all multiplicities """
126 return self._size
127
128 @property
129 def variables(self) -> Dict[str, int]:
130 return dict(self._variables)
131
132 @property
133 def variable_positions(self) -> Dict[str, int]:
134 """! Position of each named variable within the set """
135 return dict(self._positions)
136
137 @property
138 def owner(self):
139 """! The solver which initialises the set, i.e. the first one it was attached to """
140 return self._owner
141
142 @property
143 def argument_name(self) -> str:
144 """! Name of the argument of the PDE terms """
145 return self._name[0].lower() + self._name[1:]
146
147 @property
148 def delta_argument_name(self) -> str:
149 """! Name of the argument behind deltaQ """
150 return "delta" + self._name
151
152 def variable_names_per_entry(self) -> List[str]:
153 """!
154 One name per entry of the set, qualified by the set's name. A
155 variable of multiplicity k yields k names with an index.
156 """
157 names = []
158 for variable_name, multiplicity in self._variables.items():
159 if multiplicity == 1:
160 names.append(f"{self._name}.{variable_name}")
161 else:
162 names += [f"{self._name}.{variable_name}[{j}]" for j in range(multiplicity)]
163 return names
164
165 def variables_declaration(self) -> str:
166 """! C++ struct which hosts the positions of the variables within the set """
167 constants = "\n".join(
168 f" static constexpr int {variable_name} = {position};"
169 for variable_name, position in self._positions.items()
170 )
171 return (
172 f" /**\n * Positions of the variables of the auxiliary variables {self._name}\n */\n"
173 f" struct {self._name} {{\n{constants}\n }};"
174 )
175
176 def create_data_structures(self, patch_size: int, overlap: int, grid_selector):
177 """!
178
179 Create the cell and face data of the set
180
181 The routine is idempotent, as a set may be shared by several solvers:
182 the first call creates the data, any further call checks that the
183 numerical schemes of the solvers agree on the patch size and halo.
184
185 @param patch_size: int
186 Number of volumes per axis within a patch.
187
188 @param overlap: int
189 Width of the halo layer.
190
191 @param grid_selector: peano4.api.toolbox.GridSelector
192 Selects the mesh entities which hold data.
193
194 """
195 if self.patch is not None:
196 if self._patch_size != patch_size or self._overlap != overlap:
197 raise ValueError(
198 f"The auxiliary variables {self._name} are shared by solvers with different patch sizes or halos: "
199 f"{self._patch_size}/{self._overlap} vs {patch_size}/{overlap}"
200 )
201 return
202 self._patch_size = patch_size
203 self._overlap = overlap
204 identifier = self._name + "AuxiliaryVariables"
206 (patch_size, patch_size, patch_size), self._size, identifier
207 )
209 (2 * overlap, patch_size, patch_size), self._size, identifier + "CurrentTimeStep"
210 )
212 (2 * overlap, patch_size, patch_size), self._size, identifier + "Update"
213 )
214 for patch in (
215 self.patch,
218 ):
219 patch.grid_selector = grid_selector
221 patch, "double"
222 )
223 self.patch_overlap_current_time_step.generator.merge_method_definition = (
224 toolbox.blockstructured.api.get_face_merge_implementation(
226 )
227 )
228 self.patch_overlap_current_time_step.generator.includes += """
229#include "peano4/utils/Loop.h"
230"""
231 self.patch_overlap_current_time_step.generator.send_condition = grid_selector.createFaceGuard()
232 self.patch_overlap_current_time_step.generator.receive_and_merge_condition = grid_selector.createFaceGuard()
233 self.patch_overlap_update.generator.send_condition = "false"
234 self.patch_overlap_update.generator.receive_and_merge_condition = "false"
235
236 def add_to_datamodel(self, peano_project):
237 """! Register the data with the project (once, even if the set is shared) """
238 if id(peano_project) in self._added_to:
239 return
240 self._added_to.add(id(peano_project))
241 peano_project.datamodel.add_cell(self.patch)
242 peano_project.datamodel.add_face(self.patch_overlap_current_time_step)
243 peano_project.datamodel.add_face(self.patch_overlap_update)
244
245 def __str__(self) -> str:
246 return f"{self._name}: {self._variables}"
247
248
249def split_signature(signature: str) -> List[str]:
250 """! Split a C++ parameter list at the commas that are not within template brackets """
251 return [
252 parameter.strip()
253 for parameter in re.split(r",(?![^<]*>)", signature)
254 if parameter.strip()
255 ]
256
257
258def extend_signature(signature: str, solver) -> str:
259 """!
260
261 Add the pointers to the auxiliary variables of a solver to a PDE term's
262 signature
263
264 Each argument named Q is followed by one pointer per set, each argument
265 named deltaQ is followed by one pointer to the jump of each set. The
266 pointers are non-const iff Q is non-const (the initial condition) and the
267 solver owns the set.
268
269 @param signature: str
270 The signature without auxiliary variables
271
272 @param solver: exahype2.api.Solver
273
274 """
275 sets = solver.auxiliary_variables
276 if not sets:
277 return signature
278 result = []
279 for parameter in split_signature(signature):
280 result.append(parameter)
281 name = parameter.split()[-1]
282 if name == "Q":
283 writable = not parameter.startswith("const")
284 for auxiliary in sets:
285 qualifier = "" if writable and auxiliary.owner is solver else "const "
286 result.append(f"{qualifier}double* __restrict__ {auxiliary.argument_name}")
287 elif name == "deltaQ":
288 for auxiliary in sets:
289 result.append(f"const double* __restrict__ {auxiliary.delta_argument_name}")
290 return ", ".join(result)
291
292
293def forwarding_arguments(signature: str, solver) -> List[str]:
294 """!
295
296 Arguments to call the version of a PDE term with auxiliary variables
297 from a caller that holds the original signature
298
299 The caller's Q (and deltaQ) points to a volume in a buffer which hosts
300 the unknowns followed by the entries of all auxiliary variables (the
301 layout the compute kernels work on). The auxiliary variables are slices
302 of this buffer.
303
304 """
305 arguments = []
306 for parameter in split_signature(signature):
307 name = parameter.split()[-1]
308 arguments.append(name)
309 if name in ("Q", "deltaQ"):
310 for auxiliary in solver.auxiliary_variables:
311 arguments.append(f"{name} + {solver.auxiliary_variable_offset(auxiliary)}")
312 return arguments
313
314
315def register_auxiliary_variables(solver, peano_project, patch_size: int, overlap: int):
316 """!
317
318 Create and register the data of all auxiliary variables of a solver
319
320 The persistent data of a set (its patch and the face overlaps) exists only
321 once, even if several solvers share it. Each solver gets its own
322 temporary patch with halo per set (solver._auxiliary_variable_halos, a
323 dict from the name of the set to the patch), so concurrently running
324 solvers do not overwrite each other's halos.
325
326 """
327 solver._auxiliary_variable_halos = {}
328 for auxiliary in solver.auxiliary_variables:
329 auxiliary.create_data_structures(patch_size, overlap, solver.grid_selector)
330 auxiliary.add_to_datamodel(peano_project)
332 (patch_size + 2 * overlap, patch_size + 2 * overlap, patch_size + 2 * overlap),
333 auxiliary.size,
334 auxiliary.name + "AuxiliaryVariablesWithHaloOf" + solver.name,
335 )
336 halo.grid_selector = solver.grid_selector
337 halo.generator = peano4.api.datamodel.PatchToDoubleArrayWithSmartPointer(halo, "double")
338 peano_project.datamodel.add_cell(halo)
339 solver._auxiliary_variable_halos[auxiliary.name] = halo
340
341
342def add_reconstruction_of_auxiliary_variables(task, solver):
343 """!
344
345 Make a reconstruction task of a solver fill the halos of the solver's
346 auxiliary variables
347
348 @param task: peano4.api.taskgraph.Task
349 The task which also reconstructs the unknowns.
350
351 """
352 Access = peano4.api.taskgraph.Task.DataAccess
353 for auxiliary in solver.auxiliary_variables:
354 halo = solver._auxiliary_variable_halos[auxiliary.name]
355 task.use_cell(auxiliary.patch, Access.READ_AND_WRITE)
356 task.use_cell(halo, Access.WRITE)
357 task.use_face(auxiliary.patch_overlap_current_time_step, Access.READ_AND_WRITE)
358 task.add_action_set(
359 toolbox.blockstructured.api.ReconstructPatchWithHalo(
360 patch=auxiliary.patch,
361 patch_overlap=auxiliary.patch_overlap_current_time_step,
362 reconstructed_patch=halo,
363 guard=solver.grid_selector.createCellGuard(),
364 add_assertions_to_halo_exchange=False,
365 additional_includes=solver._get_default_includes() + solver.user_solver_includes,
366 )
367 )
368
369
370def use_halos_of_auxiliary_variables(task, solver):
371 """! Let a task (typically the update of the cells) read the halos of the auxiliary variables """
372 for auxiliary in solver.auxiliary_variables:
373 task.use_cell(
374 solver._auxiliary_variable_halos[auxiliary.name],
375 peano4.api.taskgraph.Task.DataAccess.READ_AND_DISCARD,
376 )
377
378
379def volumes_expression(patch_size: int, halo: int) -> str:
380 """! C++ expression for the number of volumes of a patch with halo """
381 edge = f"({patch_size} + 2 * {halo})"
382 return f"({edge} * {edge} * (Dimensions == 3 ? {edge} : 1))"
383
384
385def code_to_assemble_kernel_input(solver, buffer: str, unknowns: str, halo_size: int, parameters_of=None) -> str:
386 """!
387
388 C++ code which assembles the input buffer of the compute kernels
389
390 The kernels work on one buffer that holds the unknowns of a volume
391 followed by the entries of all sets of auxiliary variables. The code
392 defines a std::vector<double> named buffer. It is empty if the solver has
393 no auxiliary variables.
394
395 @param unknowns: str
396 C++ expression for the unknowns of the patch with halo.
397
398 @param parameters_of: function
399 Maps a set onto the C++ expression of its patch with halo. Defaults to
400 the halo patches of the solver.
401
402 """
403 if not solver.auxiliary_variables:
404 return ""
405 if parameters_of is None:
406 parameters_of = lambda auxiliary: f"fineGridCell{solver._auxiliary_variable_halos[auxiliary.name].name}.value"
407 volumes = volumes_expression(solver.numerical_scheme.patch_size, halo_size)
408 stride = solver.unknowns + solver.number_of_auxiliary_variables
409 copies = "".join(
410 f"""
411 for (int k=0; k<{auxiliary.size}; k++) {{
412 {buffer}[volume*{stride} + {solver.auxiliary_variable_offset(auxiliary)} + k] = {parameters_of(auxiliary)}[volume*{auxiliary.size} + k];
413 }}"""
414 for auxiliary in solver.auxiliary_variables
415 )
416 return f"""
417 const int {buffer}Volumes = {volumes};
418 std::vector<double> {buffer}( {buffer}Volumes * {stride}, 0.0 );
419 for (int volume=0; volume<{buffer}Volumes; volume++) {{
420 for (int k=0; k<{solver.unknowns}; k++) {{
421 {buffer}[volume*{stride} + k] = {unknowns}[volume*{solver.unknowns} + k];
422 }}{copies}
423 }}
424"""
425
426
427def code_to_allocate_kernel_output(solver, buffer: str) -> str:
428 """! C++ code which defines the output buffer (a std::vector<double>) of the compute kernels """
429 if not solver.auxiliary_variables:
430 return ""
431 volumes = volumes_expression(solver.numerical_scheme.patch_size, 0)
432 stride = solver.unknowns + solver.number_of_auxiliary_variables
433 return f"""
434 const int {buffer}Volumes = {volumes};
435 std::vector<double> {buffer}( {buffer}Volumes * {stride}, 0.0 );
436"""
437
438
439def code_to_copy_back_kernel_output(solver, buffer: str, unknowns: str) -> str:
440 """! C++ code which copies the unknowns of the output buffer back into the patch """
441 if not solver.auxiliary_variables:
442 return ""
443 stride = solver.unknowns + solver.number_of_auxiliary_variables
444 return f"""
445 for (int volume=0; volume<{buffer}Volumes; volume++) {{
446 for (int k=0; k<{solver.unknowns}; k++) {{
447 {unknowns}[volume*{solver.unknowns} + k] = {buffer}[volume*{stride} + k];
448 }}
449 }}
450"""
451
452
453def add_auxiliary_variables_to_setup(
454 solver, initial_condition_task, project_on_faces_task, project_patch_onto_faces, roll_over_updated_face
455):
456 """!
457
458 Let the setup of a solver write its auxiliary variables
459
460 Auxiliary variables are written once, by their owner: the owner
461 initialises them in its initial condition task and projects them onto the
462 faces, so the halos can be reconstructed later on. A solver that merely
463 shares a set reads it after the owner has written it.
464
465 @param project_patch_onto_faces: class
466 The action set that projects a patch onto the faces
467 (exahype2.api.fv.actionsets.ProjectPatchOntoFaces).
468
469 @param roll_over_updated_face: class
470 The action set that rolls the projection over
471 (exahype2.api.fv.actionsets.RollOverUpdatedFace).
472
473 """
474 Access = peano4.api.taskgraph.Task.DataAccess
475 includes = solver._get_default_includes() + solver.user_solver_includes
476 for auxiliary in solver.auxiliary_variables:
477 # The setup is created before the numerical scheme adds the data to the project
478 auxiliary.create_data_structures(
479 solver.numerical_scheme.patch_size,
480 solver.numerical_scheme.halo_size,
481 solver.grid_selector,
482 )
483 if auxiliary.owner is solver:
484 initial_condition_task.use_cell(auxiliary.patch, Access.WRITE)
485 project_on_faces_task.use_cell(auxiliary.patch, Access.READ_AND_WRITE)
486 project_on_faces_task.use_face(auxiliary.patch_overlap_update, Access.TEMPORARY)
487 project_on_faces_task.use_face(auxiliary.patch_overlap_current_time_step, Access.WRITE)
488 project_on_faces_task.add_action_set(
489 project_patch_onto_faces(
490 auxiliary.patch,
491 auxiliary.patch_overlap_update,
492 guard=solver.grid_selector.createCellGuard(),
493 includes=includes,
494 )
495 )
496 project_on_faces_task.add_action_set(
497 roll_over_updated_face(
498 auxiliary.patch_overlap_current_time_step,
499 auxiliary.patch_overlap_update,
500 guard=solver.grid_selector.createFaceGuard(),
501 includes=includes,
502 )
503 )
504 auxiliary._setup_initial_condition_task = initial_condition_task
505 else:
506 owners_task = getattr(auxiliary, "_setup_initial_condition_task", None)
507 if owners_task is None:
508 raise ValueError(
509 f"Solver {solver.name} shares the auxiliary variables {auxiliary.name} with "
510 f"{auxiliary.owner.name}, but the setup of {auxiliary.owner.name} has not been created yet. "
511 f"Add {auxiliary.owner.name} to the project before {solver.name}."
512 )
513 initial_condition_task.add_dependency(owners_task)
514 initial_condition_task.use_cell(auxiliary.patch, Access.READ_AND_DISCARD)
515 project_on_faces_task.use_cell(auxiliary.patch, Access.READ_AND_WRITE)
A set of auxiliary variables of a solver.
owner(self)
The solver which initialises the set, i.e.
int size(self)
Total number of entries of the set, i.e.
Dict[str, int] variable_positions(self)
Position of each named variable within the set.
List[str] variable_names_per_entry(self)
One name per entry of the set, qualified by the set's name.
__init__(self, str name, Dict[str, int] variables)
str variables_declaration(self)
C++ struct which hosts the positions of the variables within the set.
create_data_structures(self, int patch_size, int overlap, grid_selector)
Create the cell and face data of the set.
str delta_argument_name(self)
Name of the argument behind deltaQ.
str argument_name(self)
Name of the argument of the PDE terms.
add_to_datamodel(self, peano_project)
Register the data with the project (once, even if the set is shared)
Patch implementation of a degree of freedom.
Definition Patch.py:7
List[str] split_signature(str signature)
Split a C++ parameter list at the commas that are not within template brackets.
str volumes_expression(int patch_size, int halo)
C++ expression for the number of volumes of a patch with halo.
str extend_signature(str signature, solver)
Add the pointers to the auxiliary variables of a solver to a PDE term's signature.
The task graph abstraction allows users to formalise their solver steps over small task graphs which ...
Definition __init__.py:1