From 0ef3b1a195ef7c50813d86eec7dc369356b41be2 Mon Sep 17 00:00:00 2001 From: Emily Boudreaux Date: Sun, 12 Oct 2025 10:12:49 -0400 Subject: [PATCH] feat(Composition): Composition now inherits from abstract base class The composition object is now a specialization of the abstract base CompositionAbstract. This interface enforces getters but not setters (those are left up to children). The intention here is that other code can specialize particular getters for cases where special handling (like unrestricted amounts of one species) are required. --- .../include/fourdst/composition/composition.h | 56 +++++++++------- .../composition/composition_abstract.h | 44 +++++++++++++ src/composition/lib/composition.cpp | 27 ++++++-- tests/composition/compositionTest.cpp | 64 +++++++++++++++---- 4 files changed, 150 insertions(+), 41 deletions(-) create mode 100644 src/composition/include/fourdst/composition/composition_abstract.h diff --git a/src/composition/include/fourdst/composition/composition.h b/src/composition/include/fourdst/composition/composition.h index 69af37a..5ffdaa6 100644 --- a/src/composition/include/fourdst/composition/composition.h +++ b/src/composition/include/fourdst/composition/composition.h @@ -28,6 +28,7 @@ #include "fourdst/config/config.h" #include "fourdst/logging/logging.h" +#include "fourdst/composition/composition_abstract.h" #include "fourdst/composition/atomicSpecies.h" namespace fourdst::composition { @@ -251,7 +252,7 @@ namespace fourdst::composition { * } * @endcode */ - class Composition { + class Composition : public CompositionAbstract { private: struct CompositionCache { std::optional globalComp; ///< Cached global composition data. @@ -340,7 +341,7 @@ namespace fourdst::composition { /** * @brief Default destructor. */ - ~Composition() = default; + ~Composition() override = default; /** * @brief Finalizes the composition, making it ready for querying. @@ -351,7 +352,7 @@ namespace fourdst::composition { * @return True if the composition is valid and successfully finalized, false otherwise. * @post If successful, `m_finalized` is true and global properties are computed. */ - bool finalize(bool norm=false); + [[nodiscard]] bool finalize(bool norm=false); /** * @brief Constructs a Composition and registers the given symbols. @@ -480,13 +481,13 @@ namespace fourdst::composition { * @brief Gets the registered symbols. * @return A set of registered symbols. */ - [[nodiscard]] std::set getRegisteredSymbols() const; + [[nodiscard]] std::set getRegisteredSymbols() const override; /** * @brief Get a set of all species that are registered in the composition. * @return A set of `atomic::Species` objects registered in the composition. */ - [[nodiscard]] std::set getRegisteredSpecies() const; + [[nodiscard]] std::set getRegisteredSpecies() const override; /** * @brief Sets the mass fraction for a given symbol. @@ -599,7 +600,7 @@ namespace fourdst::composition { * @return An unordered map of symbols to their mass fractions. * @throws exceptions::CompositionNotFinalizedError if the composition is not finalized. */ - [[nodiscard]] std::unordered_map getMassFraction() const; + [[nodiscard]] std::unordered_map getMassFraction() const override; /** * @brief Gets the mass fraction for a given symbol. @@ -609,7 +610,7 @@ namespace fourdst::composition { * @throws exceptions::CompositionNotFinalizedError if the composition is not finalized. * @throws exceptions::UnregisteredSymbolError if the symbol is not in the composition. */ - [[nodiscard]] double getMassFraction(const std::string& symbol) const; + [[nodiscard]] double getMassFraction(const std::string& symbol) const override; /** * @brief Gets the mass fraction for a given isotope. @@ -619,7 +620,7 @@ namespace fourdst::composition { * @throws exceptions::CompositionNotFinalizedError if the composition is not finalized. * @throws exceptions::UnregisteredSymbolError if the isotope is not registered in the composition. */ - [[nodiscard]] double getMassFraction(const fourdst::atomic::Species& species) const; + [[nodiscard]] double getMassFraction(const fourdst::atomic::Species& species) const override; /** * @brief Gets the number fraction for a given symbol. @@ -629,7 +630,7 @@ namespace fourdst::composition { * @throws exceptions::CompositionNotFinalizedError if the composition is not finalized. * @throws exceptions::UnregisteredSymbolError if the symbol is not in the composition. */ - [[nodiscard]] double getNumberFraction(const std::string& symbol) const; + [[nodiscard]] double getNumberFraction(const std::string& symbol) const override; /** * @brief Gets the number fraction for a given isotope. @@ -639,7 +640,7 @@ namespace fourdst::composition { * @throws exceptions::CompositionNotFinalizedError if the composition is not finalized. * @throws exceptions::UnregisteredSymbolError if the isotope is not registered in the composition. */ - [[nodiscard]] double getNumberFraction(const fourdst::atomic::Species& species) const; + [[nodiscard]] double getNumberFraction(const fourdst::atomic::Species& species) const override; /** * @brief Gets the number fractions of all species in the composition. @@ -647,7 +648,7 @@ namespace fourdst::composition { * @return An unordered map of symbols to their number fractions. * @throws exceptions::CompositionNotFinalizedError if the composition is not finalized. */ - [[nodiscard]] std::unordered_map getNumberFraction() const; + [[nodiscard]] std::unordered_map getNumberFraction() const override; /** * @brief Gets the molar abundance (X_i / A_i) for a given symbol. @@ -657,7 +658,7 @@ namespace fourdst::composition { * @throws exceptions::CompositionNotFinalizedError if the composition is not finalized. * @throws exceptions::UnregisteredSymbolError if the symbol is not in the composition. */ - [[nodiscard]] double getMolarAbundance(const std::string& symbol) const; + [[nodiscard]] double getMolarAbundance(const std::string& symbol) const override; /** * @brief Gets the molar abundance for a given isotope. @@ -667,7 +668,7 @@ namespace fourdst::composition { * @throws exceptions::CompositionNotFinalizedError if the composition is not finalized. * @throws exceptions::UnregisteredSymbolError if the isotope is not registered in the composition. */ - [[nodiscard]] double getMolarAbundance(const fourdst::atomic::Species& species) const; + [[nodiscard]] double getMolarAbundance(const fourdst::atomic::Species& species) const override; /** * @brief Gets the composition entry and global composition data for a given symbol. @@ -703,7 +704,7 @@ namespace fourdst::composition { * @return Mean particle mass in atomic mass units (g/mol). * @throws exceptions::CompositionNotFinalizedError if the composition is not finalized. */ - [[nodiscard]] double getMeanParticleMass() const; + [[nodiscard]] double getMeanParticleMass() const override; /** * @brief Compute the mean atomic number of the composition. @@ -711,7 +712,7 @@ namespace fourdst::composition { * @return Mean atomic number . * @throws exceptions::CompositionNotFinalizedError if the composition is not finalized. */ - [[nodiscard]] double getMeanAtomicNumber() const; + [[nodiscard]] double getMeanAtomicNumber() const override; /** * @brief Compute the electron abundance of the composition. @@ -719,7 +720,7 @@ namespace fourdst::composition { * @return Ye (electron abundance). * @pre The composition must be finalized. */ - [[nodiscard]] double getElectronAbundance() const; + [[nodiscard]] double getElectronAbundance() const override; /** * @brief Creates a new Composition object containing a subset of species from this one. @@ -737,7 +738,14 @@ namespace fourdst::composition { * @param symbol The symbol to check. * @return True if the symbol is registered, false otherwise. */ - [[nodiscard]] bool hasSymbol(const std::string& symbol) const; + [[nodiscard]] bool hasSymbol(const std::string& symbol) const override; + + /** + * @brief Checks if a species is registered in the composition. + * @param species The species to check. + * @return True if the species is registered, false otherwise. + */ + [[nodiscard]] bool hasSpecies(const fourdst::atomic::Species &species) const override; /** * @brief Checks if a given isotope is present in the composition. @@ -746,7 +754,7 @@ namespace fourdst::composition { * @return True if the isotope is in the composition, false otherwise. * @throws exceptions::CompositionNotFinalizedError if the composition is not finalized. */ - [[nodiscard]] bool contains(const atomic::Species& isotope) const; + [[nodiscard]] bool contains(const atomic::Species& isotope) const override; /** * @brief Sets the composition mode (mass fraction vs. number fraction). @@ -776,7 +784,7 @@ namespace fourdst::composition { * @pre The composition must be finalized. * @throws exceptions::CompositionNotFinalizedError if the composition is not finalized. */ - [[nodiscard]] std::vector getMassFractionVector() const; + [[nodiscard]] std::vector getMassFractionVector() const override; /** * @brief Get a uniform vector representation of the number fractions stored in the composition object sorted such that the lightest species is at index 0 and the heaviest is at the last index. @@ -785,7 +793,7 @@ namespace fourdst::composition { * @pre The composition must be finalized. * @throws exceptions::CompositionNotFinalizedError if the composition is not finalized. */ - [[nodiscard]] std::vector getNumberFractionVector() const; + [[nodiscard]] std::vector getNumberFractionVector() const override; /** * @brief Get a uniform vector representation of the molar abundances stored in the composition object sorted such that the lightest species is at index 0 and the heaviest is at the last index. @@ -794,7 +802,7 @@ namespace fourdst::composition { * @pre The composition must be finalized. * @throws exceptions::CompositionNotFinalizedError if the composition is not finalized. */ - [[nodiscard]] std::vector getMolarAbundanceVector() const; + [[nodiscard]] std::vector getMolarAbundanceVector() const override; /** * @brief get the index in the sorted vector representation for a given symbol @@ -806,7 +814,7 @@ namespace fourdst::composition { * @throws exceptions::UnregisteredSymbolError if the symbol is not registered in the composition * @return The index of the symbol in the sorted vector representation. */ - [[nodiscard]] size_t getSpeciesIndex(const std::string& symbol) const; + [[nodiscard]] size_t getSpeciesIndex(const std::string& symbol) const override; /** * @brief get the index in the sorted vector representation for a given symbol @@ -818,7 +826,7 @@ namespace fourdst::composition { * @throws exceptions::UnregisteredSymbolError if the symbol is not registered in the composition * @return The index of the symbol in the sorted vector representation. */ - [[nodiscard]] size_t getSpeciesIndex(const atomic::Species& species) const; + [[nodiscard]] size_t getSpeciesIndex(const atomic::Species& species) const override; /** * @brief Get the species at a given index in the sorted vector representation. @@ -829,7 +837,7 @@ namespace fourdst::composition { * @throws std::out_of_range if the index is out of range. * @return The species at the given index in the sorted vector representation. */ - [[nodiscard]] atomic::Species getSpeciesAtIndex(size_t index) const; + [[nodiscard]] atomic::Species getSpeciesAtIndex(size_t index) const override; /** * @brief Overloaded output stream operator for Composition. diff --git a/src/composition/include/fourdst/composition/composition_abstract.h b/src/composition/include/fourdst/composition/composition_abstract.h new file mode 100644 index 0000000..8da4851 --- /dev/null +++ b/src/composition/include/fourdst/composition/composition_abstract.h @@ -0,0 +1,44 @@ +#pragma once + +#include "fourdst/composition/atomicSpecies.h" + +#include +#include +#include + +class CompositionAbstract { +public: + virtual ~CompositionAbstract() = default; + [[nodiscard]] virtual bool hasSymbol(const std::string& symbol) const = 0; + [[nodiscard]] virtual bool hasSpecies(const fourdst::atomic::Species& species) const = 0; + + [[nodiscard]] virtual bool contains(const fourdst::atomic::Species& species) const = 0; + + [[nodiscard]] virtual std::set getRegisteredSymbols() const = 0; + [[nodiscard]] virtual std::set getRegisteredSpecies() const = 0; + + [[nodiscard]] virtual std::unordered_map getMassFraction() const = 0; + [[nodiscard]] virtual std::unordered_map getNumberFraction() const = 0; + + [[nodiscard]] virtual double getMassFraction(const std::string& symbol) const = 0; + [[nodiscard]] virtual double getMassFraction(const fourdst::atomic::Species& species) const = 0; + + [[nodiscard]] virtual double getNumberFraction(const std::string& symbol) const = 0; + [[nodiscard]] virtual double getNumberFraction(const fourdst::atomic::Species& species) const = 0; + + [[nodiscard]] virtual double getMolarAbundance(const std::string& symbol) const = 0; + [[nodiscard]] virtual double getMolarAbundance(const fourdst::atomic::Species& species) const = 0; + + [[nodiscard]] virtual double getMeanParticleMass() const = 0; + [[nodiscard]] virtual double getMeanAtomicNumber() const = 0; + [[nodiscard]] virtual double getElectronAbundance() const = 0; + + [[nodiscard]] virtual std::vector getMassFractionVector() const = 0; + [[nodiscard]] virtual std::vector getNumberFractionVector() const = 0; + [[nodiscard]] virtual std::vector getMolarAbundanceVector() const = 0; + + [[nodiscard]] virtual size_t getSpeciesIndex(const std::string& symbol) const = 0; + [[nodiscard]] virtual size_t getSpeciesIndex(const fourdst::atomic::Species& species) const = 0; + + [[nodiscard]] virtual fourdst::atomic::Species getSpeciesAtIndex(size_t index) const = 0; +}; \ No newline at end of file diff --git a/src/composition/lib/composition.cpp b/src/composition/lib/composition.cpp index 9574035..396f144 100644 --- a/src/composition/lib/composition.cpp +++ b/src/composition/lib/composition.cpp @@ -223,7 +223,12 @@ namespace fourdst::composition { setNumberFraction(symbols[i], fractions[i]); } } - finalize(); + if (const bool didFinalize = finalize(); !didFinalize) { + std::string msg = "Failed to finalize composition on construction. "; + msg += "Construction of a composition object requires that the sum of the fractions vector be 1.\n"; + LOG_CRITICAL(m_logger, "{}", msg); + throw exceptions::InvalidCompositionError(msg); + } } Composition::Composition(const Composition &composition) { @@ -327,7 +332,7 @@ namespace fourdst::composition { } bool Composition::isValidComposition(const std::vector& fractions) const { - double sum = std::accumulate(fractions.begin(), fractions.end(), 0.0); + const double sum = std::accumulate(fractions.begin(), fractions.end(), 0.0); if (sum < 0.999999 || sum > 1.000001) { LOG_ERROR(m_logger, "The sum of fractions must be equal to 1 (expected 1, got {}).", sum); return false; @@ -416,7 +421,7 @@ namespace fourdst::composition { ) { std::vector symbols; symbols.reserve(species.size()); - for(const auto& s : species) symbols.push_back(std::string(s.name())); + for(const auto& s : species) symbols.emplace_back(s.name()); return setMassFraction(symbols, mass_fractions); } @@ -551,7 +556,12 @@ namespace fourdst::composition { double massFraction = fraction * thisMassFrac + otherMassFrac * (1-fraction); mixedComposition.setMassFraction(symbol, massFraction); } - mixedComposition.finalize(); + if (const bool didFinalize = mixedComposition.finalize(); !didFinalize) { + std::string msg = "Failed to finalize mixed composition. "; + msg += "This likely indicates an issue with the input compositions not summing to 1.\n"; + LOG_CRITICAL(m_logger, "{}", msg); + throw exceptions::InvalidCompositionError(msg); + } return mixedComposition; } @@ -1016,6 +1026,15 @@ namespace fourdst::composition { return m_compositions.contains(symbol); } + bool Composition::hasSpecies(const fourdst::atomic::Species &species) const { + for (const auto &entry: m_compositions | std::views::values) { + if (entry.isotope() == species) { + return true; + } + } + return false; + } + bool Composition::contains( const atomic::Species &isotope ) const { diff --git a/tests/composition/compositionTest.cpp b/tests/composition/compositionTest.cpp index 19b2b0d..8e0deee 100644 --- a/tests/composition/compositionTest.cpp +++ b/tests/composition/compositionTest.cpp @@ -146,7 +146,7 @@ TEST_F(compositionTest, setGetComposition) { EXPECT_DOUBLE_EQ(comp.setMassFraction("H-1", 0.6), 0.5); EXPECT_DOUBLE_EQ(comp.setMassFraction("He-4", 0.4), 0.5); - EXPECT_NO_THROW(comp.finalize()); + EXPECT_NO_THROW(static_cast(comp.finalize())); EXPECT_DOUBLE_EQ(comp.getMassFraction("H-1"), 0.6); EXPECT_THROW(comp.setMassFraction("He-3", 0.3), fourdst::composition::exceptions::UnregisteredSymbolError); @@ -186,7 +186,7 @@ TEST_F(compositionTest, setGetNumberFraction) { EXPECT_DOUBLE_EQ(comp.setNumberFraction("H-1", 0.6), 0.5); EXPECT_DOUBLE_EQ(comp.setNumberFraction("He-4", 0.4), 0.5); - EXPECT_NO_THROW(comp.finalize()); + EXPECT_NO_THROW(static_cast(comp.finalize())); EXPECT_DOUBLE_EQ(comp.getNumberFraction("H-1"), 0.6); EXPECT_THROW(comp.setNumberFraction("He-3", 0.3), fourdst::composition::exceptions::UnregisteredSymbolError); @@ -211,7 +211,7 @@ TEST_F(compositionTest, subset) { comp.registerSymbol("He-4"); comp.setMassFraction("H-1", 0.6); comp.setMassFraction("He-4", 0.4); - EXPECT_NO_THROW(comp.finalize()); + EXPECT_NO_THROW(static_cast(comp.finalize())); std::vector symbols = {"H-1"}; fourdst::composition::Composition subsetComp = comp.subset(symbols, "norm"); @@ -282,7 +282,7 @@ TEST_F(compositionTest, getComposition) { comp.registerSymbol("He-4"); comp.setMassFraction("H-1", 0.6); comp.setMassFraction("He-4", 0.4); - EXPECT_NO_THROW(comp.finalize()); + EXPECT_NO_THROW(static_cast(comp.finalize())); const auto compositionEntry = comp.getComposition("H-1"); EXPECT_DOUBLE_EQ(compositionEntry.first.mass_fraction(), 0.6); @@ -309,7 +309,7 @@ TEST_F(compositionTest, setCompositionMode) { comp.registerSymbol("He-4"); comp.setMassFraction("H-1", 0.6); comp.setMassFraction("He-4", 0.4); - EXPECT_NO_THROW(comp.finalize()); + EXPECT_NO_THROW(static_cast(comp.finalize())); EXPECT_DOUBLE_EQ(comp.getMassFraction("H-1"), 0.6); EXPECT_DOUBLE_EQ(comp.getMassFraction("He-4"), 0.4); @@ -320,7 +320,7 @@ TEST_F(compositionTest, setCompositionMode) { EXPECT_NO_THROW(comp.setNumberFraction("He-4", 0.1)); EXPECT_THROW(comp.setCompositionMode(true), fourdst::composition::exceptions::CompositionNotFinalizedError); - EXPECT_NO_THROW(comp.finalize()); + EXPECT_NO_THROW(static_cast(comp.finalize())); EXPECT_NO_THROW(comp.setCompositionMode(true)); } @@ -340,7 +340,7 @@ TEST_F(compositionTest, hasSymbol) { comp.registerSymbol("He-4"); comp.setMassFraction("H-1", 0.6); comp.setMassFraction("He-4", 0.4); - EXPECT_NO_THROW(comp.finalize()); + EXPECT_NO_THROW(static_cast(comp.finalize())); EXPECT_TRUE(comp.hasSymbol("H-1")); EXPECT_TRUE(comp.hasSymbol("He-4")); @@ -366,14 +366,14 @@ TEST_F(compositionTest, mix) { comp1.registerSymbol("He-4"); comp1.setMassFraction("H-1", 0.6); comp1.setMassFraction("He-4", 0.4); - EXPECT_NO_THROW(comp1.finalize()); + EXPECT_NO_THROW(static_cast(comp1.finalize())); fourdst::composition::Composition comp2; comp2.registerSymbol("H-1"); comp2.registerSymbol("He-4"); comp2.setMassFraction("H-1", 0.4); comp2.setMassFraction("He-4", 0.6); - EXPECT_NO_THROW(comp2.finalize()); + EXPECT_NO_THROW(static_cast(comp2.finalize())); fourdst::composition::Composition mixedComp = comp1 + comp2; EXPECT_TRUE(mixedComp.finalize()); @@ -401,8 +401,9 @@ TEST_F(compositionTest, molarAbundance) { comp1.registerSymbol("He-4"); comp1.setMassFraction("H-1", 0.5); comp1.setMassFraction("He-4", 0.5); - comp1.finalize(); + const bool didFinalize = comp1.finalize(); + EXPECT_TRUE(didFinalize); EXPECT_DOUBLE_EQ(comp1.getMolarAbundance("H-1"), 0.5/fourdst::atomic::H_1.mass()); EXPECT_DOUBLE_EQ(comp1.getMolarAbundance("He-4"), 0.5/fourdst::atomic::He_4.mass()); } @@ -567,11 +568,14 @@ TEST_F(compositionTest, mixErrorCases) { fourdst::config::Config::getInstance().loadConfig(EXAMPLE_FILENAME); using fourdst::composition::Composition; - Composition a; a.registerSymbol("H-1"); a.registerSymbol("He-4"); a.setMassFraction("H-1", 0.6); a.setMassFraction("He-4", 0.4); a.finalize(); + Composition a; a.registerSymbol("H-1"); a.registerSymbol("He-4"); a.setMassFraction("H-1", 0.6); a.setMassFraction("He-4", 0.4); + bool didFinalizeA = a.finalize(); + EXPECT_TRUE(didFinalizeA); Composition b; b.registerSymbol("H-1"); b.registerSymbol("He-4"); b.setMassFraction("H-1", 0.5); b.setMassFraction("He-4", 0.5); // Not finalized second comp EXPECT_THROW(static_cast(a.mix(b, 0.5)), fourdst::composition::exceptions::CompositionNotFinalizedError); - b.finalize(); + bool didFinalizeB = b.finalize(); + EXPECT_TRUE(didFinalizeB); // Invalid fraction EXPECT_THROW(static_cast(a.mix(b, -0.1)), fourdst::composition::exceptions::InvalidCompositionError); EXPECT_THROW(static_cast(a.mix(b, 1.1)), fourdst::composition::exceptions::InvalidCompositionError); @@ -706,7 +710,8 @@ TEST_F(compositionTest, vectorsAndIndexingAndSpeciesAtIndex) { // Molar abundance vector X_i/A_i in mass mode; switch back to mass mode to verify comp.setCompositionMode(true); - comp.finalize(true); + bool didFinalize = comp.finalize(true); + EXPECT_TRUE(didFinalize); auto av = comp.getMolarAbundanceVector(); ASSERT_EQ(av.size(), 3u); EXPECT_NEAR(av[iH], 0.5/species.at("H-1").mass(), 1e-12); @@ -804,3 +809,36 @@ TEST_F(compositionTest, iterationBeginEndAndIndexOutOfRange) { EXPECT_THROW(static_cast(comp.getSpeciesAtIndex(100)), std::out_of_range); } +TEST_F(compositionTest, abstractBase) { + class UnrestrictedComposition : public fourdst::composition::Composition { + private: + fourdst::atomic::Species m_species; + const Composition& m_composition; + public: + UnrestrictedComposition(const Composition& base, const fourdst::atomic::Species& species): + Composition(base), + m_species(species), + m_composition(base) + {} + + double getMolarAbundance(const fourdst::atomic::Species &species) const override { + if (species == m_species) { + return 1.0; + } + return m_composition.getMolarAbundance(species); + } + }; + + fourdst::config::Config::getInstance().loadConfig(EXAMPLE_FILENAME); + fourdst::composition::Composition comp; + comp.registerSymbol("H-1"); comp.registerSymbol("He-4"); comp.registerSymbol("O-16"); + comp.setMassFraction("H-1", 0.5); comp.setMassFraction("He-4", 0.3); comp.setMassFraction("O-16", 0.2); + ASSERT_TRUE(comp.finalize()); + + const UnrestrictedComposition uComp(comp, fourdst::atomic::H_1); + + ASSERT_DOUBLE_EQ(uComp.getMolarAbundance(fourdst::atomic::H_1), 1.0); + ASSERT_DOUBLE_EQ(uComp.getMassFraction("He-4"), comp.getMassFraction("He-4")); + +} +