diff --git a/CMakeLists.txt b/CMakeLists.txt index e467de9..cc908e9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,7 +32,7 @@ find_package( sbnobj REQUIRED ) find_package( sbnanaobj REQUIRED ) find_package( CLHEP REQUIRED ) find_package( ROOT REQUIRED ) -# find_package( Boost COMPONENTS system filesystem REQUIRED ) +find_package( Boost COMPONENTS system filesystem REQUIRED ) # macros for dictionary and simple_plugin include(ArtDictionary) diff --git a/sbnalg/CMakeLists.txt b/sbnalg/CMakeLists.txt index 3ca33b4..c576175 100644 --- a/sbnalg/CMakeLists.txt +++ b/sbnalg/CMakeLists.txt @@ -1,2 +1,3 @@ add_subdirectory(Geometry) +add_subdirectory(Utilities) add_subdirectory(gallery) diff --git a/sbnalg/Utilities/AssnsCrosser.h b/sbnalg/Utilities/AssnsCrosser.h new file mode 100644 index 0000000..8427ea8 --- /dev/null +++ b/sbnalg/Utilities/AssnsCrosser.h @@ -0,0 +1,1831 @@ +/** + * @file sbnalg/Utilities/AssnsCrosser.h + * @brief Unit test for `sbn::ns::util::AssnsCrosser` class and utilities. + * @author Gianluca Petrillo (petrillo@slac.stanford.edu) + * @date June 9, 2023 + */ + +#ifndef SBNALG_UTILITIES_ASSNSCROSSER_H +#define SBNALG_UTILITIES_ASSNSCROSSER_H + +// LArSoft libraries +#include "larcorealg/CoreUtils/DebugUtils.h" // lar::debug::demangle() +#include "larcorealg/CoreUtils/enumerate.h" + +// framework libraries +#include "canvas/Persistency/Common/Assns.h" +#include "canvas/Persistency/Common/Ptr.h" +#if defined CANVAS_DEC_VERSION && (CANVAS_DEC_VERSION >= 31100) +# include "canvas/Persistency/Common/ProductPtr.h" +#endif +#include "canvas/Persistency/Provenance/BranchDescription.h" +#include "canvas/Persistency/Provenance/ProductID.h" +#include "canvas/Utilities/InputTag.h" +#include "canvas/Utilities/Exception.h" + +// C/C++ standard libraries +#include // std::set_difference(), std::any_of(), ... +#include // std::mem_fn() +#include // std::move(), std::forward() +#include +#include +#include +#include +#include +#include +#include +#include +#include // std::move_iterator, std::back_inserter +#include // std::logic_error +#include // std::is_constructible_v, std::enable_if_t... +#include +#include + + +namespace sbn::ns::util { + + class InputSpec; + template class StartSpec; + template class StartSpecs; + template class InputSpecs; + template using hopTo = InputSpecs; + template using startFrom = StartSpecs; + template class AssnsCrosser; + + template + AssnsCrosser makeAssnsCrosser + (Event const& event, InputSpecs... inputSpecs); + + template + AssnsCrosser makeAssnsCrosser( + Event const& event, + StartSpecs, InputSpecs... inputSpecs + ); + + std::ostream& operator<< (std::ostream& out, InputSpec const& spec); + template + std::ostream& operator<< (std::ostream& out, StartSpec const& spec); + template + std::ostream& operator<< (std::ostream& out, InputSpecs const& specs); + template + std::ostream& operator<< (std::ostream& out, StartSpecs const& specs); + + namespace details { + template class SpecBase; + template class InputSpecsBase; + template class AssnsMap; + template class AssnsCrosserTypes; + template struct PointerSelector; + using SupportedInputSpecs = std::variant< + std::monostate + , art::InputTag + , art::ProductID + >; + template + using SupportedStartSpecs = std::variant< + std::monostate + , art::InputTag + , art::ProductID + , art::Ptr +#if defined CANVAS_DEC_VERSION && (CANVAS_DEC_VERSION >= 531100) + , art::ProductPtr +#endif + , std::vector> + >; + + } // // namespace sbn::ns::util::details + +} // namespace sbn::ns::util + +// ----------------------------------------------------------------------------- +/** + * @brief Builds multi-hop one-to-many associations from associated pairs. + * @tparam Key the type of the data to associate to + * @tparam OtherTypes intermediate types to reach the target type (the last one) + * + * This class facilitates the crossing of multi-level associations. + * For example, suppose the data contains associations between hits + * (`recob::Hit`) and tracks (`recob::Track`) and between tracks and particle + * flow objects (`recob::PFParticle`). Starting from a particle flow object + * (_PFO_ from now on), we want to know which hits it is associated to. + * We need therefore to cross and join two associations. This problem is solved + * by using a + * `sbn::ns::util::AssnsCrosser` + * object. + * + * This class supports any number of indirections ("hops"). + * + * One major issue in establishing the chain is to find the relevant association + * data products. There are one key type (`Key`) and one or more types to + * hop through until the target type is reached (`OtherTypes`, the last one of + * which is the target type). Assuming there are _N_ types listed in + * `OtherTypes`, and calling `Target` the last one (`OtherTypes[N-1]`), + * there are as well _N_ hops to follow: + * `Key` → `OtherTypes[0]`, `OtherTypes[0]` → `OtherTypes[1]`, ... + * up to `OtherTypes[N-2]` → `Target`. + * The interface of this object requires some information for each of the _N_ + * hops. + * + * Currently the following patterns are supported: + * + * * the data product input tag of all associations are known in advance, + * and there is only one of them. This is the simplest case for + * implementation. On the other end it may be hard for the user to know which + * are all the involved input tags, and the requirement of having a single + * association data product for each hop may be a deal-breaker. + * * the data product input tag of all associations are known in advance, + * and there may be more of them per hop. This is the case with fewest + * assumptions. As in the previous case, it may be hard for the user to know + * which are all the involved input tags. + * * the data product of the first association is known, but not all the others + * are. In that case, one assumption can be that the relevant associations + * are created by the same module and with the same label as the data product + * at the right side of the association. In the example above, this situation + * translates into knowing the tag for the track/PFO association, but not the + * hit/track one; and then the hit/track associations would be assumed to + * have been created by the same module, and with the same tag, which also + * created the track collection (because `recob::Track` is the object on the + * right of the known track/PFO association). This case is currently + * supported only when that assumption holds; otherwise, the behaviour is + * undefined. + * * the data product of the last association is known, but not all the others + * are. The assumption here may be the mirror of the one in the previous + * point. In the example above, this situation translates into knowing the + * tag for the hit/track association, but not the the track/PFO one; and then + * the track/PFO associations would be assumed to have been created by the + * same module, and with the same tag, which also created the PFO collection + * (which we know to be not likely). This case is currently supported only + * when that assumption holds; otherwise, the behaviour is undefined. + * * the data product of the starting data product (strictly speaking not the + * first association) is known, but not all the association data product tags + * are. This case is similar to the case where the first association was + * known, as described above, and it is supported under the same assumptions, + * which in this case extend to the first association as well. + * + * The result can be limited to a selected list of key entries by listing the + * desired elements in the first constructor argument (see `StartSpecs`, alias + * `startFrom`). This mirrors the feature of `art::FindXxx`, but keep in mind + * that while there the lookup is by index of the start list, in this object + * the lookup is by pointer. This also implicitly quenches duplicates in the + * input list, and there is no guarantee that the associations are presented + * in the same key order as the start list (in fact, the order of the results + * is not even defined in this object). + * + * + * ### Many-to-one associations + * + * The support for one-to-many associations in the hopping direction is full. + * In the presence of many-to-one associations, there are some things to be + * kept in mind. + * + * In the case of many-to-one associations, the same target object may appear + * associated to several keys. + * + * The list of target objects associated to a key has an unspecified order and + * it _can_ contain duplicates. For example, in a "diamond" association: + * + * B1 + * / \ + * A1 C1 + * \ / + * B2 + * + * that is an association `A1` ↔ `B1`, `A1` ↔ `B2`, + * `C1` ↔ `B1` and `C1` ↔ `B2`, `C1` will appear in the list of + * `C`s associated with `A1` twice, because there are two paths connecting + * `A1` and `C1`. + * + * + * ### Comparison with `art::FindManyP` + * + * Both `art::FindManyP` and `sbn::ns::util::AssnsCrosser`: + * * support two directly associated data products + * (but then there is little reason to use `AssnsCrosser` over `FindManyP`). + * * precompute all the information at construction, so they are better + * instantiated once. + * * yield for each associated key a vector of _art_ pointers to the associated + * target elements. + * * support a generic `art::Event`-like interface, including (in principle) + * `gallery::Event`. + * + * Differences include: + * * `AssnsCrosser` interface only covers the functionality of `art::FindManyP` + * and `art::FindOneP`, not `art::FindMany`. + * * of course, `AssnsCrosser` supports _indirectly_ associated data products. + * * the assumption on whether an association is one-to-one or one-to-many + * is reflected in which `art::FindXxx` class is chosen (respectively + * `art::FindOneP` and `art::FindManyP`), while here the same class + * `AssnsCrosser` is used, and the assumption is reflected on whether + * `assPtr()` or `assPtrs()` method is used for the query. + * * `AssnsCrosser` indexes by _art_ pointer of the key, while `art::FindManyP` + * indexes by the position of the key in the list specified as input (which + * is bound to match the pointer `key()` when a whole handle is specified as + * input). + * * `AssnsCrosser` does not support target metadata (the metadata on the + * intermediate hops is not relevant, since _art_ can use an association + * with metadata in place of one without, ignoring the metadata itself). + * This feature does not fundamentally conflict with the implementation, but + * neither the interface (presumably similar to `art::FindManyP`) nor the + * implementation were developed. + * + * + * Examples + * --------- + * + * ### Setting up a association crosser object + * + * Let's assume we have three data types, `DataTypeA` associated with + * `DataTypeB` and the latter associated with `DataTypeC`. + * The goal is to have the direct association from `DataTypeA` to `DataTypeC`. + * + * If it is known that the associations between `DataTypeA` and `DataTypeB` + * are all stored in data product tag `"B"` and the associations between + * `DataTypeB` and `DataTypeC` are all stored in data product tag `"C"`, + * the following initializations will work: + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} + * sbn::ns::util::AssnsCrosser const AtoC + * { event, art::InputTag{ "B" }, art::InputTag{ "C" } }; + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * or + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} + * using sbn::ns::util::hopTo; + * auto const AtoC = sbn::ns::util::makeAssnsCrosser + * (event, hopTo{ "B" }, hopTo{ "C" }); + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * or + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} + * using sbn::ns::util::startFrom, sbn::ns::util::hopTo; + * sbn::ns::util::AssnsCrosser const AtoC{ event + * , startFrom{} + * , hopTo{ "B" } + * , hopTo{ "C" } + * }; + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * or + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} + * using sbn::ns::util::startFrom, sbn::ns::util::hopTo; + * auto const AtoC = makeAssnsCrosser(event + * , startFrom{} + * , hopTo{ "B" } + * , hopTo{ "C" } + * ); + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * The latter describe more clearly the relation between the data types and + * their input tags. + * + * If there are two sets of associations between `DataTypeA` and `DataTypeB`, + * `"B:1"` and `"B:2"`, the following initializations will work: + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} + * sbn::ns::util::AssnsCrosser const AtoC + * { event, { "B:1", "B:2" }, { "C" } }; + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * or + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} + * using sbn::ns::util::hopTo; + * auto const AtoC = sbn::ns::util::makeAssnsCrosser( + * event, + * hopTo{ "B:1", "B:2" }, hopTo{ "C" } + * ); + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * If the associations are needed only for a certain subset of pointers, it is + * possible to specify them, in a way similar to `art::FindManyP`: + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} + * using sbn::ns::util::startFrom, sbn::ns::util::hopTo; + * auto const AtoC = sbn::ns::util::makeAssnsCrosser( + * , startFrom{ ptrA1, ptrA2 } + * , hopTo{ "B" } + * , hopTo{ "C" } + * ); + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * or + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} + * std::vector const selectedAptr { ptrA1, ptrA2 }; + * using sbn::ns::util::startFrom, sbn::ns::util::hopTo; + * auto const AtoC = sbn::ns::util::makeAssnsCrosser( + * , selectedAptr + * , hopTo{ "B" } + * , hopTo{ "C" } + * ); + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * Note that only a collection of type `std::vector>` is + * supported (for example, `art::PtrVector` would not be). + * + * + * ### Using a association crosser object + * + * As mentioned in the introduction, `AssnsCrosser` objects differ from + * `art::FindManyP` in that they are queried via _art_ pointers rather than + * indices. + * + * The interface for the query is `assPtr()` (see its documentation for + * examples). + * + */ +template +class sbn::ns::util::AssnsCrosser + : public details::AssnsCrosserTypes +{ + + using This_t = AssnsCrosser; + + public: + + using KeyPtr_t = typename This_t::KeyPtr_t; + using TargetPtr_t = typename This_t::TargetPtr_t; + using TargetPtrs_t = typename This_t::TargetPtrs_t; + + /** + * @brief Constructor: reads and joins the specified associations. + * @tparam Event type to read the data from (`art::Event` interface) + * @param event data source + * @param otherInputSpecs input specifications for all the hops + * + * The associations are read and joined reading the data from `event`. + * + * There needs to be one input specification for each hop, the first + * specification being the one from the key to the first intermediate object + * type. + */ + template + AssnsCrosser + (Event const& event, InputSpecs... otherInputSpecs); + + /** + * @brief Constructor: reads and joins the specified associations. + * @tparam Event type to read the data from (`art::Event` interface) + * @param event data source + * @param startSpec specifies which type to start hopping from + * @param otherInputSpecs input specifications for all the hops + * + * This constructor acts exactly like + * `AssnsCrosser(Event const&, InputSpecs...)`, but the additional + * argument allows C++ to fully determine the type of the object from the + * arguments, thus allowing the direct initialization syntax where data types + * are specified only once and close to their input specification: + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} + * using sbn::ns::util::startFrom, sbn::ns::util::hopTo; + * sbn::ns::util::AssnsCrosser const AtoC{ event + * , startFrom{} + * , hopTo{ "B" } + * ); + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + */ + template + AssnsCrosser( + Event const& event, + StartSpecs startSpec, + InputSpecs... otherInputSpecs + ); + + /** + * @brief Returns pointers to all target objects associated to `keyPtr`. + * @param keyPtr pointer to the key object to find the associated objects of + * @return a list pointers to all target objects associated to `keyPtr` + * + * This query supports a one-to-many association. + * If the `keyPtr` is unknown (either because it's not a valid object in this + * context, or because the pointed object does not have any associated target + * object) an empty collection is returned. + * + * In this example, associating `DataTypeA` objects to `DataTypeC` ones, + * all objects of type `DataTypeA` are tried one after the other: + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} + * sbn::ns::util::AssnsCrosser const AtoC + * { event, art::InputTag{ "B" }, art::InputTag{ "C" } }; + * + * auto const& Ahandle = event.getValidHandle>("B"); + * + * for (std::size_t iA = 0; iA < Ahandle->size(); ++iA) { + * + * art::Ptr const Aptr{ Ahandle, iA }; + * + * std::vector> const& Cptrs = AtoC.assPtrs(Aptr); + * + * // ... + * } // for + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * In the loop, a _art_ pointer to each `DataTypeA` object is created in order + * to query the associated `DataTypeC`. See also `assPtr()` for further usage + * patterns common to the two methods. + */ + TargetPtrs_t const& assPtrs(KeyPtr_t const& keyPtr) const + { return fAssnsMap.assPtrs(keyPtr); } + + /** + * @brief Returns a pointer to the target object associated to `keyPtr`. + * @param keyPtr pointer to the key object to find the associated objects of + * @return a pointer to the target object associated to `keyPtr` + * @throw art::Exception (code: `art::errors::LogicError`) if there are more + * than one target pointer associated to the specified key + * + * This query assumes a one-to-one association. + * If the `keyPtr` is unknown (either because it's not a valid object in this + * context, or because the pointed object does not have any associated target + * object) the pointer is returned null. + * If there are more than one elements associated with the key, + * an exception is thrown. + * + * In this example, associating `DataTypeA` objects to `DataTypeC` ones, + * all objects of type `DataTypeA` are tried one after the other: + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} + * sbn::ns::util::AssnsCrosser const AtoC + * { event, art::InputTag{ "B" }, art::InputTag{ "C" } }; + * + * auto const& Ahandle = event.getValidHandle>("B"); + * + * for (std::size_t iA = 0; iA < Ahandle->size(); ++iA) { + * + * art::Ptr const Aptr{ Ahandle, iA }; + * + * art::Ptr const Cptr = AtoC.assPtr(Aptr); + * + * // ... + * } // for + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * In the loop, a _art_ pointer to each `DataTypeA` object is created in order + * to query the associated `DataTypeC`. While this may add complications in + * the simplest case (as in the example), it allows for mixing multiple input + * collections (e.g. trying to cross an associations of tracks stored with one + * data product per cryostat to CRT hits stored in a single data product), and + * to pass a selection of objects (via _art_ pointers): + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} + * sbn::ns::util::AssnsCrosser const AtoC + * { event, art::InputTag{ "B" }, art::InputTag{ "C" } }; + * + * auto const& Ahandle = event.getValidHandle>("B"); + * + * for (art::Ptr const& Aptr: selectDataA(Ahandle)) { + * + * art::Ptr const Cptr = AtoC.assPtr(Aptr); + * + * // ... + * } // for + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * (with + * `template std::vector> selectDataA(Handle const& handle)` + * some selection function, templated to `Handle` to support both + * `art::Handle` and `art::ValidHandle`). + * + */ + TargetPtr_t const& assPtr(KeyPtr_t const& keyPtr) const; + + + private: + + using Key_t = typename This_t::Key_t; + using Target_t = typename This_t::Target_t; + + using AssnsMap_t = details::AssnsMap; + + /// Which algorithm to use for traversing the associations. + enum class HoppingAlgo { forward, backward }; + + AssnsMap_t fAssnsMap; ///< Associated objects per key. + + + static TargetPtr_t const NullTargetPtr; ///< Used as return reference value. + + + /// Returns the full content of the association map. + template + AssnsMap_t prepare( + Event const& event, + StartSpecs startSpecs, InputSpecs... otherInputSpecs + ) const; + + /// Determines which algorithm should be used for association traversal. + HoppingAlgo chooseTraversalAlgorithm( + StartSpecs const& startSpecs, + InputSpecs const&... otherInputSpecs + ) const; + + /// Returns a list of relevant pointers from the start specifications. + template + std::optional> keysFromSpecs + (Event const& event, StartSpecs const& specs) const; + +}; // sbn::ns::util::AssnsCrosser + + +// ----------------------------------------------------------------------------- +/// Wrapper to specify a single source of an association. +template +class sbn::ns::util::details::SpecBase: public SupportedVariants { + using SupportedVariants::SupportedVariants; + + public: + using SupportedSpecs_t = SupportedVariants; + + /// Returns the specification (as a variant). + // Newer C++17 revision won't need this. + SupportedSpecs_t const& spec() const { return *this; } + + protected: + + struct HasSpecTest { + + bool operator() (std::monostate) const { return false; } + bool operator() (art::ProductID id) const { return id != art::ProductID{}; } + bool operator() (art::InputTag const& tag) const { return !tag.empty(); } + + }; // HasSpecTest + +}; // sbn::ns::util::SpecBase + + +// ----------------------------------------------------------------------------- +/// Wrapper to specify a single source of an association. +class sbn::ns::util::InputSpec + : public details::SpecBase +{ + using Base_t = details::SpecBase; + using Base_t::Base_t; + + public: + + bool hasSpec() const noexcept; + +}; // sbn::ns::util::InputSpec + + +// ----------------------------------------------------------------------------- +/// Wrapper to specify a single source of an association. +template +class sbn::ns::util::StartSpec + : public details::SpecBase> +{ + using Base_t = details::SpecBase>; + using Base_t::Base_t; + + public: + using Key_t = T; + + bool hasSpec() const noexcept; + +}; // sbn::ns::util::StartSpec + + +// ----------------------------------------------------------------------------- +template +class sbn::ns::util::details::InputSpecsBase + : private std::vector // saved list of specifications +{ + using Specs_t = std::vector; + + public: + using Spec_t = SpecType; + + /// Constructor: single input specification (whatever can construct it). + template < + typename... Args, + typename = std::enable_if_t + > + > + InputSpecsBase(Args&&... specArgs) + : Specs_t{ Spec_t{ std::forward(specArgs)... } } {} + + /// Constructor: a list of input specifications. + InputSpecsBase(std::initializer_list specs) + : Specs_t + { std::move_iterator{ specs.begin() }, std::move_iterator{ specs.end() } } + {} + + /// Constructor: a list of input specifications. + InputSpecsBase(std::vector specs) + : Specs_t(std::move(specs)) {} + + /// Returns whether at least one of the specs specifies an input. + bool hasSpecs() const noexcept; + + /// Returns whether at least one of the specs specifies no input. + bool hasEmptySpecs() const noexcept; + + using Specs_t::empty; + using Specs_t::size; + using Specs_t::begin; + using Specs_t::end; + using Specs_t::cbegin; + using Specs_t::cend; + using Specs_t::at; + using Specs_t::operator[]; + +}; // sbn::ns::util::details::InputSpecsBase + + +// ----------------------------------------------------------------------------- +/** + * @brief Wrapper to specify the key type for the association hops. + * @tparam T type of the association hop these specifications refer to + * + * There are several ways to specify the content of a start list; + * all of them require the explicit specification of the type `T` + * (however the full type can be sometimes deduced in `AssnsCrosser` constructor + * call or `makeAssnsCrosser()` function). + * If the start list is empty or includes only invalid entries (like null + * pointers, empty input tags, invalid product IDs) it is assumed that all the + * pointers in the deduced input set are desired. + * + * Several input types are supported: the same ones as in `InputSpecs`, + * plus `art::Ptr`, `std::vector>` and `art::ProductPtr`. + * + * Examples in conjunction with `AssnsCrosser` and `makeAssnsCrosser()`, + * assuming `event` and `DataType`s being a data source object and data types: + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} + * using sbn::ns::util::StartSpecs; + * + * std::vector const Aptrs{ ptrA1, ptrA2 }; + * + * // all A entries found in the "B" associations + * sbn::ns::util::AssnsCrosser const AtoC_1 + * { event, StartSpec{}, "B", "C" }; + * + * // all A entries found in the "B" associations, same as above + * sbn::ns::util::AssnsCrosser const AtoC_1 + * { event, {}, "B", "C" }; + * + * // all entries found in the "A" data product + * sbn::ns::util::AssnsCrosser const AtoC_1 + * { event, "A", "B", "C" }; + * + * // only entries pointed by `ptrA1` and `ptrA2` + * sbn::ns::util::AssnsCrosser const AtoC_1 + * { event, { ptrA1, ptrA2 }, "B", "C" }; + * + * // only entries pointed by the pointers in `Aptrs` + * sbn::ns::util::AssnsCrosser const AtoC_1 + * { event, Aptrs, "B", "C" }; + * + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * Note that `startFrom` is available as an alias of `StartSpecs`, with exactly + * the same semantics and syntax. + */ +template +class sbn::ns::util::StartSpecs + : public details::InputSpecsBase> +{ + using details::InputSpecsBase>::InputSpecsBase; +}; + + +// ----------------------------------------------------------------------------- +/** + * @brief Wrapper to specify all the sources of an association. + * @tparam T type of the association hop these specifications refer to + * + * There are several ways to specify the content of the specifications; + * all of them require the explicit specification of the type `T`. + * + * Examples in conjunction with `AssnsCrosser` and `makeAssnsCrosser()`, + * assuming `event` and `DataType`s being a data source object and data types: + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} + * using sbn::ns::util::InputSpecs, sbn::ns::util::InputSpec; + * + * using AtoZ_t = sbn::ns::util::AssnsCrosser< + * DataTypeA, DataTypeB, DataTypeC, DataTypeD, DataTypeE, DataTypeF + * >; + * + * AtoZ_t const AtoZ{ event + * + * // implicit conversion to `art::InputTag`: + * , InputSpecs{ "TagB" } + * + * // implicit conversion to `art::InputTag` then to `InputSpecs`: + * , "TagC" + * + * // explicit vector of input tags (not recommended): + * , InputSpecs{ std::vector{ "TagD1", "TagD2" } } + * + * // list of input tags, converted to `InputSpecs`: + * , InputSpecs{ "TagE1", "TagE2" } + * + * // implicit list of input tags, converted to `InputSpecs`: + * , { "TagF1", "TagF2" } + * + * }; + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * Note that `hopTo` is available as an alias of `InputSpecs`, with exactly the + * same semantics and syntax. + */ +template +class sbn::ns::util::InputSpecs: public details::InputSpecsBase { + using details::InputSpecsBase::InputSpecsBase; +}; + + +// ---------------------------------------------------------------------------- +// --- Implementation +// ---------------------------------------------------------------------------- +namespace sbn::ns::util::details { + + /// The first of the template types. + template + using first_type_t = std::tuple_element_t<0, std::tuple>; + + /// The type with index `N` in the type list reversed from `Ts`. + template + struct end_type { + using type + = std::tuple_element_t>; + }; + + /// Returns the last of `Ts` types. + template + struct last_type { using type = typename end_type<0, Ts...>::type; }; + + template + struct MapJoiner; + + template + std::ostream& operator<< + (std::ostream& out, AssnsMap const& map); + + /// Appends to `dest` a copy of the content of `src`. + template + Dest& append(Dest& dest, Src const& src); + + /// Steals and appends to `dest` the content of `src`. + template + Dest& append(Dest& dest, Src&& src); + + /// Returns a constant reference to the `Index`-th of the `data` arguments. + template + auto const& getElement(Ts const&... data); + + /// Returns a vector with the elements of the sorted `minuend` which + /// are not present in the sorted `subtrahend`. + template + std::vector set_difference + (Minuend const& minuend, Subtrahend const& subtrahend); + + template + std::ostream& operator<< + (std::ostream& out, InputSpecsBase const& specs); + +} // namespace sbn::ns::util::details + + +// ----------------------------------------------------------------------------- +template +class sbn::ns::util::details::AssnsCrosserTypes { + + protected: + + static constexpr std::size_t NOtherTypes = sizeof...(OtherTypes); + static_assert(NOtherTypes >= 1, "AssnsCrosser requires at least two types."); + + public: + + using Key_t = KeyType; + using Target_t = typename last_type::type; + + using KeyPtr_t = art::Ptr; + using TargetPtr_t = art::Ptr; + using TargetPtrs_t = std::vector; + + using Assns_t = art::Assns; + + using AssnsMap_t = std::unordered_map; + +}; // sbn::ns::util::details::AssnsCrosserTypes + + +// ----------------------------------------------------------------------------- +template +class sbn::ns::util::details::AssnsMap + : public details::AssnsCrosserTypes +{ + + public: + + using This_t = AssnsMap; + + using KeyPtr_t = typename This_t::KeyPtr_t; + using TargetPtr_t = typename This_t::TargetPtr_t; + using TargetPtrs_t = typename This_t::TargetPtrs_t; + + using AssnsMap_t = typename This_t::AssnsMap_t; + + + // --- BEGIN -- Modify interface --------------------------------------------- + ///@name Modify interface + ///@{ + + /// Add a `targetPtr` associated to a `keyPtr` (duplicates not checked). + This_t& add(KeyPtr_t const& keyPtr, TargetPtr_t const& targetPtr); + + /// Add all `targetPtrs` associated to a `keyPtr` (duplicates not checked). + This_t& add(KeyPtr_t const& keyPtr, TargetPtrs_t const& targetPtrs); + + /// Add all `targetPtrs` associated to a `keyPtr` (duplicates not checked). + /// The content of `targetPtrs` is lost. + This_t& add(KeyPtr_t const& keyPtr, TargetPtrs_t&& targetPtrs); + + /// Returns the pointers associated to `keyPtr` (empty if none). + /// The key is not associated with them any more. + TargetPtrs_t yieldAssPtrs(KeyPtr_t const& keyPtr); + + /// Returns an iterable of pairs key/targets, which can be modified + /// (do **not** modify the key value!). + AssnsMap_t& assnsMap() { return fAssnsMap; } + + /// Removes all the stored associations and keys. + void clear() { fAssnsMap.clear(); } + + ///@} + // --- END ---- Modify interface --------------------------------------------- + + + // --- BEGIN -- Query interface ---------------------------------------------- + ///@name Query interface + ///@{ + + /// Returns whether there is data in the map. + bool empty() const noexcept { return fAssnsMap.empty(); } + + /// Returns the pointers associated to `keyPtr` (empty if none). + TargetPtrs_t const& assPtrs(KeyPtr_t const& keyPtr) const; + + /// Returns a map of key pointers to a sequence of associated target pointers. + AssnsMap_t const& assnsMap() const { return fAssnsMap; } + + /// Returns a sorted list of all the product IDs in the key pointers. + std::vector keyProductIDs() const; + + /// Returns a sorted list of all the product IDs in the target pointers. + std::vector targetProductIDs() const; + + ///@} + // --- END ---- Query interface ---------------------------------------------- + + + /// Returns a map from target to key describing the same associations as this. + AssnsMap flip() const; + + + private: + + static TargetPtrs_t const EmptyColl; + + AssnsMap_t fAssnsMap; ///< Key pointer -> all associated target pointers. + +}; // sbn::ns::util::details::AssnsMap + + +// ----------------------------------------------------------------------------- +/// Instructions on which pointers of type T to select. +template +struct sbn::ns::util::details::PointerSelector { + using Ptr_t = art::Ptr; + + PointerSelector(std::vector ptrs, std::vector IDs); + + bool operator() (Ptr_t const& ptr) const; + + private: + std::vector fPtrs; ///< Listed pointers pass. + std::vector fIDs; ///< All objects with these ID pass. + +}; // sbn::ns::util::details::PointerSelector + + +// ----------------------------------------------------------------------------- +// --- template implementation +// ----------------------------------------------------------------------------- +template +Dest& sbn::ns::util::details::append(Dest& dest, Src const& src) { + using std::begin, std::end; + dest.insert(end(dest), begin(src), end(src)); + return dest; +} // sbn::ns::util::details::append(Src const&) + + +template +Dest& sbn::ns::util::details::append(Dest& dest, Src&& src) { + using std::empty, std::begin, std::end; + if (empty(dest)) dest = std::move(src); + else { + dest.insert( + end(dest), std::move_iterator(begin(src)), std::move_iterator(end(src)) + ); + src.clear(); + } + return dest; +} // sbn::ns::util::details::append(Src&&) + + +// ----------------------------------------------------------------------------- +template +auto const& sbn::ns::util::details::getElement(Ts const&... data) { + + auto access = std::forward_as_tuple(data...); + + constexpr std::size_t index = (Index < 0)? sizeof...(data) + Index: Index; + return std::get(access); + +} // sbn::ns::util::details::getElement() + + +// ----------------------------------------------------------------------------- +template +std::vector +sbn::ns::util::details::set_difference + (Minuend const& minuend, Subtrahend const& subtrahend) +{ + using std::begin, std::end; + std::vector diff; + std::set_difference( + begin(minuend), end(minuend), begin(subtrahend), end(subtrahend), + back_inserter(diff) + ); + return diff; +} // sbn::ns::util::details::set_difference() + + +// ----------------------------------------------------------------------------- +// --- sbn::ns::util::details::AssnsMap +// ----------------------------------------------------------------------------- +template +typename sbn::ns::util::details::AssnsMap::TargetPtrs_t +const sbn::ns::util::details::AssnsMap::EmptyColl; + + +// ----------------------------------------------------------------------------- +template +auto sbn::ns::util::details::AssnsMap::add + (KeyPtr_t const& keyPtr, TargetPtr_t const& targetPtr) -> This_t& + { fAssnsMap[keyPtr].push_back(targetPtr); return *this; } + + +// ----------------------------------------------------------------------------- +template +auto sbn::ns::util::details::AssnsMap::add + (KeyPtr_t const& keyPtr, TargetPtrs_t const& targetPtrs) -> This_t& +{ + append(fAssnsMap[keyPtr], targetPtrs); + return *this; +} // sbn::ns::util::details::AssnsMap<>::add(TargetPtrs_t&) + + +// ----------------------------------------------------------------------------- +template +auto sbn::ns::util::details::AssnsMap::add + (KeyPtr_t const& keyPtr, TargetPtrs_t&& targetPtrs) -> This_t& +{ + append(fAssnsMap[keyPtr], std::move(targetPtrs)); + return *this; +} // sbn::ns::util::details::AssnsMap<>::add(TargetPtrs_t&&) + + +// ----------------------------------------------------------------------------- +template +auto sbn::ns::util::details::AssnsMap::assPtrs + (KeyPtr_t const& keyPtr) const -> TargetPtrs_t const& +{ + auto const it = fAssnsMap.find(keyPtr); + return (it == fAssnsMap.end())? EmptyColl: it->second; +} // sbn::ns::util::details::AssnsMap<>::assPtrs() + + +// ----------------------------------------------------------------------------- +template +auto sbn::ns::util::details::AssnsMap::keyProductIDs() + const -> std::vector +{ + std::vector IDs; + for (auto const& pairs: fAssnsMap) { + art::ProductID const ID = pairs.first.id(); + if (std::find(IDs.rbegin(), IDs.rend(), ID) == IDs.rend()) + IDs.push_back(ID); + } // for + std::sort(IDs.begin(), IDs.end()); + return IDs; +} // sbn::ns::util::details::AssnsMap::keyProductIDs() + + +// ----------------------------------------------------------------------------- +template +auto sbn::ns::util::details::AssnsMap::targetProductIDs + () const -> std::vector +{ + std::vector IDs; + for (auto const& pairs: fAssnsMap) { + for (art::Ptr const& ptr: pairs.second) { + art::ProductID const ID = ptr.id(); + if (std::find(IDs.rbegin(), IDs.rend(), ID) == IDs.rend()) + IDs.push_back(ID); + } // for pointers + } // for pairs + std::sort(IDs.begin(), IDs.end()); + return IDs; +} // sbn::ns::util::details::AssnsMap::targetProductIDs() + + +// ----------------------------------------------------------------------------- +template +auto sbn::ns::util::details::AssnsMap::yieldAssPtrs + (KeyPtr_t const& keyPtr) -> TargetPtrs_t +{ + auto it = fAssnsMap.find(keyPtr); + return (it == fAssnsMap.end()) + ? EmptyColl: std::exchange(it->second, TargetPtrs_t{}); +} // sbn::ns::util::details::AssnsMap<>::yieldAssPtrs() + + +// ----------------------------------------------------------------------------- +template +auto sbn::ns::util::details::AssnsMap::flip() const + -> AssnsMap +{ + // brute force + AssnsMap map; + for (auto const& [ key, targets ]: fAssnsMap) { + for (art::Ptr const& target: targets) { + map.add(target, key); + } // for key targets + } // for source keys + return map; +} // sbn::ns::util::details::AssnsMap<>::flip() + + +// ----------------------------------------------------------------------------- +template +std::ostream& sbn::ns::util::details::operator<< + (std::ostream& out, AssnsMap const& map) +{ + auto const& assnsMap = map.assnsMap(); + if (assnsMap.empty()) { + out << "no association"; + } + else { + out << "associations:"; + std::size_t nTargets = 0; + for (auto const& [ key, targets ]: assnsMap) { + out << "\n " << key << ": " << targets.size() << " associated targets"; + if (targets.empty()) continue; + nTargets += targets.size(); + for (auto const& [ iTarget, target ]: ::util::enumerate(targets)) + out << "\n [" << iTarget << "] " << target; + } // for keys + out << "\n" + << assnsMap.size() << " keys associated to " << nTargets << " targets"; + } // if ... else + return out; +} // sbn::ns::util::details::operator<< (AssnsMap) + + +// ----------------------------------------------------------------------------- +// --- sbn::ns::util::details::PointerSelector +// ----------------------------------------------------------------------------- +template +sbn::ns::util::details::PointerSelector::PointerSelector + (std::vector ptrs, std::vector IDs) + : fPtrs{ std::move(ptrs) }, fIDs{ std::move( IDs ) } +{ + std::sort(fPtrs.begin(), fPtrs.end()); + std::sort(fIDs.begin(), fIDs.end()); +} + + +// ----------------------------------------------------------------------------- +template +bool sbn::ns::util::details::PointerSelector::operator() + (Ptr_t const& ptr) const +{ + if (std::binary_search(fIDs.begin(), fIDs.end(), ptr.id())) return true; + return std::binary_search(fPtrs.begin(), fPtrs.end(), ptr); +} + + +// ----------------------------------------------------------------------------- +// --- sbn::ns::util::InputSpec and related +// ----------------------------------------------------------------------------- +bool sbn::ns::util::InputSpec::hasSpec() const noexcept { + + struct HasInputSpecTest: HasSpecTest {}; + + return std::visit(HasInputSpecTest{}, spec()); +} // sbn::ns::util::InputSpec::hasSpec() + + +// ----------------------------------------------------------------------------- +template +bool sbn::ns::util::StartSpec::hasSpec() const noexcept { + + struct HasStartSpecTest: Base_t::HasSpecTest { + using Base_t::HasSpecTest::operator(); + bool operator() (art::Ptr const& ptr) const + { return ptr.isNonnull(); } +#if defined CANVAS_DEC_VERSION && (CANVAS_DEC_VERSION >= 531100) + bool operator() (art::ProductPtr const& ptr) const + { return (*this)(ptr.id()); } +#endif + bool operator() (std::vector> const& ptrs) const + { + return std::any_of + (ptrs.begin(), ptrs.end(), std::mem_fn(&art::Ptr::isNonnull)); + } + }; + + return std::visit(HasStartSpecTest{}, Base_t::spec()); +} // sbn::ns::util::StartSpec<>::hasSpec() + + +// ----------------------------------------------------------------------------- +template +bool sbn::ns::util::details::InputSpecsBase::hasSpecs + () const noexcept +{ + return std::any_of(begin(), end(), std::mem_fn(&SpecType::hasSpec)); +} + + +// ----------------------------------------------------------------------------- +template +bool sbn::ns::util::details::InputSpecsBase::hasEmptySpecs + () const noexcept +{ + return !std::all_of(begin(), end(), std::mem_fn(&SpecType::hasSpec)); +} + + +// ----------------------------------------------------------------------------- +inline std::ostream& sbn::ns::util::operator<< + (std::ostream& out, InputSpec const& spec) +{ + struct InputSpecDumper { + + InputSpecDumper(std::ostream& out): fOut(out) {} + + void operator() (std::monostate) const + { fOut << "autodetect"; } + void operator() (art::InputTag const& tag) const + { fOut << "tag '" << tag.encode() << "'"; } + void operator() (art::ProductID const& ID) const + { fOut << "ProdID=" << ID; } + + private: + std::ostream& fOut; + }; // InputSpecDumper + + std::visit(InputSpecDumper{ out }, spec.spec()); + return out; + +} // sbn::ns::util::operator<< (InputSpec) + + +// ----------------------------------------------------------------------------- +template +inline std::ostream& sbn::ns::util::details::operator<< + (std::ostream& out, InputSpecsBase const& specs) +{ + std::size_t const nSpecs = specs.size(); + + if (nSpecs == 0) { + out << "no specs"; + return out; + } + + std::size_t iSpec = 0; + if (nSpecs > 1) + out << specs.size() << " specs: [0] "; + out << "{ " << specs[iSpec] << " }"; + while (++iSpec < nSpecs) { + out << "[" << iSpec << "] {" << specs[iSpec] << "}"; + } + + return out; +} // sbn::ns::util::details::operator<< (InputSpecsBase) + + +// ----------------------------------------------------------------------------- +template +std::ostream& sbn::ns::util::operator<< + (std::ostream& out, InputSpecs const& specs) +{ + out << "() << "> " + << static_cast const&>(specs); + return out; +} // sbn::ns::util::details::operator<< (InputSpecs) + + +// ----------------------------------------------------------------------------- +template +std::ostream& sbn::ns::util::operator<< + (std::ostream& out, StartSpecs const& specs) +{ + out << "() << "> " + << static_cast const&>(specs); + return out; +} // sbn::ns::util::details::operator<< (StartSpecs) + + +// ----------------------------------------------------------------------------- +// --- sbn::ns::util::details::MapJoiner +// ----------------------------------------------------------------------------- +template +struct sbn::ns::util::details::MapJoiner { + + // the first and other hop types are kept separate because it's hard + // to split the parameter pack of the others from the complete one otherwise + + using TargetType = typename last_type::type; + + struct NoSelector_t + { template bool operator() (T const&) const { return true; } }; + + static constexpr std::size_t nHops = 1 + sizeof...(OtherHopTypes); + + static constexpr NoSelector_t NoSelector{}; + + /** + * @brief Returns a association map from `KeyType` to `TargetType`. + * @tparam Event a data repository (`art::Event`-like interface) + * @param event the event to read the associations from + * @param firstHopInputSpec specification for the first hop associations + * @param otherHopInputSpec specification for all other hop associations + * @return a association map from `KeyType` to `TargetType` + * + * The algorithm starts from the last hop (associations from the + * previous-to-last of `OtherHopTypes` to the `TargetType`) and attached the + * associations hopping backward. + * + * This algorithm is slightly simpler than the forward one. + */ + template + static AssnsMap joinBackward( + Event const& event, + InputSpecs firstHopInputSpec, + InputSpecs... otherHopInputSpecs + ) { + + if constexpr(nHops == 1) { + std::vector const firstHopTags + = extractTagList(std::move(firstHopInputSpec), event); + return assnsToMap(event, firstHopTags); + } + else { + // 1 is the first hop (KeyType -> FirstHopType), + // 2 is all the others (FirstHopType -> TargetType) + auto assnsMap2 = MapJoiner::joinBackward + (event, std::move(otherHopInputSpecs)...); + return leftExtendMapWithAssns + (std::move(assnsMap2), event, std::move(firstHopInputSpec)); + } // if more than one hop + } // joinBackward() + + + /** + * @brief Returns a association map from `KeyType` to `TargetType`. + * @tparam Event a data repository (`art::Event`-like interface) + * @tparam Selector functor with `bool operator() const (art::Ptr)` + * @param event the event to read the associations from + * @param firstHopInputSpec specification for the first hop associations + * @param otherHopInputSpec specification for all other hop associations + * @param selector if specified, only keys passing the selector are considered + * @return a association map from `KeyType` to `TargetType` + * + * The algorithm starts from the first hop (associations from the + * `KeyType` to the `FirstHopType`) and attached the associations hopping + * forward. + */ + template + static AssnsMap joinForward( + Event const& event, + InputSpecs firstHopInputSpec, + InputSpecs... otherHopInputSpecs, + std::optional const& selector = NoSelector + ) { + std::vector const firstHopTags + = extractTagList(std::move(firstHopInputSpec), event); + + auto leftMap + = assnsToMap(event, firstHopTags, selector); + + if constexpr(nHops == 1) { + return leftMap; + } + else { + return multiRightExtendMapWithAssns + (std::move(leftMap), event, std::move(otherHopInputSpecs)...); + } + } // joinForward() + + + /// Returns an association map from `tag` associations read from `event`. + /// Only left entries passing `selector` are included. + template < + typename Left, typename Right, + typename Event, typename InputTags, typename Selector = NoSelector_t + > + static AssnsMap assnsToMap( + Event const& event, InputTags const& tags, + std::optional const& selector = std::nullopt + ) { + AssnsMap assnsMap; + for (art::InputTag const& tag: tags) + addAssnsToMap(assnsMap, event, tag, selector); + return assnsMap; + } // assnsToMap() + + /// Extends the association `map` with `tag` associations read from `event`, + /// adding only the ones with left pointer listed in `selector`. + template < + typename Left, typename Right, + typename Event, typename Selector = NoSelector_t + > + static AssnsMap& addAssnsToMap( + AssnsMap& map, Event const& event, art::InputTag const& tag, + std::optional const& selector = std::nullopt + ) { + return addAssnsToMap( + map, event.template getProduct>(tag), selector + ); + } + + /// Extends the association `map` with the specified _art_ associations, + /// adding only the ones with left pointer passing `selector`. + template + static AssnsMap& addAssnsToMap( + AssnsMap& map, art::Assns const& assns, + std::optional const& selector = std::nullopt + ) { + for (auto const& [ leftPtr, rightPtr ]: assns) { + if (!selector || (*selector)(leftPtr)) map.add(leftPtr, rightPtr); + } + return map; + } // addAssnsToMap() + + /** + * @brief Returns a new association map extended on the key side + * @tparam NewLeft (mandatory) type of key for the new map + * @tparam Left type of key of the existing map + * @tparam Right type of target of the existing map + * @tparam Event type of the data repository + * @tparam InputTags type of a collection of `art::InputTag` + * @param map the map to be "extended"; it will be depleted of its content + * @param event the data repository to read the associations from + * @param specs the specification for the input of the extending association + * @return a new association map + * + * The returned association map has `NewLeft` as the new key type and the same + * target type (`Right`) as the input `map`. + * The resulting map is joining the key of the input `map` with the target of + * the associations being read. + * The map _may_ come out smaller than the two inputs. + */ + template < + typename NewLeft, typename Left, typename Right, typename Event, typename T + > + static AssnsMap leftExtendMapWithAssns( + AssnsMap&& map, Event const& event, InputSpecs specs) { + // read the associations with the material for the extension + bool const bAutodetect = specs.hasEmptySpecs(); + std::vector const tags + = extractTagList(std::move(specs), event); + std::vector neededIDs; + if (bAutodetect) neededIDs = map.keyProductIDs(); + auto const leftMap = mapExtensionPreparation + (event, tags, std::move(neededIDs)); + return joinMaps(leftMap, std::move(map)); + } // leftExtendMapWithAssns() + + + template < + typename Left, typename Right, typename NextRight, typename... MoreRights, + typename Event + > + static AssnsMap::type> + multiRightExtendMapWithAssns( + AssnsMap&& map, + Event const& event, + InputSpecs nextInputSpec, + InputSpecs... otherInputSpec + ) + { + AssnsMap assnsMap = rightExtendMapWithAssns + (std::move(map), event, std::move(nextInputSpec)); + + if constexpr(sizeof...(MoreRights) == 0) { + return assnsMap; + } + else { + return multiRightExtendMapWithAssns + (std::move(assnsMap), event, std::move(otherInputSpec)...); + } + } // multiRightExtendMapWithAssns() + + + /** + * @brief Returns a new association map extended on the target side + * @tparam NewRight (mandatory) type of key for the new map + * @tparam Left type of key of the existing map + * @tparam Right type of target of the existing map + * @tparam Event type of the data repository + * @param map the map to be "extended"; it will be depleted of its content + * @param event the data repository to read the associations from + * @param specs the specification for the input of the extending association + * @return a new association map + * + * The returned association map has `NewRight` as the new target type and the + * same key type (`Left`) as the input `map`. + * The resulting map is joining the key of the association being read with the + * key of the input `map`. + * The map _may_ come out smaller than the two inputs. + */ + template< + typename NewRight, typename Left, typename Right, typename Event, typename T + > + static AssnsMap rightExtendMapWithAssns + (AssnsMap map, Event const& event, InputSpecs specs) + { + // read the associations with the material for the extension + bool const bAutodetect = specs.hasEmptySpecs(); + std::vector const tags + = extractTagList(std::move(specs), event); + std::vector neededIDs; + if (bAutodetect) neededIDs = map.targetProductIDs(); + auto rightMap = mapExtensionPreparation + (event, tags, std::move(neededIDs)); + return joinMaps(map, std::move(rightMap)); + } + + /// Joins two maps in the middle, stealing content from the right one. + template + static AssnsMap joinMaps + (AssnsMap const& leftMap, AssnsMap&& rightMap) + { + AssnsMap map; + for (auto& [ leftPtr, middlePtrs ]: leftMap.assnsMap()) { + for (art::Ptr const& middlePtr: middlePtrs) { + map.add(leftPtr, rightMap.yieldAssPtrs(middlePtr)); + } // for middle pointers + } // for left map + return map; + } // joinMaps() + + + /** + * @brief Collects a map of Left-to-Right pointers. + * @tparam Left type of key in the map + * @tparam Right type of target in the map + * @tparam JointSide `0` for `Left` side, `1` for `Right` side + * @tparam Event type of data repository to read data from (`art::Event` I/F) + * @tparam InputTags type of collection of `art::InputTag` objects + * @param event the event to read the data from + * @param tags the list of input tags to needed `Left`-to`Right` associations + * @param requiredIDs list of product IDs needed for the the extension + * @return a `Left`-to-`Right` association map + * @throw art::Exception (code: `art::errors::ProductNotFound`) if a required + * association is not found + * + * The returned map contains all the `Left`-to-`Right` associations specified + * by `tags`; if any is missing, an exception is thrown. + * + * After these associations are collected, the product ID of the pointers + * in the `JointSide` side are compared with the ones specified in the + * `requiredIDs` list. For each ID which is in `requiredIDs` but does not + * appear in the associations collected so far, the algorithm attempts + * to read another `Left`-to-`Right` association using the exact same input + * tag as the one associated to that product ID. If such association data + * product is found, its content is added to the map. Otherwise, the algorithm + * moves on, not considering this a fatal error. + * + * The only clear fatal error condition tested by this algorithm is when no + * mandatory tag is specified, some IDs are present in `requiredIDs` _and_ + * no data product has been found from any of them. In that case, an exception + * is thrown (still `art::errors::ProductNotFound` code). + */ + template < + typename Left, typename Right, std::size_t JointSide, + typename Event, typename InputTags + > + static AssnsMap mapExtensionPreparation( + Event const& event, InputTags const& tags, + std::vector const& requiredIDs + ) { + /* + * First read all the associations with tags that are explicitly tagged; + * then compare their ID with the IDs that we are required. + * For each required ID not present in the original tags, + * an association is read (failure is not an error). + */ + using Assns_t = AssnsMap; + Assns_t map = assnsToMap(event, tags); + + constexpr auto extractProductIDs = (JointSide == 0) + ? &Assns_t::keyProductIDs: &Assns_t::targetProductIDs; + std::vector const mapIDs = (map.*extractProductIDs)(); + + std::vector const missingIDs + = details::set_difference(requiredIDs, mapIDs); + + unsigned int nDiscovered = 0; + for (art::ProductID const ID: missingIDs) { + art::InputTag const tag = getInputTag(event, ID); + auto handle = event.template getHandle>(tag); + if (!handle) continue; + addAssnsToMap(map, *handle); + ++nDiscovered; + } // for + + // error check for an extreme case: + using std::empty; + if (empty(tags) && !missingIDs.empty() && (nDiscovered == 0)) { + std::string const leftName = lar::debug::demangle(); + std::string const rightName = lar::debug::demangle(); + // even if this error is not triggered we may still be missing some + throw art::Exception{ art::errors::ProductNotFound } + << "During preparation of " << leftName << " <=> " << rightName + << " associations to join on " + << ((JointSide == 0)? leftName: rightName) + << " couldn't find any of the needed association data products!" + << " Some must be explicitly specified via input tag." + << "\n"; + } + + return map; + } // mapExtensionPreparation() + + /// Returns the input tag associated to the product `ID` (empty if not found). + template + static art::InputTag getInputTag(Event const& event, art::ProductID ID) + { + art::BranchDescription const* branchDescr + = event.getProductDescription(ID).get(); + if (!branchDescr) return {}; + return { branchDescr->inputTag() }; + } // getInputTag() + + /// Helper returning a list of input tags out of a InputSpec + template + struct TagListExtractor { + + TagListExtractor(Event const& event): fEvent{ &event } {} + + std::vector operator() + (std::monostate) const + { return {}; } + + std::vector operator() + (art::InputTag&& inputTag) const + { return { std::move(inputTag) }; } + + std::vector operator() + (art::ProductID ID) const + { return { getInputTag(*fEvent, ID) }; } + + private: + Event const* fEvent; + + }; // TagListExtractor + + template + static std::vector extractTagList + (InputSpecs&& inputSpecs, Event const& event) + { + std::vector tags; + for (InputSpec& spec: inputSpecs) { + append(tags, + visit( + TagListExtractor{ event }, + static_cast(spec) + )); + } // for + return tags; + } + +}; // sbn::ns::util::details::MapJoiner + + +// ----------------------------------------------------------------------------- +// --- sbn::ns::util::AssnsCrosser +// ----------------------------------------------------------------------------- +template +typename sbn::ns::util::AssnsCrosser::TargetPtr_t +const sbn::ns::util::AssnsCrosser::NullTargetPtr; + + +// ----------------------------------------------------------------------------- +template +template +sbn::ns::util::AssnsCrosser::AssnsCrosser( + Event const& event, + InputSpecs... otherInputSpecs +) + : AssnsCrosser{ event, StartSpecs{}, std::move(otherInputSpecs)... } +{} + + +// ----------------------------------------------------------------------------- +template +template +sbn::ns::util::AssnsCrosser::AssnsCrosser( + Event const& event, + StartSpecs startSpecs, + InputSpecs... otherInputSpecs +) + : fAssnsMap + { prepare(event, std::move(startSpecs), std::move(otherInputSpecs)... ) } +{} + + +// ----------------------------------------------------------------------------- +template +auto sbn::ns::util::AssnsCrosser::assPtr + (KeyPtr_t const& keyPtr) const -> TargetPtr_t const& +{ + TargetPtrs_t const& targets = assPtrs(keyPtr); + if (targets.size() > 1) { + // using LogicError because that's what art::FindOne does + throw art::Exception{ art::errors::LogicError } + << "AssnsCrosser::assPtr(): there are " << targets.size() << " " + << lar::debug::demangle() << " objects associated to Ptr<" + << lar::debug::demangle() << ">=" << keyPtr << "!\n"; + } + return targets.empty()? NullTargetPtr: targets.front(); +} // sbn::ns::util::AssnsCrosser::assPtr() + + +// ----------------------------------------------------------------------------- +template +template +auto sbn::ns::util::AssnsCrosser::prepare( + Event const& event, + StartSpecs startSpecs, InputSpecs... otherInputSpecs +) const -> AssnsMap_t +{ + std::optional> keySelector + = keysFromSpecs(event, startSpecs); + HoppingAlgo const algo + = chooseTraversalAlgorithm(startSpecs, otherInputSpecs...); + switch (algo) { + case HoppingAlgo::forward: + return details::MapJoiner::joinForward + (event, std::move(otherInputSpecs)..., keySelector); + case HoppingAlgo::backward: + return details::MapJoiner::joinBackward + (event, std::move(otherInputSpecs)... ); + default: + throw std::logic_error + { "Unexpected direction: " + std::to_string(static_cast(algo)) }; + } // switch +} // sbn::ns::util::AssnsCrosser<>::prepare() + + +// ----------------------------------------------------------------------------- +template +auto sbn::ns::util::AssnsCrosser + ::chooseTraversalAlgorithm +( + StartSpecs const& startSpecs, + InputSpecs const&... otherInputSpecs +) const -> HoppingAlgo { + /* + * If there is a start specification, we need (and can) to go forward. + * Otherwise, we go backward (faster) unless there is no specification for + * the last hop (in which case we can't start from the back). + * + * When both algorithms are available, the backward one is chosen. + */ + + bool const hasStartInfo = startSpecs.hasSpecs(); + + bool const hasEndSpecs + = details::getElement<-1>(otherInputSpecs...).hasSpecs(); + + constexpr std::size_t nHops = sizeof...(OtherTypes); + +#if 0 + // --- BEGIN -- DEBUG -------------------------------------------------------- + // print details about how the specifications are received: + auto const formatter + = [&out=std::cout](auto const& specs){ out << "\n " << specs; }; + std::cout + << "Start specs: " << startSpecs << ", hasStartInfo=" << hasStartInfo + << "\n" << nHops << " hops:"; + (formatter(otherInputSpecs), ...); + std::cout << "\nLast spec: " << details::getElement<-1>(otherInputSpecs...) + << " -> hasEndSpecs=" << hasEndSpecs << std::endl; + + // --- END ---- DEBUG -------------------------------------------------------- +#endif // 0 + if constexpr(nHops == 1) { + if (hasStartInfo) return HoppingAlgo::forward; + if (hasEndSpecs) return HoppingAlgo::backward; + throw std::logic_error + { "Insufficient specifications for single association traversal." }; + } + else { + bool const hasFirstSpecs + = hasStartInfo || details::getElement<0>(otherInputSpecs...).hasSpecs(); + + if (hasFirstSpecs) return HoppingAlgo::forward; + if (hasEndSpecs) return HoppingAlgo::backward; + + throw std::logic_error{ + "Insufficient specifications for traversal of " + std::to_string(nHops) + + " associations." + }; + + } + +} // sbn::ns::util::AssnsCrosser<>::chooseTraversalAlgorithm() + + +// ----------------------------------------------------------------------------- +template +template +auto sbn::ns::util::AssnsCrosser::keysFromSpecs + (Event const& event, StartSpecs const& specs) const + -> std::optional> +{ + /* + * An interface needs to be established. + * The specification may follow the pattern of the InputSpecs, but can't + * use InputSpecsBase as it is now, since the hosted data types may need a + * templated type (see below). + * A list of possible supported input: + * * a product ID: get the handle to the `std::vector` data product and + * the product size (which unfortunately means to read the data product + * itself) and make a list of all pointers + * * a handle or valid handle: + * * an input tag: get the handle of the `std::vector` data product and + * proceed with that as above + * * a product pointer: get the product ID and proceed with that + * * a pointer to the key: require that pointer directly + * * a vector of pointers: require all the pointers in the vector + * + * One can avoid reading the size of the data product, and possibly the data + * product itself, by having a special value that denotes all possible + * pointers from a data product, i.e. from a product ID. + * This special value can be treated either as a variant (e.g. including + * a `art::ProductPtr` and a `art::Ptr`) or assigning a special key to + * an `art::Ptr`, or keeping a separate list of the two types of + * specifications (which is probably the most efficient way). + */ + if (!specs.hasSpecs()) return std::nullopt; + + std::vector> ptrs; + std::vector IDs; + + for (StartSpec const& spec: specs) { + + if (std::holds_alternative(spec)) { + auto const& handle = event.template getValidHandle> + (std::get(spec)); + IDs.push_back(handle.id()); + } + else if (std::holds_alternative>(spec)) { + ptrs.push_back(std::get>(spec)); + } + else if (std::holds_alternative>>(spec)) { + details::append(ptrs, std::get>>(spec)); + } + else if (std::holds_alternative(spec)) { + IDs.push_back(std::get(spec)); + } +#if defined CANVAS_DEC_VERSION && (CANVAS_DEC_VERSION >= 531100) + else if (std::holds_alternative>(spec)) { + IDs.push_back(std::get>(spec).id()); + } +#endif + else if (std::holds_alternative(spec)) { + // ignored, since there are other specs (or `hasSpecs()` would be `false`) + } + else throw art::Exception{ art::errors::LogicError } + << "Start spec holds an unexpected type (" << spec.index() << ").\n"; + + } // for specs + + return std::optional> + { std::in_place, std::move(ptrs), std::move(IDs) }; +} // sbn::ns::util::AssnsCrosser<>::keysFromSpecs() + + +// ----------------------------------------------------------------------------- +template +auto sbn::ns::util::makeAssnsCrosser( + Event const& event, + InputSpecs... inputSpecs +) -> AssnsCrosser +{ + return AssnsCrosser(event, std::move(inputSpecs)...); +} + + +// ----------------------------------------------------------------------------- +template +auto sbn::ns::util::makeAssnsCrosser( + Event const& event, + StartSpecs, + InputSpecs... inputSpecs +) -> AssnsCrosser +{ + return AssnsCrosser(event, std::move(inputSpecs)...); +} + + +// ----------------------------------------------------------------------------- + +#endif // SBNALG_UTILITIES_ASSNSCROSSER_H diff --git a/sbnalg/Utilities/CMakeLists.txt b/sbnalg/Utilities/CMakeLists.txt new file mode 100644 index 0000000..43649c7 --- /dev/null +++ b/sbnalg/Utilities/CMakeLists.txt @@ -0,0 +1,11 @@ +cet_make_library(INTERFACE + SOURCE + "AssnsCrosser.h" + LIBRARIES INTERFACE + larcorealg::CoreUtils + canvas::canvas + ) + +install_headers() +install_source() +install_fhicl() diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 06e962f..504222a 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,7 +1,17 @@ # cmake driver file for testing from CET build tools include(CetTest) +cet_make_library( + LIBRARY_NAME Test + INTERFACE + SOURCE + FrameworkEventMockup.h + LIBRARIES INTERFACE + canvas::canvas +) + # Always enable asserts (for tests only) cet_enable_asserts() add_subdirectory(Geometry) +add_subdirectory(Utilities) diff --git a/test/FrameworkEventMockup.h b/test/FrameworkEventMockup.h new file mode 100644 index 0000000..9ab9357 --- /dev/null +++ b/test/FrameworkEventMockup.h @@ -0,0 +1,580 @@ +/** + * @file test/FrameworkEventMockup.h + * @brief Simple _art_-like event mockup. + * @author Gianluca Petrillo (petrillo@slac.stanford.edu) + * @date June 9, 2023 + * + * This library is header only (although there are a couple of inlined things + * that would probably warrant an implementation file). + */ + +#ifndef ICARUSALG_TEST_FRAMEWORKEVENTMOCKUP_H +#define ICARUSALG_TEST_FRAMEWORKEVENTMOCKUP_H + +// framework libraries +#include "canvas/Persistency/Common/Assns.h" +#include "canvas/Persistency/Common/Ptr.h" +#include "canvas/Persistency/Provenance/ProductID.h" +#include "canvas/Persistency/Provenance/BranchDescription.h" +#include "canvas/Persistency/Provenance/ProcessConfiguration.h" +#include "canvas/Persistency/Provenance/TypeLabel.h" +#include "canvas/Utilities/TypeID.h" +#include "canvas/Utilities/InputTag.h" +#include "canvas/Utilities/Exception.h" +#include "cetlib/exempt_ptr.h" +#include "fhiclcpp/ParameterSetID.h" + +// C/C++ standard libraries +#include +#include +#include +#include +#include +#include // std::move() +#include + + +//------------------------------------------------------------------------------ +namespace testing::mockup { + class Event; + template class Handle; + template class ValidHandle; + template class PtrMaker; + namespace details { template class HandleBase; } +} // namespace testing::mockup + +/** + * @brief Mock-up class with a ridiculously small `art::Event`-like interface. + * + * This "event" contains and owns data objects and can return a constant + * reference to them on demand. It is intended to develop unit tests for + * code that requires to read data from an event. + * + * The interface is mimicking _art_'s and _gallery_'s `Event` classes, but it's + * reduced to the very bare minimum. + * + * Supported operations: + * * adding a data product associating it with an input tag (`art::InputTag`); + * the interface of this `put()` is inspired by _art_'s, but does not match + * it (nor it is intended to). In particular, this class does not currently + * use `std::unique_ptr` to store data products. + * * requesting a data product via `art::InputTag`: `getProduct()` mirrors the + * actual `art::Event` interface (it should be also in _gallery_, but as of + * `v1_20_02` that interface has not been added). + * * requesting the product ID (`art::ProductID`) of a data product specified + * by `art::InputTag` with `getProductID()`; this is _very different_ from + * `art::Event::getProductID()`, which returns ID only for data products + * from the current (producer?) module. + * + * Pretty much everything else is _not_ supported, including also: + * * handles + * * product tokens + * * views + * * selectors + * * reading many data products at once + * + * Example: + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} + * testing::mockup::Event fillEvent() { + * testing::mockup::Event event; + * event.put(std::vector{ 0.3, 0.6, 0.9 }, art::InputTag{ "A" }); + * event.put(std::vector{ 1, 6, 5, 9 }, art::InputTag{ "B" }); + * return event; + * } + * + * testing::mockup::Event const event = fillEvent(); + * + * auto const& dataB = event.getProduct>(art::InputTag{ "B" }); + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + */ +class testing::mockup::Event { + + public: + + static std::string const DefaultProcessName; ///< Default process name. + + /** + * @brief Constructor. + * @param processName sets the (default) process name for products without one + */ + Event(std::string processName = DefaultProcessName) + : fProcessName{ std::move(processName) } + {} + + + Event(Event&&) = default; + Event& operator= (Event&&) = default; + + + // --- BEGIN --- Data population interface ----------------------------------- + /// @name Data population interface + /// @{ + + /** + * @brief Moves and registers the specified data under the specified `tag`. + * @param T type of the data product being put into the event + * @param tag the input tag this data product will be registered under + * @param data the content of the data product + * @return the ID of the data product just added + * @throw art::Exception (code: `aer::errors::ProductRegistrationFailure`) + * if a data product with this type and tag is already registered + * + * The `data` is moved into the event and it will be owned by it from now on. + * A product ID is assigned to the data product and returned. + * + * Note that this interface is subtly different from `art::Event`: here we + * must supply a full input `tag`, while _art_ supports just an optional + * instance name; and _art_ (from v. 3.11) returns a handle which is + * convertible to `art::ProductID` instead of the product ID itself. + */ + template + art::ProductID put(T&& data, art::InputTag tag); + + /// @} + // --- END ----- Data population interface ----------------------------------- + + + // --- BEGIN --- Query and retrieval interface ------------------------------- + /// @name Query and retrieval interface + /// @{ + + /// Returns the ID of data product of type `T` and specified input `tag`. + template + art::ProductID getProductID(art::InputTag const& tag) const; + + /// Returns the data product of type `T` and specified input `tag`. + /// @throw art::Exception (code: `art::errors::ProductNotFound`) if not found + template + T const& getProduct(art::InputTag const& tag) const; + + /// Returns a handle to the data product of type `T` and specified `tag`. + template + Handle getHandle(art::InputTag const& tag) const; + + /// Returns a handle to the data product of type `T` and specified `tag`. + /// @throws art::Exception (code: `art::errors::ProductNotFound`) if not found + template + Handle getValidHandle(art::InputTag const& tag) const; + + /** + * @brief Returns the branch description for the specified product ID. + * @param ID the product ID to query about + * @return a branch description object, partially filled + * + * Most of the information in the _art_ branch description either does not + * apply or it is hard to discover in this mockup. + * Currently the only information reliably stored is the input tag. + */ + cet::exempt_ptr getProductDescription + (art::ProductID ID) const; + + + /// @} + // --- END ----- Query and retrieval interface ------------------------------- + + + private: + + struct ProductKey: std::pair { + + using std::pair::pair; + + static int comp(art::InputTag const& a, art::InputTag const& b) noexcept; + + }; // ProductKey + + friend bool operator< (ProductKey const& a, ProductKey const& b) noexcept; + + struct DataProductRecord_t { + art::InputTag tag; + art::ProductID id; + std::any data; + }; + + struct BranchRecord_t { + art::BranchDescription branchDescr; + }; + + + // don't copy (not deleted because we may want in the future a helper to copy) + Event(Event const&) = default; + Event& operator= (Event const&) = default; + + + // --- BEGIN --- Configuration ----------------------------------------------- + + std::string fProcessName; + + // --- END ----- Configuration ----------------------------------------------- + + + // --- BEGIN --- Object data ------------------------------------------------- + + art::ProductID::value_type fLastProductID = art::ProductID{}.value(); // =invalid + + std::map fDataPointers; ///< The data. + + /// Some "branch" information. + std::map fProductIDs; + + // --- END ----- Object data ------------------------------------------------- + + + /// Returns the pointer to the product information for `tag`. + /// @returns pointer to the information record, `nullptr` if not available + template + DataProductRecord_t const* getProductInfo(art::InputTag const& tag) const; + + /// Returns the pointer to the data in the record. Throws if wrong type. + template + T const* getDataPointer(DataProductRecord_t const& dataRecord) const; + + /// Returns the pointer to the product information for `tag`. + /// @throws art::Exception (`art::errors::ProductNotFound`) if not available + template + DataProductRecord_t const& getValidProductInfo + (art::InputTag const& tag) const; + + /// Adds the default process name to the `tag` if it does not have any. + art::InputTag completeTag(art::InputTag tag) const; + + template + static ProductKey makeKey(art::InputTag tag); + +}; // testing::mockup::Event + + +/// Base class for mockup data product handles. +template +class testing::mockup::details::HandleBase { + art::ProductID fID; ///< ID of this product. + T const* fData = nullptr; ///< Pointer to the actual data. + + protected: + void checkValidity() const; + + public: + using element_type = T; + class HandleTag {}; ///< Utility tag to recognise a handle. + + HandleBase() = default; + HandleBase(art::ProductID ID, T const* data): fID{ ID }, fData{ data } {} + + T const& operator*() const { return *product(); } + T const* operator->() const { return product(); } + T const* product() const { return fData; } + + art::ProductID id() const { return fID; } + + explicit operator bool() const noexcept { return isValid(); } + + /// Returns whether the handle has actual data and from a valid source. + bool isValid() const noexcept { return fData && (fID != art::ProductID{}); } + + /// Returns whether the handle has actual data. + bool failedToGet() const { return fData == nullptr; } + +}; // testing::mockup::details::HandleBase + + +/// Mockup class of data product handle. Acts like a "smart" pointer. +template +class testing::mockup::Handle: public details::HandleBase { + using Base_t = details::HandleBase; + + public: + using Base_t::Base_t; + + T const* product() const + { Base_t::checkValidity(); return Base_t::product(); } + +}; // testing::mockup::Handle + + +/// Mockup class of data product valid handle. Acts like a "smart" pointer. +template +class testing::mockup::ValidHandle: public details::HandleBase { + using Base_t = details::HandleBase; + + public: + ValidHandle(art::ProductID ID, T const* data): Base_t{ ID, data } {} + +}; // testing::mockup::ValidHandle + + +// ----------------------------------------------------------------------------- +/** + * @brief Creates `art::Ptr` from the specified data product. + * @tparam T data type of the pointers + * + * This class is initialised with a data product (either product ID and data, + * or event and input tag) of type `std::vector` and can return functional + * `art::Ptr` to the elements of that data product. + * + * Example: + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} + * testing::mockup::Event event; + * event.put(std::vector{ 0.3, 0.6, 0.9 }, art::InputTag{ "A" }); + * event.put(std::vector{ 1, 6, 5, 9 }, art::InputTag{ "B" }); + * + * auto const& dataB = event.getProduct>(art::InputTag{ "B" }); + * + * testing::mockup::PtrMaker const makeAptr{ event, art::InputTag{ "A" } }; + * testing::mockup::PtrMaker const makeBptr{ event, art::InputTag{ "B" } }; + * + * art::Assns assnsAB; + * assnsAB.addSingle(makeAptr(1), makeBptr(1)); + * assnsAB.addSingle(makeAptr(2), makeBptr(3)); + * event.put(std::move(assnsAB, art::InputTag{ "B" }); + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + */ +template +class testing::mockup::PtrMaker { + + public: + using Data_t = T; + using ProdColl_t = std::vector; + using Ptr_t = art::Ptr; + + /** + * @brief Constructor: pointers to a product of specified `tag` from `event`. + * @param event the "event" to read the data product from + * @param tag the input tag of the data product + * + * The data product with the specified `tag` is read from `event`, and an + * `art::Ptr` is created out of it. + * The pointer can actually dereference to the data. + * + * This interface is not compatible with `art::PtrMaker`. + * + * The data product must exist in `event`. + */ + PtrMaker(Event const& event, art::InputTag const& tag) + : PtrMaker + { event.getProductID(tag), event.getProduct(tag) } + {} + + /** + * @brief Constructor: pointers with product `prodID` and pointing to `data`. + * @param prodID product ID to be assigned to the pointers + * @param data data the pointers will be pointing to + * + * Pointers will have the specified product ID and will point to elements of + * the `data` collection. + * The pointer can actually dereference to the data. + * Pointers may becoming dangling if the underlying content of `data` is + * deleted or moved away (which is normally not possible in `art::Event`). + * They may also point to non-existing elements after the end of the data + * collection, condition which is not checked. + * + * This interface is not compatible with `art::PtrMaker`. + */ + PtrMaker(art::ProductID prodID, ProdColl_t const& data) + : fProdID{ prodID }, fData{ data } {} + + // @{ + /** + * @brief Creates a pointer to the specified element of the data product. + * @param index the index of the element in the data product + * @return a pointer + * + * No check is performed on the index, which may point beyond the end of the + * data product. + */ + Ptr_t make(std::size_t index) const + { return Ptr_t{ fProdID, &(fData[index]), index }; } + Ptr_t operator() (std::size_t index) const { return make(index); } + // @} + + private: + + art::ProductID const fProdID; ///< Product ID to record in the pointers. + ProdColl_t const& fData; ///< Pointer to the original data product. + +}; // testing::mockup::PtrMaker + + +// ----------------------------------------------------------------------------- +// --- template implementation +// ----------------------------------------------------------------------------- +// --- testing::mockup::Event +// ----------------------------------------------------------------------------- +inline std::string const testing::mockup::Event::DefaultProcessName{ "mockup" }; + + +// ----------------------------------------------------------------------------- +int testing::mockup::Event::ProductKey::comp + (art::InputTag const& a, art::InputTag const& b) noexcept +{ + if (int cmp = a.process().compare(b.process())) return cmp; + if (int cmp = a.label().compare(b.label())) return cmp; + return a.instance().compare(b.instance()); +} + + +// ----------------------------------------------------------------------------- +namespace testing::mockup { + + bool operator< + (Event::ProductKey const& a, Event::ProductKey const& b) noexcept + { + if (int cmp = Event::ProductKey::comp(a.first, b.first)) return cmp < 0; + return a.second < b.second; + } // operator< (Event::ProductKey, Event::ProductKey) + +} // namespace testing::mockup + + +// ----------------------------------------------------------------------------- +template +art::ProductID testing::mockup::Event::put(T&& data, art::InputTag tag) { + + tag = completeTag(tag); + auto key = makeKey(tag); + if (fDataPointers.find(key) != fDataPointers.end()) { + throw art::Exception{ art::errors::ProductRegistrationFailure } + << "Data product '" << tag.encode() << "' already registered.\n"; + } + + art::ProductID ID{ ++fLastProductID }; + fhicl::ParameterSetID const PSetID{}; // no parameter set ID + + fProductIDs.emplace( + ID, + BranchRecord_t{ + art::BranchDescription{ + art::InEvent // branch type + , art::TypeLabel{ art::TypeID{ typeid(T) }, tag.instance(), true } + , tag.label() + , PSetID + , art::ProcessConfiguration{ tag.process(), PSetID, "" } + } + } + ); + + fDataPointers.emplace( + std::move(key), + DataProductRecord_t{ + std::move(tag), + ID, + std::move(data) + } + ); + + return ID; +} // testing::mockup::Event::put() + + +// ----------------------------------------------------------------------------- +template +art::ProductID testing::mockup::Event::getProductID + (art::InputTag const& tag) const + { return getValidProductInfo(tag).id; } + + +// ----------------------------------------------------------------------------- +template +T const& testing::mockup::Event::getProduct(art::InputTag const& tag) const { + return *getDataPointer(getValidProductInfo(tag)); +} // testing::mockup::Event::getProduct() + + +// ----------------------------------------------------------------------------- +template +auto testing::mockup::Event::getHandle(art::InputTag const& tag) const + -> Handle +{ + if (DataProductRecord_t const* dataRecord = getProductInfo(tag)) + return { dataRecord->id, getDataPointer(*dataRecord) }; + return {}; +} // testing::mockup::Event::getHandle() + + +// ----------------------------------------------------------------------------- +template +auto testing::mockup::Event::getValidHandle(art::InputTag const& tag) const + -> Handle +{ + DataProductRecord_t const& dataRecord = getValidProductInfo(tag); + return { dataRecord.id, getDataPointer(dataRecord) }; +} // testing::mockup::Event::getValidHandle() + + +// ----------------------------------------------------------------------------- +template +auto testing::mockup::Event::getProductInfo(art::InputTag const& tag) const + -> DataProductRecord_t const* +{ + auto const it = fDataPointers.find(makeKey(completeTag(tag))); + return (it == fDataPointers.end())? nullptr: &(it->second); +} // testing::mockup::Event::getProductInfo() + + +// ----------------------------------------------------------------------------- +template +auto testing::mockup::Event::getValidProductInfo(art::InputTag const& tag) const + -> DataProductRecord_t const& +{ + DataProductRecord_t const* dataRecord = getProductInfo(tag); + if (dataRecord) return *dataRecord; + throw art::Exception{ art::errors::ProductNotFound } + << "Data product '" << tag.encode() << "' not registered or wrong type.\n"; +} // testing::mockup::Event::getValidProductInfo() + + +// ----------------------------------------------------------------------------- +template +T const* testing::mockup::Event::getDataPointer + (DataProductRecord_t const& dataRecord) const +{ + try { return &std::any_cast(dataRecord.data); } + catch (std::bad_any_cast const&) { + throw art::Exception{ art::errors::LogicError } + << "Data product '" << dataRecord.tag.encode() + << "' not of requested type.\n"; + } +} // testing::mockup::Event::getDataPointer() + + +// ----------------------------------------------------------------------------- +cet::exempt_ptr +inline testing::mockup::Event::getProductDescription + (art::ProductID ID) const +{ + auto const it = fProductIDs.find(ID); + return (it == fProductIDs.end())? nullptr: &it->second.branchDescr; +} + + +// ----------------------------------------------------------------------------- +template +auto testing::mockup::Event::makeKey(art::InputTag tag) -> ProductKey + { return { std::move(tag), std::type_index{ typeid(T) } }; } + + +// ----------------------------------------------------------------------------- +inline art::InputTag testing::mockup::Event::completeTag + (art::InputTag tag) const +{ + if (tag.process().empty()) + return art::InputTag{ tag.label(), tag.instance(), fProcessName }; + else return tag; +} // art::InputTag testing::mockup::Event::completeTag() + + +// ----------------------------------------------------------------------------- +// --- testing::mockup::Handle and related +// ----------------------------------------------------------------------------- +template +void testing::mockup::details::HandleBase::checkValidity() const { + if (fData) return; + throw art::Exception(art::errors::NullPointerError) + << "Attempt to de-reference product that points to 'nullptr'.\n"; +} + + +// ----------------------------------------------------------------------------- + +#endif // ICARUSALG_TEST_FRAMEWORKEVENTMOCKUP_H diff --git a/test/Utilities/AssnsCrosser_test.cc b/test/Utilities/AssnsCrosser_test.cc new file mode 100644 index 0000000..5dec7bd --- /dev/null +++ b/test/Utilities/AssnsCrosser_test.cc @@ -0,0 +1,1117 @@ +/** + * @file AssnsCrosser_test.cc + * @brief Unit test for `sbn::ns::util::AssnsCrosser` class and utilities. + * @author Gianluca Petrillo (petrillo@slac.stanford.edu) + * @date June 9, 2023 + * @see sbnalg/Utilities/AssnsCrosser.h + */ + + +// Boost libraries +#define BOOST_TEST_MODULE AssnsCrosser +#include // BOOST_AUTO_TEST_CASE() +#include // BOOST_TEST() + +// library to test +#include "sbnalg/Utilities/AssnsCrosser.h" + +// ICARUS and LArSoft libraries +#include "test/FrameworkEventMockup.h" +#include "larcorealg/CoreUtils/enumerate.h" + +// C/C++ standard libraries +#include +#include +#include +#include // std::runtime_error +#include +#include +#include // std::move() +#include + + +//------------------------------------------------------------------------------ +// test data +template +class DataType { + + public: + + static constexpr std::size_t tag = Tag; + + static constexpr std::size_t NoID = 0; + + DataType(std::size_t ID = NoID): fID(ID) {} + + operator std::string() const + { + return + "DataType<" + std::to_string(tag) + ">[ID=" + std::to_string(fID) + "]"; + } + + private: + std::size_t fID = NoID; + +}; // DataType<> + + +struct DataTypeA: DataType<1> { using DataType<1>::DataType; }; +struct DataTypeB: DataType<2> { using DataType<2>::DataType; }; +struct DataTypeC: DataType<3> { using DataType<3>::DataType; }; +struct DataTypeD: DataType<4> { using DataType<4>::DataType; }; +struct DataTypeE: DataType<5> { using DataType<5>::DataType; }; +struct DataTypeF: DataType<6> { using DataType<6>::DataType; }; +struct DataTypeG: DataType<7> { using DataType<7>::DataType; }; + + +//------------------------------------------------------------------------------ +testing::mockup::Event makeTestEvent1() { + std::vector dataA { // 16 + /* 0 */ DataTypeA{ 0 }, + /* 1 */ DataTypeA{ 16 }, + /* 2 */ DataTypeA{ 32 }, + /* 3 */ DataTypeA{ 48 }, + /* 4 */ DataTypeA{ 64 } + }; + std::vector dataA1 { // 16 + /* 0 */ DataTypeA{ 0 }, + /* 1 */ DataTypeA{ 16 }, + }; + std::vector dataA2 { // 16 + /* 0 */ DataTypeA{ 32 }, + /* 1 */ DataTypeA{ 48 }, + /* 2 */ DataTypeA{ 64 } + }; + + std::vector dataB { // 8 + /* 0 */ DataTypeB{ 16 }, + /* 1 */ DataTypeB{ 24 }, + /* 2 */ DataTypeB{ 32 }, + /* 3 */ DataTypeB{ 48 }, + /* 4 */ DataTypeB{ 56 } + }; + + std::vector dataC { // 4 + /* 0 */ DataTypeC{ 16 }, + /* 1 */ DataTypeC{ 20 }, + /* 2 */ DataTypeC{ 24 }, + /* 3 */ DataTypeC{ 28 }, + /* 4 */ DataTypeC{ 32 }, + /* 5 */ DataTypeC{ 56 }, + /* 6 */ DataTypeC{ 60 }, + /* 7 */ DataTypeC{ 64 }, + /* 8 */ DataTypeC{ 72 } + }; + + std::vector dataD { // 2 + /* 0 */ DataTypeD{ 16 }, + /* 1 */ DataTypeD{ 18 }, + /* 2 */ DataTypeD{ 28 }, + /* 3 */ DataTypeD{ 36 }, + /* 4 */ DataTypeD{ 60 }, + /* 5 */ DataTypeD{ 64 }, + /* 6 */ DataTypeD{ 72 }, + /* 7 */ DataTypeD{ 76 }, + }; + + testing::mockup::Event event; + + event.put(std::move(dataA), art::InputTag{ "A" }); + event.put(std::move(dataA1), art::InputTag{ "A1" }); + event.put(std::move(dataA2), art::InputTag{ "A2" }); + event.put(std::move(dataB), art::InputTag{ "B" }); + event.put(std::move(dataC), art::InputTag{ "C" }); + event.put(std::move(dataD), art::InputTag{ "D" }); + + testing::mockup::PtrMaker makeAptr{ event, art::InputTag{ "A" } }; + testing::mockup::PtrMaker makeA1ptr + { event, art::InputTag{ "A1" } }; + testing::mockup::PtrMaker makeA2ptr + { event, art::InputTag{ "A2" } }; + testing::mockup::PtrMaker makeBptr{ event, art::InputTag{ "B" } }; + testing::mockup::PtrMaker makeCptr{ event, art::InputTag{ "C" } }; + testing::mockup::PtrMaker makeDptr{ event, art::InputTag{ "D" } }; + + /* + * The plan: + * A[0] <=> none + * A[1] <=> B[0], B[1] + * A[2] <=> B[2] + * A[3] <=> B[3] + * A[4] <=> none + * none <=> B[4] + * + * B[0] <=> C[0], C[1] + * B[1] <=> C[2], C[3] + * B[2] <=> C[4] + * B[3] <=> none + * B[4] <=> C[5], C[6] + * none <=> C[7] + * none <=> C[8] + * + * C[0] <=> D[0], D[1] + * C[1] <=> none + * C[2] <=> none + * C[3] <=> D[2] + * none <=> D[3] + * C[4] <=> none + * C[5] <=> none + * C[6] <=> D[4] + * C[7] <=> D[5] + * C[8] <=> D[6] + * none <=> D[7] + * + * A1[0] <=> none + * A1[1] <=> B[0], B[1] + * none <=> B[2] + * none <=> B[3] + * none <=> B[4] + * + * none <=> B[0], B[1] + * A2[0] <=> B[2] + * A2[1] <=> B[3] + * A2[2] <=> none + * none <=> B[4] + * + * + * A[0] <=> none <=> none <=> none + * A[1] <=> B[0..1] <=> C[0..3] <=> D[0..2] + * A[2] <=> B[2] <=> C[4] <=> none + * A[3] <=> B[3] <=> none <=> none + * A[4] <=> none <=> none <=> none + * + * A1[0] <=> none <=> none <=> none + * A1[1] <=> B[0..1] <=> C[0..3] <=> D[0..2] + * A2[0] <=> B[2] <=> C[4] <=> none + * A2[1] <=> B[3] <=> none <=> none + * A2[2] <=> none <=> none <=> none + * + */ + art::Assns assnsAB; + assnsAB.addSingle(makeAptr(1), makeBptr(0)); + assnsAB.addSingle(makeAptr(1), makeBptr(1)); + assnsAB.addSingle(makeAptr(2), makeBptr(2)); + assnsAB.addSingle(makeAptr(3), makeBptr(3)); + event.put(std::move(assnsAB), art::InputTag{ "B" }); + + art::Assns assnsA1B; + assnsA1B.addSingle(makeA1ptr(1), makeBptr(0)); + assnsA1B.addSingle(makeA1ptr(1), makeBptr(1)); + event.put(std::move(assnsA1B), art::InputTag{ "B:1" }); + + art::Assns assnsA2B; + assnsA2B.addSingle(makeA2ptr(0), makeBptr(2)); + assnsA2B.addSingle(makeA2ptr(1), makeBptr(3)); + event.put(std::move(assnsA2B), art::InputTag{ "B:2" }); + + art::Assns assnsBC; + assnsBC.addSingle(makeBptr(0), makeCptr(0)); + assnsBC.addSingle(makeBptr(0), makeCptr(1)); + assnsBC.addSingle(makeBptr(1), makeCptr(2)); + assnsBC.addSingle(makeBptr(1), makeCptr(3)); + assnsBC.addSingle(makeBptr(2), makeCptr(4)); + assnsBC.addSingle(makeBptr(4), makeCptr(5)); + assnsBC.addSingle(makeBptr(4), makeCptr(6)); + event.put(std::move(assnsBC), art::InputTag{ "C" }); + + art::Assns assnsCD; + assnsCD.addSingle(makeCptr(0), makeDptr(0)); + assnsCD.addSingle(makeCptr(0), makeDptr(1)); + assnsCD.addSingle(makeCptr(3), makeDptr(2)); + assnsCD.addSingle(makeCptr(6), makeDptr(4)); + assnsCD.addSingle(makeCptr(7), makeDptr(5)); + assnsCD.addSingle(makeCptr(8), makeDptr(6)); + event.put(std::move(assnsCD), art::InputTag{ "D" }); + + return event; +} // makeTestEvent1() + + +// ----------------------------------------------------------------------------- +void AssnsCrosser1_test() { + /* + * Test with a single association. + * + * The plan: + * A[0] <=> none + * A[1] <=> B[0], B[1] + * A[2] <=> B[2] + * A[3] <=> B[3] + * A[4] <=> none + * none <=> B[4] + */ + + testing::mockup::Event const event = makeTestEvent1(); + + testing::mockup::PtrMaker makeAptr{ event, art::InputTag{ "A" } }; + testing::mockup::PtrMaker makeBptr{ event, art::InputTag{ "B" } }; + + sbn::ns::util::AssnsCrosser AtoB + { event, art::InputTag{ "B" } }; + + { + auto const& Bs = AtoB.assPtrs(makeAptr(0)); + static_assert + (std::is_same_v> const&>); + + BOOST_TEST(Bs.empty()); + } + + { + auto const& Bs = AtoB.assPtrs(makeAptr(1)); + + BOOST_TEST(Bs.size() == 2); + if (Bs.size() > 0) BOOST_TEST(Bs[0] == makeBptr(0)); + if (Bs.size() > 1) BOOST_TEST(Bs[1] == makeBptr(1)); + } + + { + auto const& Bs = AtoB.assPtrs(makeAptr(2)); + + BOOST_TEST(Bs.size() == 1); + if (Bs.size() > 0) BOOST_TEST(Bs[0] == makeBptr(2)); + } + + { + auto const& Bs = AtoB.assPtrs(makeAptr(3)); + + BOOST_TEST(Bs.size() == 1); + if (Bs.size() > 0) BOOST_TEST(Bs[0] == makeBptr(3)); + } + + { + auto const& Bs = AtoB.assPtrs(makeAptr(4)); + + BOOST_TEST(Bs.empty()); + } + + { + auto const& Bs = AtoB.assPtrs(makeAptr(5)); + + BOOST_TEST(Bs.empty()); + } + + { + auto const& Bs = AtoB.assPtrs(makeAptr(6)); + static_assert + (std::is_same_v> const&>); + BOOST_TEST(Bs.empty()); + } + +} // AssnsCrosser1_test() + + +//------------------------------------------------------------------------------ +void AssnsCrosser2check( + testing::mockup::Event const& event, + sbn::ns::util::AssnsCrosser const& AtoC +) { + /* + * Test with a three-hop association. + * + * The plan: + * A[0] <=> none + * A[1] <=> B[0], B[1] + * A[2] <=> B[2] + * A[3] <=> B[3] + * A[4] <=> none + * none <=> B[4] + * + * B[0] <=> C[0], C[1] + * B[1] <=> C[2], C[3] + * B[2] <=> C[4] + * B[3] <=> none + * B[4] <=> C[5], C[6] + * none <=> C[7] + * none <=> C[8] + * + * A[0] <=> none <=> none <=> none + * A[1] <=> B[0..1] <=> C[0..3] <=> D[0..2] + * A[2] <=> B[2] <=> C[4] <=> none + * A[3] <=> B[3] <=> none <=> none + * A[4] <=> none <=> none <=> none + * + */ + + testing::mockup::PtrMaker makeAptr{ event, art::InputTag{ "A" } }; + testing::mockup::PtrMaker makeCptr{ event, art::InputTag{ "C" } }; + + { + auto const& Cs = AtoC.assPtrs(makeAptr(0)); + static_assert + (std::is_same_v> const&>); + + BOOST_TEST(Cs.empty()); + } + + { + auto const& Cs = AtoC.assPtrs(makeAptr(1)); + + BOOST_TEST(Cs.size() == 4); + if (Cs.size() > 0) BOOST_TEST(Cs[0] == makeCptr(0)); + if (Cs.size() > 1) BOOST_TEST(Cs[1] == makeCptr(1)); + if (Cs.size() > 2) BOOST_TEST(Cs[2] == makeCptr(2)); + if (Cs.size() > 3) BOOST_TEST(Cs[3] == makeCptr(3)); + } + + { + auto const& Cs = AtoC.assPtrs(makeAptr(2)); + + BOOST_TEST(Cs.size() == 1); + if (Cs.size() > 0) BOOST_TEST(Cs[0] == makeCptr(4)); + } + + { + auto const& Cs = AtoC.assPtrs(makeAptr(3)); + + BOOST_TEST(Cs.empty()); + } + + { + auto const& Cs = AtoC.assPtrs(makeAptr(4)); + + BOOST_TEST(Cs.empty()); + } + + { + auto const& Cs = AtoC.assPtrs(makeAptr(5)); + static_assert + (std::is_same_v> const&>); + BOOST_TEST(Cs.empty()); + } + +} // AssnsCrosser2check() + + +//------------------------------------------------------------------------------ +void AssnsCrosser2_test() { + /* + * Test with a two-hop association. + * + * The plan: + * A[0] <=> none + * A[1] <=> B[0], B[1] + * A[2] <=> B[2] + * A[3] <=> B[3] + * A[4] <=> none + * none <=> B[4] + * + * B[0] <=> C[0], C[1] + * B[1] <=> C[2], C[3] + * B[2] <=> C[4] + * B[3] <=> none + * B[4] <=> C[5], C[6] + * none <=> C[7] + * none <=> C[8] + * + * A[0] <=> none + * A[1] <=> B[0..1] <=> C[0..3] + * A[2] <=> B[2] <=> C[4] + * A[3] <=> B[3] <=> none + * A[4] <=> none <=> none + * + */ + + testing::mockup::Event const event = makeTestEvent1(); + + sbn::ns::util::AssnsCrosser AtoC + { event, art::InputTag{ "B" }, art::InputTag{ "C" } }; + + BOOST_TEST_CONTEXT("Test: 2 hops with full InputTag specification") { + AssnsCrosser2check(event, AtoC); + } + +} // AssnsCrosser2_test() + + +//------------------------------------------------------------------------------ +void AssnsCrosserDiamond_test() { + /* + * Test with a diamond association. + */ + + std::vector dataA { DataTypeA{ 10 } }; + std::vector dataB { DataTypeB{ 20 }, DataTypeB{ 21 } }; + std::vector dataC { DataTypeC{ 30 } }; + + testing::mockup::Event event; + + event.put(std::move(dataA), art::InputTag{ "A" }); + event.put(std::move(dataB), art::InputTag{ "B" }); + event.put(std::move(dataC), art::InputTag{ "C" }); + + testing::mockup::PtrMaker makeAptr{ event, art::InputTag{ "A" } }; + testing::mockup::PtrMaker makeBptr{ event, art::InputTag{ "B" } }; + testing::mockup::PtrMaker makeCptr{ event, art::InputTag{ "C" } }; + + /* + * The plan: + * A[1] <=> B[0], B[1] + * + * B[0] <=> C[0], + * B[1] <=> C[0] + * + * A[0] <=> B[0..1] <=> C[0] (but via two paths) + */ + art::Assns assnsAB; + assnsAB.addSingle(makeAptr(0), makeBptr(0)); + assnsAB.addSingle(makeAptr(0), makeBptr(1)); + event.put(std::move(assnsAB), art::InputTag{ "B" }); + + art::Assns assnsBC; + assnsBC.addSingle(makeBptr(0), makeCptr(0)); + assnsBC.addSingle(makeBptr(1), makeCptr(0)); + event.put(std::move(assnsBC), art::InputTag{ "C" }); + + using sbn::ns::util::hopTo; + + auto const AtoC = sbn::ns::util::makeAssnsCrosser + (event, hopTo("B"), hopTo("C")); + + auto const& Cs = AtoC.assPtrs(makeAptr(0)); + + BOOST_TEST(Cs.size() == 2); + if (Cs.size() > 0) BOOST_TEST(Cs[0] == makeCptr(0)); + if (Cs.size() > 1) BOOST_TEST(Cs[1] == makeCptr(0)); + +} // AssnsCrosserDiamond_test() + + +//------------------------------------------------------------------------------ +void AssnsCrosser3check( + testing::mockup::Event const& event, + sbn::ns::util::AssnsCrosser const& AtoD +) { + /* + * Test with a three-hop association. + * + * The plan: + * A[0] <=> none + * A[1] <=> B[0], B[1] + * A[2] <=> B[2] + * A[3] <=> B[3] + * A[4] <=> none + * none <=> B[4] + * + * B[0] <=> C[0], C[1] + * B[1] <=> C[2], C[3] + * B[2] <=> C[4] + * B[3] <=> none + * B[4] <=> C[5], C[6] + * none <=> C[7] + * none <=> C[8] + * + * C[0] <=> D[0], D[1] + * C[1] <=> none + * C[2] <=> none + * C[3] <=> D[2] + * none <=> D[3] + * C[4] <=> none + * C[5] <=> none + * C[6] <=> D[4] + * C[7] <=> D[5] + * C[8] <=> D[6] + * none <=> D[7] + * + * A[0] <=> none <=> none <=> none + * A[1] <=> B[0..1] <=> C[0..3] <=> D[0..2] + * A[2] <=> B[2] <=> C[4] <=> none + * A[3] <=> B[3] <=> none <=> none + * A[4] <=> none <=> none <=> none + * + */ + + testing::mockup::PtrMaker makeAptr{ event, art::InputTag{ "A" } }; + testing::mockup::PtrMaker makeDptr{ event, art::InputTag{ "D" } }; + + { + auto const& Ds = AtoD.assPtrs(makeAptr(0)); + static_assert + (std::is_same_v> const&>); + + BOOST_TEST(Ds.empty()); + } + + { + auto const& Ds = AtoD.assPtrs(makeAptr(1)); + + BOOST_TEST(Ds.size() == 3); + if (Ds.size() > 0) BOOST_TEST(Ds[0] == makeDptr(0)); + if (Ds.size() > 1) BOOST_TEST(Ds[1] == makeDptr(1)); + if (Ds.size() > 2) BOOST_TEST(Ds[2] == makeDptr(2)); + } + + { + auto const& Ds = AtoD.assPtrs(makeAptr(2)); + BOOST_TEST(Ds.empty()); + } + + { + auto const& Ds = AtoD.assPtrs(makeAptr(3)); + BOOST_TEST(Ds.empty()); + } + + { + auto const& Ds = AtoD.assPtrs(makeAptr(4)); + BOOST_TEST(Ds.empty()); + } + + { + auto const& Ds = AtoD.assPtrs(makeAptr(5)); + static_assert + (std::is_same_v> const&>); + BOOST_TEST(Ds.empty()); + } + +} // AssnsCrosser3check() + + +//------------------------------------------------------------------------------ +void AssnsCrosser3_test() { + + testing::mockup::Event const event = makeTestEvent1(); + + sbn::ns::util::AssnsCrosser + const AtoD + { event, art::InputTag{ "B" }, art::InputTag{ "C" }, art::InputTag{ "D" } }; + + AssnsCrosser3check(event, AtoD); + +} // AssnsCrosser3_test() + + +//------------------------------------------------------------------------------ +void AssnsCrosser3withID_test() { + + testing::mockup::Event const event = makeTestEvent1(); + + // the tag of B <=> C associations is the same as the one of the C data; + // we have lost track of the ID of the latter, but we can still ask the event + art::ProductID const dataC_ID + = event.getProductID>("C"); + BOOST_TEST_REQUIRE(dataC_ID != art::ProductID{}); + + sbn::ns::util::AssnsCrosser + const AtoD + { event, art::InputTag{ "B" }, dataC_ID, art::InputTag{ "D" } }; + + BOOST_TEST_CONTEXT("Test: 3 hops with a product ID") { + AssnsCrosser3check(event, AtoD); + } + +} // AssnsCrosser3withID_test() + + +//------------------------------------------------------------------------------ +void AssnsCrosser3withJump_test() { + /* + * In this test, the first hop should be discovered. + * + * The selected algorithm should be the backward one + * (because the forward one has no starting point) + * and the "D" associations should point to "C" data, + * the (implicitly converted) "C" input tag should point to "B" data, + * and "B" tag should also denote an association to "A". + * The tag "B" should be discovered from the left pointers of the "C" + * association. + */ + + testing::mockup::Event const event = makeTestEvent1(); + + sbn::ns::util::AssnsCrosser + const AtoD + { event, {}, "C", art::InputTag{ "D" } }; + + BOOST_TEST_CONTEXT("Test: 3 hops with autodetection of first hop") { + AssnsCrosser3check(event, AtoD); + } + +} // AssnsCrosser3withJump_test() + + +//------------------------------------------------------------------------------ +void AssnsCrosser3with2jumps_test() { + /* + * In this test, the first and second hops should be discovered. + * + * The selected algorithm should be the backward one + * (because the forward one has no starting point) + * and the "D" associations should point to "C" data, + * and "C" tag should also denote an association to "B". + * The tag "C" should be discovered from the left pointers of the "D" + * association. + * The same should afterward happen from "C" to "B". + */ + + testing::mockup::Event const event = makeTestEvent1(); + + sbn::ns::util::AssnsCrosser + const AtoD + { event, {}, {}, "D" }; + + BOOST_TEST_CONTEXT("Test: 3 hops with autodetection of first and second hop") + { + AssnsCrosser3check(event, AtoD); + } + +} // AssnsCrosser3with2jumps_test() + + +//------------------------------------------------------------------------------ +void AssnsCrosserInputList1_test() { + /* + * Test with a two-hop association. + * + * The plan: + * A1[0] <=> none + * A1[1] <=> B[0], B[1] + * A2[0] <=> B[2] + * A2[1] <=> B[3] + * A2[2] <=> none + * none <=> B[4] + * + * B[0] <=> C[0], C[1] + * B[1] <=> C[2], C[3] + * B[2] <=> C[4] + * B[3] <=> none + * B[4] <=> C[5], C[6] + * none <=> C[7] + * none <=> C[8] + * + * A1[0] <=> none + * A1[1] <=> B[0..1] <=> C[0..3] + * A2[0] <=> B[2] <=> C[4] + * A2[1] <=> B[3] <=> none + * A2[2] <=> none <=> none + * + */ + testing::mockup::Event event = makeTestEvent1(); + + testing::mockup::PtrMaker makeA1ptr + { event, art::InputTag{ "A1" } }; + testing::mockup::PtrMaker makeA2ptr + { event, art::InputTag{ "A2" } }; + testing::mockup::PtrMaker makeCptr{ event, art::InputTag{ "C" } }; + + // note that the associations between A1/2 and B are called B:1 and B:2 + using sbn::ns::util::hopTo; + auto const AtoC = sbn::ns::util::makeAssnsCrosser( + event, + hopTo{ "B:1", "B:2" }, hopTo{ "C" } + ); + + { + auto const& Cs = AtoC.assPtrs(makeA1ptr(0)); + static_assert + (std::is_same_v> const&>); + + BOOST_TEST(Cs.empty()); + } + + { + auto const& Cs = AtoC.assPtrs(makeA1ptr(1)); + + BOOST_TEST(Cs.size() == 4); + if (Cs.size() > 0) BOOST_TEST(Cs[0] == makeCptr(0)); + if (Cs.size() > 1) BOOST_TEST(Cs[1] == makeCptr(1)); + if (Cs.size() > 2) BOOST_TEST(Cs[2] == makeCptr(2)); + if (Cs.size() > 3) BOOST_TEST(Cs[3] == makeCptr(3)); + } + + { + auto const& Cs = AtoC.assPtrs(makeA1ptr(2)); + static_assert + (std::is_same_v> const&>); + BOOST_TEST(Cs.empty()); + } + + { + auto const& Cs = AtoC.assPtrs(makeA2ptr(0)); + + BOOST_TEST(Cs.size() == 1); + if (Cs.size() > 0) BOOST_TEST(Cs[0] == makeCptr(4)); + } + + { + auto const& Cs = AtoC.assPtrs(makeA2ptr(1)); + + BOOST_TEST(Cs.empty()); + } + + { + auto const& Cs = AtoC.assPtrs(makeA2ptr(2)); + + BOOST_TEST(Cs.empty()); + } + + { + auto const& Cs = AtoC.assPtrs(makeA2ptr(3)); + static_assert + (std::is_same_v> const&>); + BOOST_TEST(Cs.empty()); + } + +} // AssnsCrosserInputList1_test() + + +//------------------------------------------------------------------------------ +void AssnsCrosserStartList1_test() { + + using sbn::ns::util::startFrom; + + testing::mockup::Event const event = makeTestEvent1(); + + /* + // the tag of B <=> C associations is the same as the one of the C data; + // we have lost track of the ID of the latter, but we can still ask the event + art::ProductID const dataC_ID + = event.getProductID>("C"); + BOOST_TEST_REQUIRE(dataC_ID != art::ProductID{}); + */ + + testing::mockup::PtrMaker makeAptr{ event, art::InputTag{ "A" } }; + testing::mockup::PtrMaker makeCptr{ event, art::InputTag{ "C" } }; + + sbn::ns::util::AssnsCrosser const AtoC + { event, { makeAptr(2), makeAptr(3) }, "B", "C" }; + + { + auto const& Cs = AtoC.assPtrs(makeAptr(0)); + static_assert + (std::is_same_v> const&>); + + BOOST_TEST(Cs.empty()); + } + + { + auto const& Cs = AtoC.assPtrs(makeAptr(1)); + + BOOST_TEST(Cs.empty()); + } + + { + auto const& Cs = AtoC.assPtrs(makeAptr(2)); + + BOOST_TEST(Cs.size() == 1); + if (Cs.size() > 0) BOOST_TEST(Cs[0] == makeCptr(4)); + } + + { + auto const& Cs = AtoC.assPtrs(makeAptr(3)); + + BOOST_TEST(Cs.empty()); + } + + { + auto const& Cs = AtoC.assPtrs(makeAptr(4)); + + BOOST_TEST(Cs.empty()); + } + + { + auto const& Cs = AtoC.assPtrs(makeAptr(5)); + static_assert + (std::is_same_v> const&>); + BOOST_TEST(Cs.empty()); + } + +} // AssnsCrosserStartList1_test() + + +//------------------------------------------------------------------------------ +void AssnsCrosserStartList2_test() { + + using sbn::ns::util::startFrom; + + testing::mockup::Event const event = makeTestEvent1(); + + art::ProductID const dataA_ID + = event.getProductID>("A"); + BOOST_TEST_REQUIRE(dataA_ID != art::ProductID{}); + + sbn::ns::util::AssnsCrosser const AtoC + { event, dataA_ID, "B", "C" }; + + BOOST_TEST_CONTEXT("Test: 2 hops with a product ID") { + AssnsCrosser2check(event, AtoC); + } + +} // AssnsCrosserStartList2_test() + + +//------------------------------------------------------------------------------ +void AssnsCrosserStartList3_test() { + + using sbn::ns::util::startFrom; + + testing::mockup::Event const event = makeTestEvent1(); + + // startFrom{} is not required, but it increases readability + sbn::ns::util::AssnsCrosser const AtoC + { event, startFrom{ "A" }, "B", "C" }; + + BOOST_TEST_CONTEXT("Test: 2 hops with an input tag start") { + AssnsCrosser2check(event, AtoC); + } + +} // AssnsCrosserStartList3_test() + + +//------------------------------------------------------------------------------ +void AssnsCrosserStartList4_test() { + + using sbn::ns::util::startFrom; + + testing::mockup::Event const event = makeTestEvent1(); + + testing::mockup::PtrMaker makeAptr{ event, art::InputTag{ "A" } }; + testing::mockup::PtrMaker makeCptr{ event, art::InputTag{ "C" } }; + + std::vector const startA{ makeAptr(2), makeAptr(3) }; + sbn::ns::util::AssnsCrosser const AtoC + { event, startA, "B", "C" }; + + { + auto const& Cs = AtoC.assPtrs(makeAptr(0)); + static_assert + (std::is_same_v> const&>); + + BOOST_TEST(Cs.empty()); + } + + { + auto const& Cs = AtoC.assPtrs(makeAptr(1)); + + BOOST_TEST(Cs.empty()); + } + + { + auto const& Cs = AtoC.assPtrs(makeAptr(2)); + + BOOST_TEST(Cs.size() == 1); + if (Cs.size() > 0) BOOST_TEST(Cs[0] == makeCptr(4)); + } + + { + auto const& Cs = AtoC.assPtrs(makeAptr(3)); + + BOOST_TEST(Cs.empty()); + } + + { + auto const& Cs = AtoC.assPtrs(makeAptr(4)); + + BOOST_TEST(Cs.empty()); + } + + { + auto const& Cs = AtoC.assPtrs(makeAptr(5)); + static_assert + (std::is_same_v> const&>); + BOOST_TEST(Cs.empty()); + } + +} // AssnsCrosserStartList4_test() + + +//------------------------------------------------------------------------------ +void AssnsCrosserClassDocumentation_test() { + + /* + * The promise: + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} + * sbn::ns::util::AssnsCrosser const AtoC + * { event, art::InputTag{ "B" }, art::InputTag{ "C" } }; + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * or + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} + * using sbn::ns::util::hopTo; + * auto const AtoC = sbn::ns::util::makeAssnsCrosser + * (event, hopTo{ "B" }, hopTo{ "C" }); + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * or + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} + * using sbn::ns::util::startFrom, sbn::ns::util::hopTo; + * sbn::ns::util::AssnsCrosser const AtoC{ event + * , startFrom{} + * , hopTo{ "B" } + * , hopTo{ "C" } + * }; + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * or + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} + * using sbn::ns::util::startFrom, sbn::ns::util::hopTo; + * auto const AtoC = makeAssnsCrosser(event + * , startFrom{} + * , hopTo{ "B" } + * , hopTo{ "C" } + * ); + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * The latter describe more clearly the relation between the data types and + * their input tags. + * + * If there are two sets of associations between `DataTypeA` and `DataTypeB`, + * `"B:1"` and `"B:2"`, the following initializations will work: + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} + * sbn::ns::util::AssnsCrosser const AtoC + * { event, { "B:1", "B:2" }, { "C" } }; + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * or + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.cpp} + * using sbn::ns::util::hopTo; + * auto const AtoC = sbn::ns::util::makeAssnsCrosser( + * event, + * hopTo{ "B:1", "B:2" }, hopTo{ "C" } + * ); + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + */ + + using ExpectedAtoC_t + = sbn::ns::util::AssnsCrosser const; + + testing::mockup::Event const event = makeTestEvent1(); + + { + sbn::ns::util::AssnsCrosser const AtoC + [[maybe_unused]] + { event, art::InputTag{ "B" }, art::InputTag{ "C" } }; + static_assert(std::is_same_v); + } + + { + using sbn::ns::util::hopTo; + auto const AtoC = sbn::ns::util::makeAssnsCrosser + (event, hopTo{ "B" }, hopTo{ "C" }); + static_assert(std::is_same_v); + } + + { + using sbn::ns::util::startFrom, sbn::ns::util::hopTo; + sbn::ns::util::AssnsCrosser const AtoC{ event + , startFrom{} + , hopTo{ "B" } + , hopTo{ "C" } + }; + static_assert(std::is_same_v); + } + + { + using sbn::ns::util::startFrom, sbn::ns::util::hopTo; + auto const AtoC = makeAssnsCrosser(event + , startFrom{} + , hopTo{ "B" } + , hopTo{ "C" } + ); + static_assert(std::is_same_v); + } + + { + sbn::ns::util::AssnsCrosser const AtoC + { event, { "B:1", "B:2" }, { "C" } }; + static_assert(std::is_same_v); + } + + { + using sbn::ns::util::hopTo; + auto const AtoC = sbn::ns::util::makeAssnsCrosser( + event, + hopTo{ "B:1", "B:2" }, hopTo{ "C" } + ); + static_assert(std::is_same_v); + } + +} // AssnsCrosserClassDocumentation_test() + + +//------------------------------------------------------------------------------ +void InputSpecsClassDocumentation_test() { + + using sbn::ns::util::InputSpecs, sbn::ns::util::InputSpec; + + using AtoZ_t = sbn::ns::util::AssnsCrosser< + DataTypeA, DataTypeB, DataTypeC, DataTypeD, DataTypeE, DataTypeF + >; + + // the purpose is to confirm that this code compiles + using instantiated [[maybe_unused]] = decltype( + AtoZ_t{ std::declval() + + // implicit conversion to `art::InputTag`: + , InputSpecs{ "TagB" } + + // implicit conversion to `art::InputTag` then to `InputSpecs`: + , "TagC" + + // explicit vector of input tags (not recommended): + , InputSpecs{ std::vector{ "TagD1", "TagD2" } } + + // list of input tags, converted to `InputSpecs`: + , InputSpecs{ "TagE1", "TagE2" } + + // implicit list of input tags, converted to `InputSpecs`: + , { "TagF1", "TagF2" } + + } + ); + +} // InputSpecsClassDocumentation_test() + + +//------------------------------------------------------------------------------ +//--- The tests +//--- +BOOST_AUTO_TEST_CASE( AssnsCrosser1_testCase ) { + + AssnsCrosser1_test(); + +} // BOOST_AUTO_TEST_CASE( AssnsCrosser1_testCase ) + + +BOOST_AUTO_TEST_CASE( AssnsCrosser2_testCase ) { + + AssnsCrosser2_test(); + AssnsCrosserDiamond_test(); + +} // BOOST_AUTO_TEST_CASE( AssnsCrosser2_testCase ) + + +BOOST_AUTO_TEST_CASE( AssnsCrosser3_testCase ) { + + AssnsCrosser3_test(); + +} // BOOST_AUTO_TEST_CASE( AssnsCrosser3_testCase ) + + +BOOST_AUTO_TEST_CASE( AssnsCrosserInput_testCase ) { + + // tests with different input specification styles + AssnsCrosserInputList1_test(); + AssnsCrosser3withID_test(); + AssnsCrosser3withJump_test(); + AssnsCrosser3with2jumps_test(); + +} // BOOST_AUTO_TEST_CASE( AssnsCrosserInput_testCase ) + + +BOOST_AUTO_TEST_CASE( AssnsCrosserStart_testCase ) { + + // tests with different start specification styles + AssnsCrosserStartList1_test(); + AssnsCrosserStartList2_test(); + AssnsCrosserStartList3_test(); + AssnsCrosserStartList4_test(); + +} // BOOST_AUTO_TEST_CASE( AssnsCrosserStart_testCase ) + + +BOOST_AUTO_TEST_CASE( AssnsCrosserDocumentation_testCase ) { + + AssnsCrosserClassDocumentation_test(); + InputSpecsClassDocumentation_test(); + +} // BOOST_AUTO_TEST_CASE( AssnsCrosserDocumentation_testCase ) + + +//------------------------------------------------------------------------------ + diff --git a/test/Utilities/CMakeLists.txt b/test/Utilities/CMakeLists.txt new file mode 100644 index 0000000..f5e9f54 --- /dev/null +++ b/test/Utilities/CMakeLists.txt @@ -0,0 +1,8 @@ +cet_test(AssnsCrosser_test + LIBRARIES + sbnalg::Utilities + sbnalg::Test + canvas::canvas + cetlib::cetlib + USE_BOOST_UNIT + )