SpECTRE Documentation Coverage Report
Current view: top level - IO/Observer - ReductionActions.hpp Coverage Total Hit
Commit: a37d7b3c1803b88b537d2aef1c27ca5e6b499398 Lines: 56.0 % 25 14
Test Date: 2026-11-12 04:47:19
Legend: Lines:     hit not hit

            Line data    Source code
       1            0 : // Distributed under the MIT License.
       2              : // See LICENSE.txt for details.
       3              : 
       4              : #pragma once
       5              : 
       6              : #include <cstddef>
       7              : #include <mutex>
       8              : #include <optional>
       9              : #include <string>
      10              : #include <tuple>
      11              : #include <unordered_map>
      12              : #include <utility>
      13              : #include <vector>
      14              : 
      15              : #include "DataStructures/DataBox/DataBox.hpp"
      16              : #include "DataStructures/TaggedTuple.hpp"
      17              : #include "IO/H5/AccessType.hpp"
      18              : #include "IO/H5/Dat.hpp"
      19              : #include "IO/H5/File.hpp"
      20              : #include "IO/Observer/Helpers.hpp"
      21              : #include "IO/Observer/ObservationId.hpp"
      22              : #include "IO/Observer/Protocols/ReductionDataFormatter.hpp"
      23              : #include "IO/Observer/Tags.hpp"
      24              : #include "Parallel/ArrayComponentId.hpp"
      25              : #include "Parallel/ArrayIndex.hpp"
      26              : #include "Parallel/GlobalCache.hpp"
      27              : #include "Parallel/Info.hpp"
      28              : #include "Parallel/Invoke.hpp"
      29              : #include "Parallel/Local.hpp"
      30              : #include "Parallel/NodeLock.hpp"
      31              : #include "Parallel/ParallelComponentHelpers.hpp"
      32              : #include "Parallel/Printf/Printf.hpp"
      33              : #include "Parallel/Reduction.hpp"
      34              : #include "Utilities/ErrorHandling/Assert.hpp"
      35              : #include "Utilities/ErrorHandling/Error.hpp"
      36              : #include "Utilities/GetOutput.hpp"
      37              : #include "Utilities/Gsl.hpp"
      38              : #include "Utilities/PrettyType.hpp"
      39              : #include "Utilities/ProtocolHelpers.hpp"
      40              : #include "Utilities/Requires.hpp"
      41              : #include "Utilities/Serialization/PupStlCpp17.hpp"
      42              : #include "Utilities/StdHelpers.hpp"
      43              : #include "Utilities/TMPL.hpp"
      44              : 
      45              : namespace observers {
      46              : 
      47              : /// \cond
      48              : template <class Metavariables>
      49              : struct ObserverWriter;
      50              : /// \endcond
      51              : 
      52            0 : namespace ThreadedActions {
      53              : /// \cond
      54              : struct CollectReductionDataOnNode;
      55              : struct WriteReductionData;
      56              : /// \endcond
      57              : }  // namespace ThreadedActions
      58              : 
      59              : /// Indicates no formatter is selected
      60            1 : struct NoFormatter {
      61            0 :   void pup(PUP::er& /*p*/) {}
      62              : };
      63              : 
      64              : /*!
      65              :  * \brief Rows of data to append to one `h5::Dat` subfile of the reductions
      66              :  * file with `ThreadedActions::WriteMultipleReductionDataRows`.
      67              :  */
      68            1 : struct DatRows {
      69              :   /// Name of the `h5::Dat` subfile, including a leading slash
      70            1 :   std::string subfile_name{};
      71              :   /// Column labels of the subfile
      72            1 :   std::vector<std::string> legend{};
      73              :   /// Rows to append. Each row must have as many entries as the `legend`.
      74            1 :   std::vector<std::vector<double>> rows{};
      75              : 
      76              :   // NOLINTNEXTLINE(google-runtime-references)
      77            0 :   void pup(PUP::er& p);
      78              : };
      79              : 
      80            0 : bool operator==(const DatRows& lhs, const DatRows& rhs);
      81            0 : bool operator!=(const DatRows& lhs, const DatRows& rhs);
      82              : 
      83              : namespace Actions {
      84              : /// \cond
      85              : struct ContributeReductionDataToWriter;
      86              : /// \endcond
      87              : 
      88              : /*!
      89              :  * \ingroup ObserversGroup
      90              :  * \brief Send reduction data to the observer group.
      91              :  *
      92              :  * Once everything at a specific `ObservationId` has been contributed to the
      93              :  * reduction, the groups reduce to their local nodegroup.
      94              :  *
      95              :  * The caller of this Action (which is to be invoked on the Observer parallel
      96              :  * component) must pass in an `observation_id` used to uniquely identify the
      97              :  * observation in time, the name of the `h5::Dat` subfile in the HDF5 file (e.g.
      98              :  * `/element_data`, where the slash is important), a `std::vector<std::string>`
      99              :  * of names of the quantities being reduced (e.g. `{"Time", "L1ErrorDensity",
     100              :  * "L2ErrorDensity"}`), and the `Parallel::ReductionData` that holds the
     101              :  * `ReductionDatums` containing info on how to do the reduction.
     102              :  *
     103              :  * The observer components need to know all expected reduction data types by
     104              :  * compile-time, so they rely on the
     105              :  * `Metavariables::observed_reduction_data_tags` alias to collect them in one
     106              :  * place. To this end, each Action that contributes reduction data must expose
     107              :  * the type alias as:
     108              :  *
     109              :  * \snippet ObserverHelpers.hpp make_reduction_data_tags
     110              :  *
     111              :  * Then, in the `Metavariables` collect them from all observing Actions using
     112              :  * the `observers::collect_reduction_data_tags` metafunction.
     113              :  *
     114              :  * This action also accepts a "formatter" that will be forwarded along with the
     115              :  * reduction data and used to print an informative message when the reduction is
     116              :  * complete. The formatter must conform to
     117              :  * `observers::protocols::ReductionDataFormatter`.
     118              :  *
     119              :  * This action also supports observing the intermediate stage of the reduction
     120              :  * over just the processing element that the element is currently on. This can
     121              :  * be useful e.g. to measure performance metric to assess load-balancing such as
     122              :  * the number of grid points on each core. Enable per-core observations by
     123              :  * passing `true` for the `observe_per_core` argument (default: `false`). The
     124              :  * data will be written in one H5 file per _node_ prefixed with the
     125              :  * `observers::Tags::ReductionFileName`, in a `Core{core_id}` subfile, where
     126              :  * `core_id` is an integer identifying the core across all nodes (see
     127              :  * `Parallel::my_proc`). For example, when running on 2 nodes with 2 cores each
     128              :  * you will end up with `Reductions0.h5` containing `/Core0/{subfile_name}.dat`
     129              :  * and `/Core1/{subfile_name}.dat`, and `Reductions1.h5` containing
     130              :  * `/Core2/{subfile_name}.dat` and `/Core3/{subfile_name}.dat`. This is in
     131              :  * addition to the usual reduction output over _all_ registered elements,
     132              :  * written to `Reductions.h5` (no node ID suffix in the file name).
     133              :  */
     134            1 : struct ContributeReductionData {
     135              :   template <typename ParallelComponent, typename DbTagsList,
     136              :             typename Metavariables, typename ArrayIndex, typename... Ts,
     137              :             typename Formatter = observers::NoFormatter>
     138            0 :   static void apply(db::DataBox<DbTagsList>& box,
     139              :                     Parallel::GlobalCache<Metavariables>& cache,
     140              :                     const ArrayIndex& array_index,
     141              :                     const observers::ObservationId& observation_id,
     142              :                     const Parallel::ArrayComponentId& sender_array_id,
     143              :                     const std::string& subfile_name,
     144              :                     const std::vector<std::string>& reduction_names,
     145              :                     Parallel::ReductionData<Ts...>&& reduction_data,
     146              :                     std::optional<Formatter>&& formatter = std::nullopt,
     147              :                     const bool observe_per_core = false) {
     148              :     db::mutate<Tags::ReductionData<Ts...>, Tags::ReductionDataNames<Ts...>,
     149              :                Tags::ContributorsOfReductionData>(
     150              :         [&array_index, &cache, &observation_id,
     151              :          reduction_data = std::move(reduction_data), &reduction_names,
     152              :          &sender_array_id, &subfile_name, &formatter, &observe_per_core](
     153              :             const gsl::not_null<std::unordered_map<
     154              :                 ObservationId, Parallel::ReductionData<Ts...>>*>
     155              :                 reduction_data_map,
     156              :             const gsl::not_null<
     157              :                 std::unordered_map<ObservationId, std::vector<std::string>>*>
     158              :                 reduction_names_map,
     159              :             const gsl::not_null<std::unordered_map<
     160              :                 ObservationId, std::unordered_set<Parallel::ArrayComponentId>>*>
     161              :                 reduction_observers_contributed,
     162              :             const std::unordered_map<
     163              :                 ObservationKey,
     164              :                 std::unordered_set<Parallel::ArrayComponentId>>&
     165              :                 observations_registered) mutable {  // NOLINT(spectre-mutable)
     166              :           ASSERT(
     167              :               observations_registered.find(observation_id.observation_key()) !=
     168              :                   observations_registered.end(),
     169              :               "Couldn't find registration key "
     170              :                   << observation_id.observation_key()
     171              :                   << " in the registered observers. Known IDs are "
     172              :                   << keys_of(observations_registered)
     173              :                   << "\nIt could be that you are using a nodegroup DG "
     174              :                      "collection but the observation code for the event hasn't "
     175              :                      "been updated to work with the nodegroup yet.");
     176              : 
     177              :           auto& contributed_array_ids =
     178              :               (*reduction_observers_contributed)[observation_id];
     179              :           if (UNLIKELY(contributed_array_ids.find(sender_array_id) !=
     180              :                        contributed_array_ids.end())) {
     181              :             ERROR("Already received reduction data to observation id "
     182              :                   << observation_id << " from array component id "
     183              :                   << sender_array_id);
     184              :           }
     185              :           contributed_array_ids.insert(sender_array_id);
     186              : 
     187              :           if (reduction_data_map->count(observation_id) == 0) {
     188              :             reduction_data_map->emplace(observation_id,
     189              :                                         std::move(reduction_data));
     190              :             reduction_names_map->emplace(observation_id, reduction_names);
     191              :           } else {
     192              :             if (UNLIKELY(reduction_names_map->at(observation_id) !=
     193              :                          reduction_names)) {
     194              :               using ::operator<<;
     195              :               ERROR("Reduction names differ at ObservationId "
     196              :                     << observation_id << " with the expected names being "
     197              :                     << reduction_names_map->at(observation_id)
     198              :                     << " and the received names being " << reduction_names);
     199              :             }
     200              :             reduction_data_map->operator[](observation_id)
     201              :                 .combine(std::move(reduction_data));
     202              :           }
     203              : 
     204              :           // Check if we have received all reduction data from the registered
     205              :           // elements. If so, we reduce to the local ObserverWriter nodegroup.
     206              :           if (UNLIKELY(
     207              :                   contributed_array_ids.size() ==
     208              :                   observations_registered.at(observation_id.observation_key())
     209              :                       .size())) {
     210              :             auto& local_writer = *Parallel::local_branch(
     211              :                 Parallel::get_parallel_component<ObserverWriter<Metavariables>>(
     212              :                     cache));
     213              :             auto& my_proxy =
     214              :                 Parallel::get_parallel_component<ParallelComponent>(cache);
     215              :             const std::optional<int> observe_with_core_id =
     216              :                 observe_per_core ? std::make_optional(Parallel::my_proc<int>(
     217              :                                        *Parallel::local_branch(my_proxy)))
     218              :                                  : std::nullopt;
     219              :             Parallel::threaded_action<
     220              :                 ThreadedActions::CollectReductionDataOnNode>(
     221              :                 local_writer, observation_id,
     222              :                 Parallel::make_array_component_id<ParallelComponent>(
     223              :                     array_index),
     224              :                 subfile_name, (*reduction_names_map)[observation_id],
     225              :                 std::move((*reduction_data_map)[observation_id]),
     226              :                 std::move(formatter), observe_with_core_id);
     227              :             reduction_data_map->erase(observation_id);
     228              :             reduction_names_map->erase(observation_id);
     229              :             reduction_observers_contributed->erase(observation_id);
     230              :           }
     231              :         },
     232              :         make_not_null(&box),
     233              :         db::get<Tags::ExpectedContributorsForObservations>(box));
     234              :     // Silence a gcc <= 9 unused-variable warning
     235              :     (void)observe_per_core;
     236              :   }
     237              : };
     238              : }  // namespace Actions
     239              : 
     240              : namespace ThreadedActions {
     241              : 
     242              : namespace ReductionActions_detail {
     243              : void append_to_reduction_data(
     244              :     const gsl::not_null<std::vector<double>*> all_reduction_data,
     245              :     const double t);
     246              : 
     247              : template <typename T>
     248              : void append_to_reduction_data(
     249              :     const gsl::not_null<std::vector<double>*> all_reduction_data,
     250              :     const std::vector<T>& t);
     251              : 
     252              : void append_to_reduction_data(
     253              :     gsl::not_null<std::vector<double>*> all_reduction_data,
     254              :     const std::array<double, 3>& t);
     255              : 
     256              : template <typename... Ts, size_t... Is>
     257              : void write_data(const std::string& subfile_name,
     258              :                 const std::string& input_source,
     259              :                 std::vector<std::string> legend, const std::tuple<Ts...>& data,
     260              :                 const std::string& file_prefix,
     261              :                 std::index_sequence<Is...> /*meta*/) {
     262              :   static_assert(sizeof...(Ts) > 0,
     263              :                 "Must be reducing at least one piece of data");
     264              :   std::vector<double> data_to_append{};
     265              :   EXPAND_PACK_LEFT_TO_RIGHT(
     266              :       append_to_reduction_data(&data_to_append, std::get<Is>(data)));
     267              : 
     268              :   if (legend.size() != data_to_append.size()) {
     269              :     ERROR(
     270              :         "There must be one name provided for each piece of data. You provided "
     271              :         << legend.size() << " names: '" << get_output(legend)
     272              :         << "' but there are " << data_to_append.size()
     273              :         << " pieces of data being reduced");
     274              :   }
     275              : 
     276              :   h5::H5File<h5::AccessType::ReadWrite> h5file(file_prefix + ".h5", true,
     277              :                                                input_source);
     278              :   constexpr size_t version_number = 0;
     279              :   auto& time_series_file = h5file.try_insert<h5::Dat>(
     280              :       subfile_name, std::move(legend), version_number);
     281              :   time_series_file.append(data_to_append);
     282              : }
     283              : 
     284              : void write_rows(const std::vector<DatRows>& data,
     285              :                 const std::string& input_source,
     286              :                 const std::string& file_prefix);
     287              : }  // namespace ReductionActions_detail
     288              : 
     289              : /*!
     290              :  * \brief Gathers all the reduction data from all processing elements/cores on a
     291              :  * node.
     292              :  */
     293            1 : struct CollectReductionDataOnNode {
     294              :  public:
     295              :   template <typename ParallelComponent, typename DbTagsList,
     296              :             typename Metavariables, typename ArrayIndex,
     297              :             typename... ReductionDatums,
     298              :             typename Formatter = observers::NoFormatter>
     299            0 :   static void apply(
     300              :       db::DataBox<DbTagsList>& box, Parallel::GlobalCache<Metavariables>& cache,
     301              :       const ArrayIndex& /*array_index*/,
     302              :       const gsl::not_null<Parallel::NodeLock*> node_lock,
     303              :       const observers::ObservationId& observation_id,
     304              :       Parallel::ArrayComponentId observer_group_id,
     305              :       const std::string& subfile_name,
     306              :       std::vector<std::string>&& reduction_names,
     307              :       Parallel::ReductionData<ReductionDatums...>&& received_reduction_data,
     308              :       std::optional<Formatter>&& formatter = std::nullopt,
     309              :       const std::optional<int> observe_with_core_id = std::nullopt) {
     310              :     // The below gymnastics with pointers is done in order to minimize the
     311              :     // time spent locking the entire node, which is necessary because the
     312              :     // DataBox does not allow any functions calls, both get and mutate, during
     313              :     // a mutate. This design choice in DataBox is necessary to guarantee a
     314              :     // consistent state throughout mutation. Here, however, we need to be
     315              :     // reasonable efficient in parallel and so we manually guarantee that
     316              :     // consistent state. To this end, we create pointers and assign to them
     317              :     // the data in the DataBox which is guaranteed to be pointer stable. The
     318              :     // data itself is guaranteed to be stable inside the ReductionDataLock.
     319              :     std::unordered_map<observers::ObservationId,
     320              :                        Parallel::ReductionData<ReductionDatums...>>*
     321              :         reduction_data = nullptr;
     322              :     std::unordered_map<ObservationId, std::vector<std::string>>*
     323              :         reduction_names_map = nullptr;
     324              :     std::unordered_map<observers::ObservationId,
     325              :                        std::unordered_set<Parallel::ArrayComponentId>>*
     326              :         reduction_observers_contributed = nullptr;
     327              :     Parallel::NodeLock* reduction_data_lock = nullptr;
     328              :     Parallel::NodeLock* reduction_file_lock = nullptr;
     329              :     size_t observations_registered_with_id = std::numeric_limits<size_t>::max();
     330              : 
     331              :     {
     332              :       const std::lock_guard hold_lock(*node_lock);
     333              :       db::mutate<Tags::ReductionData<ReductionDatums...>,
     334              :                  Tags::ReductionDataNames<ReductionDatums...>,
     335              :                  Tags::ContributorsOfReductionData, Tags::ReductionDataLock,
     336              :                  Tags::H5FileLock>(
     337              :           [&reduction_data, &reduction_names_map,
     338              :            &reduction_observers_contributed, &reduction_data_lock,
     339              :            &reduction_file_lock, &observation_id, &observer_group_id,
     340              :            &observations_registered_with_id](
     341              :               const gsl::not_null<std::unordered_map<
     342              :                   observers::ObservationId,
     343              :                   Parallel::ReductionData<ReductionDatums...>>*>
     344              :                   reduction_data_ptr,
     345              :               const gsl::not_null<
     346              :                   std::unordered_map<ObservationId, std::vector<std::string>>*>
     347              :                   reduction_names_map_ptr,
     348              :               const gsl::not_null<std::unordered_map<
     349              :                   observers::ObservationId,
     350              :                   std::unordered_set<Parallel::ArrayComponentId>>*>
     351              :                   reduction_observers_contributed_ptr,
     352              :               const gsl::not_null<Parallel::NodeLock*> reduction_data_lock_ptr,
     353              :               const gsl::not_null<Parallel::NodeLock*> reduction_file_lock_ptr,
     354              :               const std::unordered_map<
     355              :                   ObservationKey,
     356              :                   std::unordered_set<Parallel::ArrayComponentId>>&
     357              :                   observations_registered) {
     358              :             const ObservationKey& key{observation_id.observation_key()};
     359              :             const auto& registered_group_ids = observations_registered.at(key);
     360              :             if (UNLIKELY(registered_group_ids.find(observer_group_id) ==
     361              :                          registered_group_ids.end())) {
     362              :               ERROR("The observer group id "
     363              :                     << observer_group_id
     364              :                     << " was not registered for the observation id "
     365              :                     << observation_id);
     366              :             }
     367              :             reduction_data = &*reduction_data_ptr;
     368              :             reduction_names_map = &*reduction_names_map_ptr;
     369              :             reduction_observers_contributed =
     370              :                 &*reduction_observers_contributed_ptr;
     371              :             reduction_data_lock = &*reduction_data_lock_ptr;
     372              :             reduction_file_lock = &*reduction_file_lock_ptr;
     373              :             observations_registered_with_id =
     374              :                 observations_registered.at(key).size();
     375              :           },
     376              :           make_not_null(&box),
     377              :           db::get<Tags::ExpectedContributorsForObservations>(box));
     378              :     }
     379              : 
     380              :     ASSERT(
     381              :         observations_registered_with_id != std::numeric_limits<size_t>::max(),
     382              :         "Failed to set observations_registered_with_id when mutating the "
     383              :         "DataBox. This is a bug in the code.");
     384              : 
     385              :     bool send_data = false;
     386              :     // Now that we've retrieved pointers to the data in the DataBox we wish to
     387              :     // manipulate, lock the data and manipulate it.
     388              :     {
     389              :       const std::lock_guard hold_data_lock(*reduction_data_lock);
     390              :       auto& contributed_group_ids =
     391              :           (*reduction_observers_contributed)[observation_id];
     392              : 
     393              :       if (UNLIKELY(contributed_group_ids.find(observer_group_id) !=
     394              :                    contributed_group_ids.end())) {
     395              :         ERROR("Already received reduction data to observation id "
     396              :               << observation_id << " from array component id "
     397              :               << observer_group_id);
     398              :       }
     399              :       contributed_group_ids.insert(observer_group_id);
     400              : 
     401              :       // If requested, write the intermediate reduction data from the particular
     402              :       // core to one file per node. This allows measuring reduction data
     403              :       // per-core, e.g. performance metrics to assess load balancing.
     404              :       if (observe_with_core_id.has_value()) {
     405              :         auto reduction_data_this_core = received_reduction_data;
     406              :         reduction_data_this_core.finalize();
     407              :         auto reduction_names_this_core = reduction_names;
     408              :         auto& my_proxy =
     409              :             Parallel::get_parallel_component<ParallelComponent>(cache);
     410              :         const std::lock_guard hold_file_lock(*reduction_file_lock);
     411              :         ReductionActions_detail::write_data(
     412              :             "/Core" + std::to_string(observe_with_core_id.value()) +
     413              :                 subfile_name,
     414              :             observers::input_source_from_cache(cache),
     415              :             std::move(reduction_names_this_core),
     416              :             std::move(reduction_data_this_core.data()),
     417              :             Parallel::get<Tags::ReductionFileName>(cache) +
     418              :                 std::to_string(
     419              :                     Parallel::my_node<int>(*Parallel::local_branch(my_proxy))),
     420              :             std::make_index_sequence<sizeof...(ReductionDatums)>{});
     421              :       }
     422              : 
     423              :       if (reduction_data->find(observation_id) == reduction_data->end()) {
     424              :         // This Action has been called for the first time,
     425              :         // so all we need to do is move the input data to the
     426              :         // reduction_data in the DataBox.
     427              :         reduction_data->operator[](observation_id) =
     428              :             std::move(received_reduction_data);
     429              :       } else {
     430              :         // This Action is being called at least the second time
     431              :         // (but not the final time if on node 0).
     432              :         reduction_data->at(observation_id)
     433              :             .combine(std::move(received_reduction_data));
     434              :       }
     435              : 
     436              :       if (UNLIKELY(reduction_names.empty())) {
     437              :         ERROR(
     438              :             "The reduction names, which is a std::vector of the names of "
     439              :             "the columns in the file, must be non-empty.");
     440              :       }
     441              :       if (auto current_names = reduction_names_map->find(observation_id);
     442              :           current_names == reduction_names_map->end()) {
     443              :         reduction_names_map->emplace(observation_id,
     444              :                                      std::move(reduction_names));
     445              :       } else if (UNLIKELY(current_names->second != reduction_names)) {
     446              :         ERROR(
     447              :             "The reduction names passed in must match the currently "
     448              :             "known reduction names.");
     449              :       }
     450              : 
     451              :       // Check if we have received all reduction data from the Observer
     452              :       // group. If so we reduce to node 0 for writing to disk. We use a bool
     453              :       // `send_data` to allow us to defer the send call until after we've
     454              :       // unlocked the lock.
     455              :       if (reduction_observers_contributed->at(observation_id).size() ==
     456              :           observations_registered_with_id) {
     457              :         send_data = true;
     458              :         // We intentionally move the data out of the map and erase it
     459              :         // before call `WriteReductionData` since if the call to
     460              :         // `WriteReductionData` is inlined and we erase data from the maps
     461              :         // afterward we would lose data.
     462              :         reduction_names =
     463              :             std::move(reduction_names_map->operator[](observation_id));
     464              :         received_reduction_data =
     465              :             std::move(reduction_data->operator[](observation_id));
     466              :         reduction_observers_contributed->erase(observation_id);
     467              :         reduction_data->erase(observation_id);
     468              :         reduction_names_map->erase(observation_id);
     469              :       }
     470              :     }
     471              : 
     472              :     if (send_data) {
     473              :       auto& my_proxy =
     474              :           Parallel::get_parallel_component<ParallelComponent>(cache);
     475              :       Parallel::threaded_action<WriteReductionData>(
     476              :           Parallel::get_parallel_component<ObserverWriter<Metavariables>>(
     477              :               cache)[0],
     478              :           observation_id,
     479              :           Parallel::my_node<size_t>(*Parallel::local_branch(my_proxy)),
     480              :           subfile_name,
     481              :           // NOLINTNEXTLINE(bugprone-use-after-move)
     482              :           std::move(reduction_names), std::move(received_reduction_data),
     483              :           std::move(formatter));
     484              :     }
     485              :   }
     486              : };
     487              : 
     488              : /*!
     489              :  * \ingroup ObserversGroup
     490              :  * \brief Write reduction data to disk from node 0.
     491              :  */
     492            1 : struct WriteReductionData {
     493              :   template <typename ParallelComponent, typename DbTagsList,
     494              :             typename Metavariables, typename ArrayIndex,
     495              :             typename... ReductionDatums,
     496              :             typename Formatter = observers::NoFormatter>
     497            0 :   static void apply(
     498              :       db::DataBox<DbTagsList>& box, Parallel::GlobalCache<Metavariables>& cache,
     499              :       const ArrayIndex& /*array_index*/,
     500              :       const gsl::not_null<Parallel::NodeLock*> node_lock,
     501              :       const observers::ObservationId& observation_id,
     502              :       const size_t sender_node_number, const std::string& subfile_name,
     503              :       std::vector<std::string>&& reduction_names,
     504              :       Parallel::ReductionData<ReductionDatums...>&& received_reduction_data,
     505              :       std::optional<Formatter>&& formatter = std::nullopt) {
     506              :     if constexpr (not std::is_same_v<Formatter, observers::NoFormatter>) {
     507              :       static_assert(
     508              :           tt::assert_conforms_to_v<Formatter,
     509              :                                    protocols::ReductionDataFormatter>);
     510              :       static_assert(
     511              :           std::is_same_v<typename Formatter::reduction_data,
     512              :                          Parallel::ReductionData<ReductionDatums...>>,
     513              :           "Mismatch between the formatter's `reduction_data` type alias and "
     514              :           "the reduction data that is being reduced.");
     515              :     }
     516              :     // The below gymnastics with pointers is done in order to minimize the
     517              :     // time spent locking the entire node, which is necessary because the
     518              :     // DataBox does not allow any functions calls, both get and mutate, during
     519              :     // a mutate. This design choice in DataBox is necessary to guarantee a
     520              :     // consistent state throughout mutation. Here, however, we need to be
     521              :     // reasonable efficient in parallel and so we manually guarantee that
     522              :     // consistent state. To this end, we create pointers and assign to them
     523              :     // the data in the DataBox which is guaranteed to be pointer stable. The
     524              :     // data itself is guaranteed to be stable inside the ReductionDataLock.
     525              :     std::unordered_map<observers::ObservationId,
     526              :                        Parallel::ReductionData<ReductionDatums...>>*
     527              :         reduction_data = nullptr;
     528              :     std::unordered_map<ObservationId, std::vector<std::string>>*
     529              :         reduction_names_map = nullptr;
     530              :     std::unordered_map<observers::ObservationId, std::unordered_set<size_t>>*
     531              :         nodes_contributed = nullptr;
     532              :     Parallel::NodeLock* reduction_data_lock = nullptr;
     533              :     Parallel::NodeLock* reduction_file_lock = nullptr;
     534              :     size_t observations_registered_with_id = std::numeric_limits<size_t>::max();
     535              : 
     536              :     {
     537              :       const std::lock_guard hold_lock(*node_lock);
     538              :       db::mutate<Tags::ReductionData<ReductionDatums...>,
     539              :                  Tags::ReductionDataNames<ReductionDatums...>,
     540              :                  Tags::NodesThatContributedReductions, Tags::ReductionDataLock,
     541              :                  Tags::H5FileLock>(
     542              :           [&nodes_contributed, &reduction_data, &reduction_names_map,
     543              :            &reduction_data_lock, &reduction_file_lock, &observation_id,
     544              :            &observations_registered_with_id, &sender_node_number](
     545              :               const gsl::not_null<
     546              :                   typename Tags::ReductionData<ReductionDatums...>::type*>
     547              :                   reduction_data_ptr,
     548              :               const gsl::not_null<
     549              :                   std::unordered_map<ObservationId, std::vector<std::string>>*>
     550              :                   reduction_names_map_ptr,
     551              :               const gsl::not_null<std::unordered_map<
     552              :                   ObservationId, std::unordered_set<size_t>>*>
     553              :                   nodes_contributed_ptr,
     554              :               const gsl::not_null<Parallel::NodeLock*> reduction_data_lock_ptr,
     555              :               const gsl::not_null<Parallel::NodeLock*> reduction_file_lock_ptr,
     556              :               const std::unordered_map<ObservationKey, std::set<size_t>>&
     557              :                   nodes_registered_for_reductions) {
     558              :             const ObservationKey& key{observation_id.observation_key()};
     559              :             ASSERT(nodes_registered_for_reductions.find(key) !=
     560              :                        nodes_registered_for_reductions.end(),
     561              :                    "Performing reduction with unregistered ID key "
     562              :                        << observation_id.observation_key());
     563              :             const auto& registered_nodes =
     564              :                 nodes_registered_for_reductions.at(key);
     565              : 
     566              :             if (UNLIKELY(registered_nodes.find(sender_node_number) ==
     567              :                          registered_nodes.end())) {
     568              :               ERROR("Node " << sender_node_number
     569              :                             << " was not registered for the observation id "
     570              :                             << observation_id);
     571              :             }
     572              : 
     573              :             reduction_data = &*reduction_data_ptr;
     574              :             reduction_names_map = &*reduction_names_map_ptr;
     575              :             nodes_contributed = &*nodes_contributed_ptr;
     576              :             reduction_data_lock = &*reduction_data_lock_ptr;
     577              :             reduction_file_lock = &*reduction_file_lock_ptr;
     578              :             observations_registered_with_id =
     579              :                 nodes_registered_for_reductions.at(key).size();
     580              :           },
     581              :           make_not_null(&box),
     582              :           db::get<Tags::NodesExpectedToContributeReductions>(box));
     583              :     }
     584              : 
     585              :     ASSERT(
     586              :         observations_registered_with_id != std::numeric_limits<size_t>::max(),
     587              :         "Failed to set observations_registered_with_id when mutating the "
     588              :         "DataBox. This is a bug in the code.");
     589              : 
     590              :     bool write_to_disk = false;
     591              :     // Now that we've retrieved pointers to the data in the DataBox we wish to
     592              :     // manipulate, lock the data and manipulate it.
     593              :     {
     594              :       const std::lock_guard hold_lock(*reduction_data_lock);
     595              :       auto& nodes_contributed_to_observation =
     596              :           (*nodes_contributed)[observation_id];
     597              :       if (nodes_contributed_to_observation.find(sender_node_number) !=
     598              :           nodes_contributed_to_observation.end()) {
     599              :         ERROR("Already received reduction data at observation id "
     600              :               << observation_id << " from node " << sender_node_number);
     601              :       }
     602              :       nodes_contributed_to_observation.insert(sender_node_number);
     603              : 
     604              :       if (UNLIKELY(reduction_names.empty())) {
     605              :         ERROR(
     606              :             "The reduction names, which is a std::vector of the names of "
     607              :             "the columns in the file, must be non-empty.");
     608              :       }
     609              :       if (auto current_names = reduction_names_map->find(observation_id);
     610              :           current_names == reduction_names_map->end()) {
     611              :         reduction_names_map->emplace(observation_id,
     612              :                                      std::move(reduction_names));
     613              :       } else if (UNLIKELY(current_names->second != reduction_names)) {
     614              :         using ::operator<<;
     615              :         ERROR(
     616              :             "The reduction names passed in must match the currently "
     617              :             "known reduction names. Current ones are "
     618              :             << current_names->second << " while the received are "
     619              :             << reduction_names);
     620              :       }
     621              : 
     622              :       if (reduction_data->find(observation_id) == reduction_data->end()) {
     623              :         // This Action has been called for the first time,
     624              :         // so all we need to do is move the input data to the
     625              :         // reduction_data in the DataBox.
     626              :         reduction_data->operator[](observation_id) =
     627              :             std::move(received_reduction_data);
     628              :       } else {
     629              :         // This Action is being called at least the second time
     630              :         // (but not the final time if on node 0).
     631              :         reduction_data->at(observation_id)
     632              :             .combine(std::move(received_reduction_data));
     633              :       }
     634              : 
     635              :       // We use a bool `write_to_disk` to allow us to defer the data writing
     636              :       // until after we've unlocked the lock. For the same reason, we move the
     637              :       // final, reduced result into `received_reduction_data` and
     638              :       // `reduction_names`.
     639              :       if (nodes_contributed_to_observation.size() ==
     640              :           observations_registered_with_id) {
     641              :         write_to_disk = true;
     642              :         received_reduction_data =
     643              :             std::move(reduction_data->operator[](observation_id));
     644              :         reduction_names =
     645              :             std::move(reduction_names_map->operator[](observation_id));
     646              :         reduction_data->erase(observation_id);
     647              :         reduction_names_map->erase(observation_id);
     648              :         nodes_contributed->erase(observation_id);
     649              :       }
     650              :     }
     651              : 
     652              :     if (write_to_disk) {
     653              :       const std::lock_guard hold_lock(*reduction_file_lock);
     654              :       // NOLINTNEXTLINE(bugprone-use-after-move)
     655              :       received_reduction_data.finalize();
     656              :       if constexpr (not std::is_same_v<Formatter, NoFormatter>) {
     657              :         if (formatter.has_value()) {
     658              :           Parallel::printf(
     659              :               std::apply(*formatter, received_reduction_data.data()));
     660              :         }
     661              :       }
     662              :       ReductionActions_detail::write_data(
     663              :           subfile_name, observers::input_source_from_cache(cache),
     664              :           // NOLINTNEXTLINE(bugprone-use-after-move)
     665              :           std::move(reduction_names), std::move(received_reduction_data.data()),
     666              :           Parallel::get<Tags::ReductionFileName>(cache),
     667              :           std::make_index_sequence<sizeof...(ReductionDatums)>{});
     668              :     }
     669              :   }
     670              : };
     671              : 
     672              : /*!
     673              :  * \brief Write a single row of data to the reductions file without the need to
     674              :  * register or reduce anything, e.g. from a singleton component or from a
     675              :  * specific chare.
     676              :  *
     677              :  * Use observers::Actions::ContributeReductionData instead if you need to
     678              :  * perform a reduction before writing to the file. Use
     679              :  * `WriteMultipleReductionDataRows` to write to several subfiles, or several
     680              :  * rows, in a single call.
     681              :  *
     682              :  * Invoke this action on the observers::ObserverWriter component on node 0. Pass
     683              :  * the following arguments when invoking this action:
     684              :  *
     685              :  * - `subfile_name`: the name of the `h5::Dat` subfile in the HDF5 file. Include
     686              :  *   a leading slash, e.g., `/element_data`.
     687              :  * - `legend`: a `std::vector<std::string>` of column labels for the quantities
     688              :  *   being observed (e.g. `{"Time", "L1ErrorDensity", "L2ErrorDensity"}`).
     689              :  * - `reduction_data`: a `std::tuple<...>` with the data to write. The tuple can
     690              :  *   hold either `double`s or `std::vector<double>`s and is flattened before it
     691              :  *   is written to the file to form a single row of values. The total number of
     692              :  *   values must match the length of the `legend`.
     693              :  */
     694            1 : struct WriteReductionDataRow {
     695              :   /// \brief The apply call for the threaded action
     696              :   template <typename ParallelComponent, typename DbTagsList,
     697              :             typename Metavariables, typename ArrayIndex, typename... Ts>
     698            1 :   static void apply(db::DataBox<DbTagsList>& box,
     699              :                     Parallel::GlobalCache<Metavariables>& cache,
     700              :                     const ArrayIndex& /*array_index*/,
     701              :                     const gsl::not_null<Parallel::NodeLock*> node_lock,
     702              :                     const std::string& subfile_name,
     703              :                     std::vector<std::string> legend,
     704              :                     std::tuple<Ts...>&& reduction_data) {
     705              :     apply<ParallelComponent>(box, node_lock, cache, subfile_name,
     706              :                              std::move(legend), std::move(reduction_data));
     707              :   }
     708              : 
     709              :   // The local synchronous action
     710            0 :   using return_type = void;
     711              : 
     712              :   /// \brief The apply call for the local synchronous action
     713              :   template <typename ParallelComponent, typename DbTagList,
     714              :             typename Metavariables, typename... Ts>
     715            1 :   static return_type apply(
     716              :       db::DataBox<DbTagList>& box,
     717              :       const gsl::not_null<Parallel::NodeLock*> /*node_lock*/,
     718              :       Parallel::GlobalCache<Metavariables>& cache,
     719              :       const std::string& subfile_name, std::vector<std::string> legend,
     720              :       std::tuple<Ts...>&& reduction_data) {
     721              :     auto& reduction_file_lock =
     722              :         db::get_mutable_reference<Tags::H5FileLock>(make_not_null(&box));
     723              :     const std::lock_guard hold_lock(reduction_file_lock);
     724              :     ThreadedActions::ReductionActions_detail::write_data(
     725              :         subfile_name, observers::input_source_from_cache(cache),
     726              :         std::move(legend), std::move(reduction_data),
     727              :         Parallel::get<Tags::ReductionFileName>(cache),
     728              :         std::make_index_sequence<sizeof...(Ts)>{});
     729              :   }
     730              : };
     731              : 
     732              : /*!
     733              :  * \brief Write rows of data to one or more `h5::Dat` subfiles of the
     734              :  * reductions file, opening the file only once.
     735              :  *
     736              :  * Like `WriteReductionDataRow`, this action requires no registration or
     737              :  * reduction. Each `WriteReductionDataRow` call opens and closes the reductions
     738              :  * file, so prefer this action when writing to many subfiles at once (e.g. one
     739              :  * subfile per component of a function of time) or several rows to one subfile
     740              :  * at once.
     741              :  *
     742              :  * Invoke this action on the observers::ObserverWriter component on node 0 with
     743              :  * a `std::vector<observers::DatRows>`, one entry per subfile to write to. The
     744              :  * `legend` of each entry must have one name per column, and each of its `rows`
     745              :  * must have one value per column.
     746              :  */
     747            1 : struct WriteMultipleReductionDataRows {
     748              :   /// \brief The apply call for the threaded action
     749              :   template <typename ParallelComponent, typename DbTagsList,
     750              :             typename Metavariables, typename ArrayIndex>
     751            1 :   static void apply(db::DataBox<DbTagsList>& box,
     752              :                     Parallel::GlobalCache<Metavariables>& cache,
     753              :                     const ArrayIndex& /*array_index*/,
     754              :                     const gsl::not_null<Parallel::NodeLock*> node_lock,
     755              :                     const std::vector<DatRows>& data) {
     756              :     apply<ParallelComponent>(box, node_lock, cache, data);
     757              :   }
     758              : 
     759              :   // The local synchronous action
     760            0 :   using return_type = void;
     761              : 
     762              :   /// \brief The apply call for the local synchronous action
     763              :   template <typename ParallelComponent, typename DbTagList,
     764              :             typename Metavariables>
     765            1 :   static return_type apply(
     766              :       db::DataBox<DbTagList>& box,
     767              :       const gsl::not_null<Parallel::NodeLock*> /*node_lock*/,
     768              :       Parallel::GlobalCache<Metavariables>& cache,
     769              :       const std::vector<DatRows>& data) {
     770              :     auto& reduction_file_lock =
     771              :         db::get_mutable_reference<Tags::H5FileLock>(make_not_null(&box));
     772              :     const std::lock_guard hold_lock(reduction_file_lock);
     773              :     ThreadedActions::ReductionActions_detail::write_rows(
     774              :         data, observers::input_source_from_cache(cache),
     775              :         Parallel::get<Tags::ReductionFileName>(cache));
     776              :   }
     777              : };
     778              : 
     779              : }  // namespace ThreadedActions
     780              : }  // namespace observers
        

Generated by: LCOV version 2.0-1