Line data Source code
1 0 : \cond NEVER
2 : Distributed under the MIT License.
3 : See LICENSE.txt for details.
4 : \endcond
5 : # Motivation for SpECTRE's DataBox {#databox_foundations}
6 :
7 : \tableofcontents
8 :
9 : # Introduction {#databox_introduction}
10 : This page walks the user through the iterative process that led to SpECTRE's
11 : DataBox. At each stage, it discusses the advances and challenges that result
12 : from each improvement.
13 :
14 : # Towards SpECTRE's DataBox {#databox_towards_spectres_databox}
15 :
16 : ## Working without DataBoxes {#databox_working_without_databoxes}
17 : In a small C++ program, it is common to use the built-in fundamental types
18 : (bool, int, double, etc.) in computations, and to give variable names to
19 : objects of these types. For example, a section of a small program may look like
20 : this:
21 :
22 : \snippet Test_DataBoxDocumentation.cpp working_without_databoxes_small_program_1
23 :
24 : What changes as our program's size increases in scale? In SpECTRE, one of our
25 : driving design goals is modularity. In other words, functionality should be
26 : easy to swap in and out as desired. These smaller modules are easier to test,
27 : and keep the code flexible. We could wrap our calculation in such a module,
28 : which would then allow us to decouple the initial setup of the variables from
29 : where they are used in calculations:
30 :
31 : \snippet Test_DataBoxDocumentation.cpp working_without_databoxes_mass_compute
32 :
33 : \snippet Test_DataBoxDocumentation.cpp working_without_databoxes_accel_compute
34 :
35 : Our small program can now be written as:
36 :
37 : \snippet Test_DataBoxDocumentation.cpp working_without_databoxes_small_program_2
38 :
39 : One advantage is immediate: we are free to add other computation modules that
40 : are independently testable and reusable. As the number of routines grows, we
41 : can even begin to write routines that work on top of existing ones.
42 :
43 : \snippet Test_DataBoxDocumentation.cpp working_without_databoxes_force_compute
44 :
45 : While we have made progress, two problems arise. Our first problem is that as
46 : the number of quantities grows, it becomes more unwieldy to have to specify
47 : each function argument in our routine. The second problem is worse: the
48 : arguments passed to functions can be transposed and the program will still
49 : compile and run, but produce incorrect output. For example, the following two
50 : lines are equally well-formed from the point of view of the program:
51 :
52 : \snippet Test_DataBoxDocumentation.cpp working_without_databoxes_failed_accel
53 :
54 : Every time we call `acceleration_compute` we need to make sure we pass in the
55 : arguments in the correct order. In large programs where `acceleration_compute`
56 : is called many times, it becomes inevitable that the arguments will be
57 : accidentally transposed. We can address the two problems described above with a
58 : `std::map`, the first container we'll consider in this series.
59 :
60 : ##A std::map DataBox {#databox_a_std_map_databox}
61 : We can encapsulate the variables we use in a `std::map`, and the first half of
62 : our small program example now looks like this:
63 :
64 : \snippet Test_DataBoxDocumentation.cpp std_map_databox_small_program_1
65 :
66 : We have not yet taken full advantage of the encapsulation that `std::map`
67 : provides. We do so by rewriting our other routines to take only a single
68 : argument, i.e. the `std::map` itself:
69 :
70 : \snippet Test_DataBoxDocumentation.cpp std_map_databox_mass_compute
71 :
72 : \snippet Test_DataBoxDocumentation.cpp std_map_databox_accel_compute
73 :
74 : Notice that this solves the problem of having to provide the arguments
75 : in the correct order to every function call of `acceleration_compute`:
76 :
77 : \snippet Test_DataBoxDocumentation.cpp std_map_databox_force_compute
78 :
79 :
80 : Our small program now looks like:
81 :
82 : \snippet Test_DataBoxDocumentation.cpp std_map_databox_small_program_2
83 :
84 : Within each function, we no longer need to worry about passing in the arguments
85 : in the correct order. This is a great improvement, but our reliance on proper
86 : names does leave us open to the following mistake:
87 :
88 : ~~~{.c}
89 : // returns 0 without emitting an error!
90 : return naive_databox["MisspelledKey"];
91 : ~~~
92 :
93 : In the above example, the map is asked to return a value given a key
94 : that does not exist! As written, however, the program is well-formed and no
95 : error is emitted. (In the case of std::map, [a value is created.]
96 : (https://en.cppreference.com/w/cpp/container/map/operator_at)) Because the keys
97 : are indistinguishable from their type alone, the mistake cannot be caught at
98 : compile time. In our example, the mistake won't even be caught at run time. The
99 : run time portion of a SpECTRE calculation will typically be much longer (up to
100 : thousands of times longer!) than the compile time portion, so it is critical to
101 : catch costly mistakes like this as early into the calculation as possible.
102 : Although names encoded as `std::string` cannot be distinguished by the
103 : compiler, names encoded as types *can* be. This is possible with C++'s static
104 : typing, and to take advantage of this we need a container that is
105 : *heterogeneous*, that is, capable of holding objects of different types.
106 :
107 : ## A std::tuple DataBox {#databox_a_std_tuple_databox}
108 :
109 : A well-documented example of a fixed-size heterogeneous container of types is
110 : [std::tuple](https://en.cppreference.com/w/cpp/utility/tuple):
111 :
112 : \snippet Test_DataBoxDocumentation.cpp std_tuple_databox_1
113 :
114 : The contents of the `std_tuple` are obtained using
115 : [std::get](https://en.cppreference.com/w/cpp/utility/tuple/get):
116 :
117 : \snippet Test_DataBoxDocumentation.cpp std_tuple_databox_2
118 :
119 : In the above, we can see that we have promoted our keys from different values
120 : all of type `std::string` to different types entirely. We are not limited to
121 : fundamental types, we are free to make our own structs that serve as keys.
122 : These user-created types are called *tags*.
123 :
124 : As the sole purpose of the tag is to provide the compiler with a type
125 : distinguishable from other types, they can be as simple as the following:
126 :
127 : \snippet Test_DataBoxDocumentation.cpp std_tuple_tags
128 :
129 : Note that we have now promoted `Velocity`, `Radius`, etc. from being *values*
130 : associated with `std::string`s at run time, to *types* distinguishable from
131 : other types at compile time. A large portion of SpECTRE is designed with the
132 : philosophy of enlisting the help of the compiler in assuring the correctness
133 : of our programs. An example of a `std::tuple` making use of these tags might
134 : look like:
135 :
136 : ~~~{.c}
137 : // Note: This won't work!
138 : std::tuple<Velocity, Radius, Density, Volume> sophomore_databox =
139 : std::make_tuple(4.0, 2.0, 0.5, 10.0);
140 : ~~~
141 :
142 : Unfortunately, this will not work. The types passed as template parameters to
143 : `std::tuple` must also be the types of the arguments passed to
144 : `std::make_tuple`. Using a `std::pair`, we could write the above as:
145 :
146 : \snippet Test_DataBoxDocumentation.cpp std_tuple_small_program_1
147 :
148 : What remains is to rewrite our functions to use `std::tuple` instead of
149 : `std::map`. Note that since we are now using a heterogeneous container of
150 : potentially unknown type, our functions must be templated on the
151 : pairs used to create the `sophomore_databox`. Our functions then look like:
152 :
153 : \snippet Test_DataBoxDocumentation.cpp std_tuple_mass_compute
154 :
155 : \snippet Test_DataBoxDocumentation.cpp std_tuple_acceleration_compute
156 :
157 : \snippet Test_DataBoxDocumentation.cpp std_tuple_force_compute
158 :
159 : Using all these `std::pair`s to get our `std::tuple` to work is a bit
160 : cumbersome. There is another way to package together the tagging ability
161 : of the struct names with the type information of the values we wish to store.
162 : To do this we need to make modifications to both our tags as well as our
163 : %Databox implementation. This is what is done in SpECTRE's
164 : `tuples::TaggedTuple`, which is an improved implementation of `std::tuple` in
165 : terms of both performance and interface.
166 :
167 : ##A TaggedTuple DataBox {#databox_a_taggedtuple_databox}
168 :
169 : TaggedTuple is an implementation of a compile time container where the keys
170 : are tags.
171 :
172 : Tags that are compatible with SpECTRE's `tuples::TaggedTuple` must have the
173 : type alias `type` in their structs. This type alias carries the type
174 : information of the data we wish to store in the databox. `tuples::TaggedTuple`
175 : is able to make use of this type information so we won't need auxiliary
176 : constructs such as `std::pair` to package this information together anymore.
177 : Our new tags now look like:
178 :
179 : \snippet Test_DataBoxDocumentation.cpp tagged_tuple_tags
180 :
181 : We are now able to create the `junior_databox` below in the same way we
182 : initially wished to create the `sophomore_databox` above:
183 :
184 : \snippet Test_DataBoxDocumentation.cpp tagged_tuple_databox_1
185 :
186 : Our functions similarly simplify:
187 :
188 : \snippet Test_DataBoxDocumentation.cpp tagged_tuple_mass_compute
189 :
190 : \snippet Test_DataBoxDocumentation.cpp tagged_tuple_acceleration_compute
191 :
192 : \snippet Test_DataBoxDocumentation.cpp tagged_tuple_force_compute
193 :
194 : In each of these iterations of the Databox, we started with initial quantities
195 : and computed subsequent quantities. Let us consider again `force_compute`, in
196 : which `mass` and `acceleration` are recomputed for every call to
197 : `force_compute`. If `Mass` and `Acceleration` were tags somewhow, that is, if we
198 : could compute them once, place them in the databox, and get them back out
199 : through the use of tags, we could get around this problem. We are now ready to
200 : consider SpECTRE's DataBox, which provides the solution to this problem in the
201 : form of `ComputeTags`.
202 :
203 : # SpECTRE's DataBox {#databox_a_proper_databox}
204 :
205 : A brief description of SpECTRE's DataBox: a TaggedTuple with compute-on-demand.
206 : For a detailed description of SpECTRE's DataBox, see the
207 : \ref DataBoxGroup "DataBox documentation".
208 :
209 : ## SimpleTags {#databox_documentation_for_simple_tags}
210 : Just as we needed to modify our tags to make them compatible with
211 : `TaggedTuple`, we need to again modify them for use with DataBox. Our ordinary
212 : tags become SpECTRE's SimpleTags:
213 :
214 : \snippet Test_DataBoxDocumentation.cpp proper_databox_tags
215 :
216 : As seen above, SimpleTags have a `type` and a `name` in their struct.
217 : When creating tags for use with a DataBox, we must make sure to the tag
218 : inherits from one of the existing DataBox tag types such as `db::SimpleTag`.
219 : We now create our first DataBox using these `SimpleTags`:
220 :
221 : \snippet Test_DataBoxDocumentation.cpp refined_databox
222 :
223 : We can get our quantities out of the DataBox by using `db::get`:
224 :
225 : \snippet Test_DataBoxDocumentation.cpp refined_databox_get
226 :
227 : So far, the usage of DataBox has been similar to the usage of TaggedTuple. To
228 : address the desire to combine the functionality of tags with the modularity of
229 : functions, DataBox provides ComputeTags.
230 :
231 : ## ComputeTags {#databox_documentation_for_compute_tags}
232 : ComputeTags are used to tag functions that are used in conjunction with a
233 : DataBox to produce a new quantity. ComputeTags look like:
234 :
235 : \snippet Test_DataBoxDocumentation.cpp compute_tags
236 :
237 : ComputeTags inherit from `db::ComputeTag`, and it is convenient to have them
238 : additionally inherit from an existing SimpleTag (in this case `Mass`) so that
239 : the quantity `MassCompute` can be obtained through the SimpleTag `Mass`.
240 : We use the naming convention `TagNameCompute` so that `TagNameCompute` and
241 : `TagName` appear next to each other in documentation that lists tags in
242 : alphabetical order.
243 :
244 : We have also added the type alias `argument_tags`, which is necessary in order
245 : to refer to the correct tagged quantities in the DataBox.
246 :
247 : \note
248 : The `tmpl::list` used in the type alias is a contiguous container only
249 : holding types. That is, there is no variable runtime data associated with it
250 : like there is for `std::tuple`, which is a container associating types with
251 : values. `tmpl::list`s are useful in situations when one is working with
252 : multiple tags at once.
253 :
254 : Using nested type aliases to pass around information at compile time is a
255 : common pattern in SpECTRE. Let us see how we can compute our beloved quantity
256 : of mass times acceleration:
257 :
258 : \snippet Test_DataBoxDocumentation.cpp compute_tags_force_compute
259 :
260 : And that's it! `db::get` utilizes the `argument_tags` specified in
261 : `ForceCompute` to determine which items to get out of the `refined_databox`.
262 : With the corresponding quantities in hand, `db::get` passes them as arguments
263 : to the `function` specified in `ForceCompute`. This is why every `ComputeTag`
264 : must have an `argument_tags` as well as a `function` specified; this is the
265 : contract with DataBox they must satisfy in order to enjoy the full benefits of
266 : DataBox's generality.
267 :
268 : ## Mutating DataBox items {#databox_documentation_for_mutate_tags}
269 : It is reasonable to expect that in a complicated calculation, we will encounter
270 : time-dependent or iteration-dependent variables. As a result, in addition to
271 : adding and retrieving items from our DataBox, we also need a way to *mutate*
272 : quantities already present in the DataBox. This can be done via
273 : `db::mutate_apply` and can look like the following:
274 :
275 : \note
276 : There is an alternative to `db::mutate_apply`, `db::mutate`. See the
277 : \ref DataBoxGroup "DataBox documentation" for more details.
278 :
279 : \snippet Test_DataBoxDocumentation.cpp mutate_tags
280 : \snippet Test_DataBoxDocumentation.cpp time_dep_databox
281 :
282 : \note
283 : The `not_null`s here are used to give us the assurance that the pointers
284 : `time` and `falling_speed` are not null pointers. Using raw pointers alone, we
285 : risk running into segmentation faults if we dereference null pointers. With
286 : `not_null`s we instead run into a run time error that tells us what went wrong.
287 : For information on the usage of `not_null`, see the documentation for Gsl.hpp.
288 :
289 : In the above `db::mutate_apply` example, we are changing two values in the
290 : DataBox using four values from the DataBox. The mutated quantities must be
291 : passed in as `gsl::not_null`s to the lambda. The non-mutated quantities are
292 : passed in as const references to the lambda.
293 :
294 : \note
295 : It is critical to guarantee that there is a strict demarcation between
296 : pre-`db::mutate` and post-`db::mutate` quantities. `db::mutate` provides this
297 : guarantee via a locking mechanism; within one mutate call, all initial
298 : pre-mutated quantities are obtained from the DataBox before performing a single
299 : mutation.
300 :
301 : \note
302 : The mutate functions described above are the only accepted ways to edit data
303 : in the Databox. It is technically possible to use pointers or references to
304 : edit data stored in the Databox, but this bypasses the compute tags
305 : architecture. All changes to the Databox must be made by the Databox itself
306 : via mutate functions.
307 :
308 : From the above, we can see that the different kinds of tags are provided in two
309 : different `tmpl::list`s. The `MutateTags`, also called `ReturnTags`, refer to
310 : the quantities in the DataBox we wish to mutate, and the `ArgumentTags` refer to
311 : additional quantities we need from the DataBox to complete our computation. We
312 : now return to the recurring question of how to make this construction more
313 : modular.
314 :
315 : We have now worked our way up to SpECTRE's DataBox, but as we can see in the
316 : above `db::mutate_apply` example, the lambda used to perform the mutation ends
317 : up being independent of any tags or template parameters! This means we can
318 : factor it out and place it in its own module, where it can be tested
319 : independently of the DataBox.
320 :
321 : # Toward SpECTRE's Actions {#databox_towards_actions}
322 :
323 : ## Mutators {#databox_documentation_for_mutators}
324 : These constructs that exist independently of the DataBox are the precursors to
325 : SpECTRE's *Actions*. As they are designed to be used with `db::mutate` and
326 : `db::mutate_apply`, we give them the name *Mutators*. Here is the above lambda
327 : written as a Mutator-prototype, a struct-with-void-apply:
328 :
329 : \snippet Test_DataBoxDocumentation.cpp intended_mutation
330 :
331 : The call to `db::mutate_apply` has now been made much simpler:
332 :
333 : \snippet Test_DataBoxDocumentation.cpp time_dep_databox2
334 :
335 : There is a key step that we take here after this point, to make our
336 : struct-with-void-apply into a proper Mutator. As we will see, this addition
337 : will allow us to entirely divorce the internal details of `IntendedMutation`
338 : from the mechanism through which we update the DataBox. The key step is to
339 : add type aliases to `IntendedMutation`:
340 :
341 : \snippet Test_DataBoxDocumentation.cpp intended_mutation2
342 :
343 : We are now able to write our call to `db::mutate_apply` in the following way:
344 :
345 : \snippet Test_DataBoxDocumentation.cpp time_dep_databox3
346 :
347 : As we saw earlier with `QuantityCompute`, we found that we were able to imbue
348 : structs with the ability to "read in" types specified in other structs, through
349 : the use of templates and member type aliases. This liberated us from having to
350 : hard-code in specific types. We notice immediately that `IntendedMutation` is a
351 : hard-coded type that we can factor out in favor of a template parameter:
352 :
353 : \snippet Test_DataBoxDocumentation.cpp my_first_action
354 :
355 : Note how the `return_tags` and `argument_tags` are used as metavariables and
356 : are resolved by the compiler. Our call to `db::mutate_apply` has been fully
357 : wrapped and now takes the form:
358 :
359 : \snippet Test_DataBoxDocumentation.cpp time_dep_databox4
360 :
361 : The details of applying Mutators to the DataBox are entirely handled by
362 : `MyFirstAction`, with the details of the specific Mutator itself entirely
363 : encapsulated in `IntendedMutation`.
364 :
365 : SpECTRE algorithms are decomposed into Actions which can depend on more
366 : things than we have considered here. Feel free to look at the
367 : existing \ref ActionsGroup "actions that have been written." The intricacies of
368 : Actions at the level that SpECTRE uses them is the subject of a future addition
369 : to the \ref dev_guide "Developer's Guide."
|