SpECTRE Documentation Coverage Report
 Current view: top level - __w/spectre/spectre/docs/DevGuide - Databox.md Hit Total Coverage Commit: f1ddee3e40d81480e49140855d2b0e66fafaa908 Lines: 0 1 0.0 % Date: 2020-12-02 17:35:08 Legend: Lines: hit not hit
  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::strings 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 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::pairs 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::lists 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_nulls 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_nulls 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_nulls 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::lists. 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." 

 Generated by: LCOV version 1.14