SpECTRE Documentation Coverage Report
Current view: top level - __w/spectre/spectre/docs/DevGuide - Databox.md Hit Total Coverage
Commit: 3c072f0ce967e2e56649d3fa12aa2a0e4fe2a42e Lines: 0 1 0.0 %
Date: 2024-04-23 20:50:18
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::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."

Generated by: LCOV version 1.14