From 84947a2b120e0ff24ef82e81d04f24ec605c6610 Mon Sep 17 00:00:00 2001 From: Emily Boudreaux Date: Fri, 7 Nov 2025 15:49:25 -0500 Subject: [PATCH 1/2] feat(composition): changed how composition is conmstructed Composition objects now must be built from vectors of molar abundances. Things such as mass fraction and number fraction are computed on the fly (with some caching for performance). This will allow for many fewer issues when converting from solver space to composition space. BREAKING CHANGE: The entire old API has been broken. There is no longer any need to finalize. In fact the entire concept of finalization has been removed. Further, the entire CompositionEntry and GlobalComposition data structure has been removed. Any code written for the old version will no longer work and major reworking will be needed to use the new version. --- .../{composition => atomic}/atomicSpecies.h | 18 +- .../{composition => atomic}/elements.h | 0 .../fourdst/{composition => atomic}/species.h | 4 +- .../include/fourdst/composition/composition.h | 492 ++------ .../composition/composition_abstract.h | 37 +- .../exceptions/exceptions_composition.h | 24 +- .../include/fourdst/composition/utils.h | 23 + src/composition/lib/composition.cpp | 1031 +++++------------ src/composition/lib/utils.cpp | 58 + src/composition/meson.build | 21 +- tests/composition/compositionTest.cpp | 777 +------------ 11 files changed, 519 insertions(+), 1966 deletions(-) rename src/composition/include/fourdst/{composition => atomic}/atomicSpecies.h (96%) rename src/composition/include/fourdst/{composition => atomic}/elements.h (100%) rename src/composition/include/fourdst/{composition => atomic}/species.h (99%) create mode 100644 src/composition/include/fourdst/composition/utils.h create mode 100644 src/composition/lib/utils.cpp diff --git a/src/composition/include/fourdst/composition/atomicSpecies.h b/src/composition/include/fourdst/atomic/atomicSpecies.h similarity index 96% rename from src/composition/include/fourdst/composition/atomicSpecies.h rename to src/composition/include/fourdst/atomic/atomicSpecies.h index 958dc46..f5a389a 100644 --- a/src/composition/include/fourdst/composition/atomicSpecies.h +++ b/src/composition/include/fourdst/atomic/atomicSpecies.h @@ -3,7 +3,7 @@ #include #include -#include +#include #include @@ -17,7 +17,7 @@ namespace fourdst::atomic { * @param jpi_string The spin-parity string to convert (e.g., "1/2+", "5/2-", "0+"). * @return The spin value as a double. Returns `NaN` for invalid or unparsable strings. */ - inline double convert_jpi_to_double(const std::string& jpi_string); + inline double convert_jpi_to_double(const std::string& jpi_string) noexcept; /** * @struct Species @@ -63,7 +63,7 @@ namespace fourdst::atomic { std::string m_decayModes; ///< Decay modes as a string. double m_atomicMass; ///< Atomic mass in atomic mass units (u). double m_atomicMassUnc; ///< Uncertainty in the atomic mass. - double m_spin = 0.0; ///< Nuclear spin as a double, derived from m_spinParity. + mutable std::optional m_spin = std::nullopt; ///< Nuclear spin as a double, derived from m_spinParity. /** * @brief Constructs a Species object with detailed properties. @@ -114,9 +114,7 @@ namespace fourdst::atomic { m_spinParity(spinParity), m_decayModes(decayModes), m_atomicMass(atomicMass), - m_atomicMassUnc(atomicMassUnc) { - m_spin = convert_jpi_to_double(m_spinParity); - }; + m_atomicMassUnc(atomicMassUnc) {}; /** * @brief Copy constructor for Species. @@ -138,7 +136,6 @@ namespace fourdst::atomic { m_decayModes = species.m_decayModes; m_atomicMass = species.m_atomicMass; m_atomicMassUnc = species.m_atomicMassUnc; - m_spin = convert_jpi_to_double(m_spinParity); } @@ -259,7 +256,10 @@ namespace fourdst::atomic { * @return The spin as a double. */ [[nodiscard]] double spin() const { - return m_spin; + if (!m_spin.has_value()) { // The spin calculation is very expensive, and we almost never need it so we only compute it the first time it is requested + m_spin = convert_jpi_to_double(m_spinParity); + } + return m_spin.value(); } /** @@ -345,7 +345,7 @@ namespace fourdst::atomic { * * @return The spin value as a `double`. Returns `NaN` for invalid or unparsable strings. */ - inline double convert_jpi_to_double(const std::string& jpi_string) { + inline double convert_jpi_to_double(const std::string& jpi_string) noexcept { std::string s = jpi_string; if (s.empty()) { diff --git a/src/composition/include/fourdst/composition/elements.h b/src/composition/include/fourdst/atomic/elements.h similarity index 100% rename from src/composition/include/fourdst/composition/elements.h rename to src/composition/include/fourdst/atomic/elements.h diff --git a/src/composition/include/fourdst/composition/species.h b/src/composition/include/fourdst/atomic/species.h similarity index 99% rename from src/composition/include/fourdst/composition/species.h rename to src/composition/include/fourdst/atomic/species.h index 240b4b2..18c0174 100644 --- a/src/composition/include/fourdst/composition/species.h +++ b/src/composition/include/fourdst/atomic/species.h @@ -3,8 +3,8 @@ #include #include #include // Required for std::numeric_limits -#include "fourdst/composition/atomicSpecies.h" -#include "fourdst/composition/elements.h" +#include "fourdst/atomic/atomicSpecies.h" +#include "elements.h" #include // For std::expected diff --git a/src/composition/include/fourdst/composition/composition.h b/src/composition/include/fourdst/composition/composition.h index 11905a6..7c1c033 100644 --- a/src/composition/include/fourdst/composition/composition.h +++ b/src/composition/include/fourdst/composition/composition.h @@ -24,13 +24,12 @@ #include #include -#include #include #include "fourdst/config/config.h" #include "fourdst/logging/logging.h" #include "fourdst/composition/composition_abstract.h" -#include "fourdst/composition/atomicSpecies.h" +#include "fourdst/atomic/atomicSpecies.h" namespace fourdst::composition { /** @@ -62,158 +61,6 @@ namespace fourdst::composition { } }; - /** - * @brief Represents global properties of a finalized composition. - * @details This struct holds derived quantities that describe the entire composition, - * such as mean particle mass. It is typically returned by `Composition` methods - * after the composition has been finalized and is intended for internal or advanced use. - */ - struct GlobalComposition { - double specificNumberDensity; ///< The specific number density (moles per unit mass, sum of X_i/M_i), where X_i is mass fraction and M_i is molar mass. Units: mol/g. - double meanParticleMass; ///< The mean mass per particle (inverse of specific number density). Units: g/mol. - - // Overload the output stream operator for GlobalComposition - friend std::ostream& operator<<(std::ostream& os, const GlobalComposition& comp); - }; - - /** - * @brief Represents a single entry (an isotope) within a composition. - * @details This struct holds the properties of one component, including its symbol, - * the corresponding `atomic::Species` object, and its abundance (either as a mass - * fraction or number fraction). It manages the state and conversions for that single entry. - */ - struct CompositionEntry { - std::optional m_symbol = std::nullopt; ///< The chemical symbol of the species (e.g., "H-1", "Fe-56"). - std::optional m_isotope = std::nullopt; ///< The `atomic::Species` object containing detailed isotope data. - bool m_massFracMode = true; ///< The mode of the composition entry. True if mass fraction, false if number fraction. - - double m_massFraction = 0.0; ///< The mass fraction of the species. Valid only if `m_massFracMode` is true. - double m_numberFraction = 0.0; ///< The number fraction (mole fraction) of the species. Valid only if `m_massFracMode` is false. - double m_relAbundance = 0.0; ///< The relative abundance, used internally for conversions. For mass fraction mode, this is X_i / A_i; for number fraction mode, it's n_i * A_i. - double m_molesPerMass = 0.0; - double m_cachedNumberFraction = 0.0; ///< Cached number fraction for conversions when in mass fraction mode. - - - bool m_initialized = false; ///< True if the composition entry has been initialized with a valid species. - - /** - * @brief Default constructor. Initializes a default entry (H-1), but in an uninitialized state. - */ - CompositionEntry(); - - /** - * @brief Constructs a CompositionEntry for a given symbol and abundance mode. - * @param symbol The chemical symbol of the species (e.g., "He-4"). - * @param massFracMode True to operate in mass fraction mode, false for number fraction mode. - * @throws exceptions::InvalidSpeciesSymbolError if the symbol does not exist in the atomic species database. - * @throws exceptions::EntryAlreadyInitializedError if setSpecies is called on an already initialized entry. - * @par Usage Example: - * @code - * CompositionEntry entry("H-1", true); // Entry for H-1 in mass fraction mode. - * @endcode - */ - explicit CompositionEntry(const std::string& symbol, bool massFracMode=true); - - /** - * @brief Copy constructor. - * @param entry The CompositionEntry to copy. - */ - CompositionEntry(const CompositionEntry& entry); - - /** - * @brief Sets the species for the composition entry. This can only be done once. - * @param symbol The chemical symbol of the species. - * @throws exceptions::EntryAlreadyInitializedError if the entry has already been initialized. - * @throws exceptions::InvalidSpeciesSymbolError if the symbol is not found in the atomic species database. - */ - void setSpecies(const std::string& symbol); - - /** - * @brief Gets the chemical symbol of the species. - * @return The chemical symbol. - */ - [[nodiscard]] std::string symbol() const; - - /** - * @brief Gets the mass fraction of the species. - * @pre The entry must be in mass fraction mode. - * @return The mass fraction of the species. - * @throws exceptions::CompositionModeError if the entry is in number fraction mode. - */ - [[nodiscard]] double mass_fraction() const; - - /** - * @brief Gets the number fraction of the species. - * @pre The entry must be in number fraction mode. - * @return The number fraction of the species. - * @throws exceptions::CompositionModeError if the entry is in mass fraction mode. - */ - [[nodiscard]] double number_fraction() const; - - /** - * @brief Gets the number fraction, converting from mass fraction if necessary. - * @param totalMolesPerMass The total moles per unit mass (specific number density) of the entire composition. - * @return The number fraction of the species. - */ - [[nodiscard]] double number_fraction(double totalMolesPerMass) const; - - /** - * @brief Gets the relative abundance of the species. - * @return The relative abundance. - */ - [[nodiscard]] double rel_abundance() const; - - /** - * @brief Gets the isotope data for the species. - * @return A const reference to the `atomic::Species` object. - */ - [[nodiscard]] atomic::Species isotope() const; - - /** - * @brief Gets the mode of the composition entry. - * @return True if in mass fraction mode, false if in number fraction mode. - */ - [[nodiscard]] bool getMassFracMode() const; - - /** - * @brief Sets the mass fraction of the species. - * @param mass_fraction The mass fraction to set. Must be in [0, 1]. - * @pre The entry must be in mass fraction mode. - * @throws exceptions::CompositionModeError if the entry is in number fraction mode. - */ - void setMassFraction(double mass_fraction); - - /** - * @brief Sets the number fraction of the species. - * @param number_fraction The number fraction to set. Must be in [0, 1]. - * @pre The entry must be in number fraction mode. - * @throws exceptions::CompositionModeError if the entry is in mass fraction mode. - */ - void setNumberFraction(double number_fraction); - - /** - * @brief Switches the mode to mass fraction mode. - * @param meanMolarMass The mean molar mass of the composition, required for conversion. - * @return True if the mode was successfully set, false otherwise. - */ - bool setMassFracMode(double meanMolarMass); - - /** - * @brief Switches the mode to number fraction mode. - * @param totalMolesPerMass The total moles per unit mass (specific number density) of the composition. - * @return True if the mode was successfully set, false otherwise. - */ - bool setNumberFracMode(double totalMolesPerMass); - - /** - * @brief Overloaded output stream operator for CompositionEntry. - * @param os The output stream. - * @param entry The CompositionEntry to output. - * @return The output stream. - */ - friend std::ostream& operator<<(std::ostream& os, const CompositionEntry& entry); - }; - /** * @class Composition * @brief Manages a collection of chemical species and their abundances. @@ -256,7 +103,6 @@ namespace fourdst::composition { class Composition : public CompositionAbstract { private: struct CompositionCache { - std::optional globalComp; ///< Cached global composition data. std::optional canonicalComp; ///< Cached canonical composition data. std::optional> massFractions; ///< Cached vector of mass fractions. std::optional> numberFractions; ///< Cached vector of number fractions. @@ -266,7 +112,6 @@ namespace fourdst::composition { std::optional Ye; ///< Cached electron abundance. void clear() { - globalComp = std::nullopt; canonicalComp = std::nullopt; massFractions = std::nullopt; numberFractions = std::nullopt; @@ -277,7 +122,7 @@ namespace fourdst::composition { } [[nodiscard]] bool is_clear() const { - return !globalComp.has_value() && !canonicalComp.has_value() && !massFractions.has_value() && + return !canonicalComp.has_value() && !massFractions.has_value() && !numberFractions.has_value() && !molarAbundances.has_value() && !sortedSymbols.has_value() && !Ye.has_value() && !sortedSpecies.has_value(); } @@ -289,52 +134,11 @@ namespace fourdst::composition { return logger; } - bool m_finalized = false; ///< True if the composition is finalized. - double m_specificNumberDensity = 0.0; ///< The specific number density of the composition (\sum_{i} X_i m_i. Where X_i is the number fraction of the ith species and m_i is the mass of the ith species). - double m_meanParticleMass = 0.0; ///< The mean particle mass of the composition (\sum_{i} \frac{n_i}{m_i}. where n_i is the number fraction of the ith species and m_i is the mass of the ith species). - bool m_massFracMode = true; ///< True if mass fraction mode, false if number fraction mode. - - std::set m_registeredSymbols; ///< The registered symbols. - std::unordered_map m_compositions; ///< The compositions. + std::set m_registeredSpecies; + std::map m_molarAbundances; mutable CompositionCache m_cache; ///< Cache for computed properties to avoid redundant calculations. - - /** - * @brief Checks if the given symbol is valid by checking against the global species database. - * @param symbol The symbol to check. - * @return True if the symbol is valid, false otherwise. - */ - static bool isValidSymbol(const std::string& symbol); - - /** - * @brief Checks if the given fractions are valid (sum to ~1.0). - * @param fractions The fractions to check. - * @return True if the fractions are valid, false otherwise. - */ - [[nodiscard]] static bool isValidComposition(const std::vector& fractions) ; - - /** - * @brief Validates the given fractions, throwing an exception on failure. - * @param fractions The fractions to validate. - * @throws exceptions::InvalidCompositionError if the fractions are invalid. - */ - static void validateComposition(const std::vector& fractions) ; - - /** - * @brief Finalizes the composition in mass fraction mode. - * @param norm If true, the composition will be normalized to sum to 1. - * @return True if the composition is successfully finalized, false otherwise. - */ - bool finalizeMassFracMode(bool norm); - - /** - * @brief Finalizes the composition in number fraction mode. - * @param norm If true, the composition will be normalized to sum to 1. - * @return True if the composition is successfully finalized, false otherwise. - */ - bool finalizeNumberFracMode(bool norm); - public: /** * @brief Default constructor. @@ -346,17 +150,6 @@ namespace fourdst::composition { */ ~Composition() override = default; - /** - * @brief Finalizes the composition, making it ready for querying. - * @details This method checks if the sum of all fractions (mass or number) is approximately 1.0. - * It also computes global properties like mean particle mass. This **must** be called before - * any `get...` method can be used. - * @param norm If true, the composition will be normalized to sum to 1 before validation. [Default: false] - * @return True if the composition is valid and successfully finalized, false otherwise. - * @post If successful, `m_finalized` is true and global properties are computed. - */ - [[nodiscard]] bool finalize(bool norm=false); - /** * @brief Constructs a Composition and registers the given symbols. * @param symbols The symbols to register. The composition will be in mass fraction mode by default. @@ -372,6 +165,8 @@ namespace fourdst::composition { */ explicit Composition(const std::vector& symbols); + explicit Composition(const std::vector& species); + /** * @brief Constructs a Composition and registers the given symbols from a set. * @param symbols The symbols to register. The composition will be in mass fraction mode by default. @@ -384,13 +179,14 @@ namespace fourdst::composition { */ explicit Composition(const std::set& symbols); + explicit Composition(const std::set& species); + /** * @brief Constructs and finalizes a Composition with the given symbols and fractions. * @details This constructor provides a convenient way to create a fully-formed, finalized composition in one step. * The provided fractions must be valid and sum to 1.0. * @param symbols The symbols to initialize the composition with. - * @param fractions The fractions (mass or number) corresponding to the symbols. - * @param massFracMode True if `fractions` are mass fractions, false if they are number fractions. [Default: true] + * @param molarAbundances The corresponding molar abundances for each symbol. * @throws exceptions::InvalidCompositionError if the number of symbols and fractions do not match, or if the fractions do not sum to ~1.0. * @throws exceptions::InvalidSymbolError if any symbol is invalid. * @post The composition is immediately finalized. @@ -401,7 +197,11 @@ namespace fourdst::composition { * Composition comp(symbols, mass_fractions); // Finalized on construction * @endcode */ - Composition(const std::vector& symbols, const std::vector& fractions, bool massFracMode=true); + Composition(const std::vector& symbols, const std::vector& molarAbundances); + + Composition(const std::vector& species, const std::vector& molarAbundances); + + Composition(const std::set& symbols, const std::vector& molarAbundances); /** * @brief Constructs a Composition from another Composition. @@ -420,7 +220,6 @@ namespace fourdst::composition { * @brief Registers a new symbol for inclusion in the composition. * @details A symbol must be registered before its abundance can be set. The first registration sets the mode (mass/number fraction) for the entire composition. * @param symbol The symbol to register (e.g., "Fe-56"). - * @param massFracMode True for mass fraction mode, false for number fraction mode. This is only effective for the first symbol registered. * @throws exceptions::InvalidSymbolError if the symbol is not in the atomic species database. * @throws exceptions::CompositionModeError if attempting to register with a mode that conflicts with the existing mode. * @par Usage Example: @@ -430,12 +229,11 @@ namespace fourdst::composition { * comp.registerSymbol("He-4"); // Must also be mass fraction mode * @endcode */ - void registerSymbol(const std::string& symbol, bool massFracMode=true); + void registerSymbol(const std::string& symbol); /** * @brief Registers multiple new symbols. * @param symbols The symbols to register. - * @param massFracMode True for mass fraction mode, false for number fraction mode. * @throws exceptions::InvalidSymbolError if any symbol is invalid. * @throws exceptions::CompositionModeError if the mode conflicts with an already set mode. * @par Usage Example: @@ -445,12 +243,11 @@ namespace fourdst::composition { * comp.registerSymbol(symbols); * @endcode */ - void registerSymbol(const std::vector& symbols, bool massFracMode=true); + void registerSymbol(const std::vector& symbols); /** * @brief Registers a new species by extracting its symbol. * @param species The species to register. - * @param massFracMode True for mass fraction mode, false for number fraction mode. * @throws exceptions::InvalidSymbolError if the species' symbol is invalid. * @throws exceptions::CompositionModeError if the mode conflicts. * @par Usage Example: @@ -460,13 +257,12 @@ namespace fourdst::composition { * comp.registerSpecies(fourdst::atomic::species.at("H-1")); * @endcode */ - void registerSpecies(const fourdst::atomic::Species& species, bool massFracMode=true); + void registerSpecies(const atomic::Species& species); /** * @brief Registers a vector of new species. * @param species The vector of species to register. - * @param massFracMode True for mass fraction mode, false for number fraction mode. * @throws exceptions::InvalidSymbolError if any species' symbol is invalid. * @throws exceptions::CompositionModeError if the mode conflicts. * @par Usage Example: @@ -477,8 +273,50 @@ namespace fourdst::composition { * comp.registerSpecies(my_species, false); // Number fraction mode * @endcode */ - void registerSpecies(const std::vector& species, bool massFracMode=true); + void registerSpecies(const std::vector& species); + /** + * @brief Checks if a given isotope is present in the composition. + * @pre The composition must be finalized. + * @param species The isotope to check for. + * @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& species) const override; + + [[nodiscard]] bool contains(const std::string& symbol) const override; + + [[nodiscard]] size_t size() const override; + + void setMolarAbundance( + const std::string& symbol, + const double& molar_abundance + ); + + void setMolarAbundance( + const atomic::Species& species, + const double& molar_abundance + ); + + void setMolarAbundance( + const std::vector& symbols, + const std::vector& molar_abundances + ); + + void setMolarAbundance( + const std::vector& species, + const std::vector& molar_abundances + ); + + void setMolarAbundance( + const std::set& symbols, + const std::vector& molar_abundances + ); + + void setMolarAbundance( + const std::set& species, + const std::vector& molar_abundances + ); /** * @brief Gets the registered symbols. @@ -490,112 +328,7 @@ namespace fourdst::composition { * @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 override; - - /** - * @brief Sets the mass fraction for a given symbol. - * @param symbol The symbol to set the mass fraction for. - * @param mass_fraction The mass fraction to set (must be in [0, 1]). - * @return The previous mass fraction that was set for the symbol. - * @throws exceptions::UnregisteredSymbolError if the symbol is not registered. - * @throws exceptions::CompositionModeError if the composition is in number fraction mode. - * @throws exceptions::InvalidCompositionError if the mass fraction is not between 0 and 1. - * @post The composition is marked as not finalized. - * @par Usage Example: - * @code - * Composition comp; - * comp.registerSymbol("H-1"); - * comp.setMassFraction("H-1", 0.7); - * @endcode - */ - double setMassFraction(const std::string& symbol, const double& mass_fraction); - - /** - * @brief Sets the mass fraction for multiple symbols. - * @param symbols The symbols to set the mass fractions for. - * @param mass_fractions The mass fractions corresponding to the symbols. - * @return A vector of the previous mass fractions that were set. - * @throws exceptions::InvalidCompositionError if symbol and fraction counts differ. - * @throws See `setMassFraction(const std::string&, const double&)` for other exceptions. - * @post The composition is marked as not finalized. - */ - std::vector setMassFraction(const std::vector& symbols, const std::vector& mass_fractions); - - /** - * @brief Sets the mass fraction for a given species. - * @param species The species to set the mass fraction for. - * @param mass_fraction The mass fraction to set. - * @return The previous mass fraction that was set for the species. - * @throws exceptions::UnregisteredSymbolError if the species is not registered. - * @throws exceptions::CompositionModeError if the composition is in number fraction mode. - * @throws exceptions::InvalidCompositionError if the mass fraction is not between 0 and 1. - */ - double setMassFraction(const fourdst::atomic::Species& species, const double& mass_fraction); - - /** - * @brief Sets the mass fraction for multiple species. - * @param species The vector of species to set the mass fractions for. - * @param mass_fractions The vector of mass fractions corresponding to the species. - * @return A vector of the previous mass fractions that were set. - * @throws See `setMassFraction(const std::vector&, const std::vector&)` for exceptions. - */ - std::vector setMassFraction(const std::vector& species, const std::vector& mass_fractions); - - /** - * @brief Sets the number fraction for a given symbol. - * @param symbol The symbol to set the number fraction for. - * @param number_fraction The number fraction to set (must be in [0, 1]). - * @return The previous number fraction that was set. - * @throws exceptions::UnregisteredSymbolError if the symbol is not registered. - * @throws exceptions::CompositionModeError if the composition is in mass fraction mode. - * @throws exceptions::InvalidCompositionError if the number fraction is not between 0 and 1. - * @post The composition is marked as not finalized. - */ - double setNumberFraction(const std::string& symbol, const double& number_fraction); - - /** - * @brief Sets the number fraction for multiple symbols. - * @param symbols The symbols to set the number fractions for. - * @param number_fractions The number fractions corresponding to the symbols. - * @return A vector of the previous number fractions that were set. - * @throws exceptions::InvalidCompositionError if symbol and fraction counts differ. - * @throws See `setNumberFraction(const std::string&, const double&)` for other exceptions. - */ - std::vector setNumberFraction(const std::vector& symbols, const std::vector& number_fractions); - - /** - * @brief Sets the number fraction for a given species. - * @param species The species to set the number fraction for. - * @param number_fraction The number fraction to set for the species. - * @return The previous number fraction that was set for the species. - * @throws exceptions::UnregisteredSymbolError if the species is not registered. - * @throws exceptions::CompositionModeError if the composition is in mass fraction mode. - * @throws exceptions::InvalidCompositionError if the number fraction is not between 0 and 1. - */ - double setNumberFraction(const fourdst::atomic::Species& species, const double& number_fraction); - - /** - * @brief Sets the number fraction for multiple species. - * @param species The vector of species to set the number fractions for. - * @param number_fractions The vector of number fractions corresponding to the species. - * @return The vector of the previous number fractions that were set. - * @throws See `setNumberFraction(const std::vector&, const std::vector&)` for exceptions. - */ - std::vector setNumberFraction(const std::vector& species, const std::vector& number_fractions); - - /** - * @brief Mixes this composition with another to produce a new composition. - * @details The mixing is performed linearly on the mass fractions. The formula for each species is: - * `new_X_i = fraction * this_X_i + (1 - fraction) * other_X_i`. - * The resulting composition is automatically finalized. - * @param other The other composition to mix with. - * @param fraction The mixing fraction. A value of 1.0 means the new composition is 100% `this`, 0.0 means 100% `other`. - * @return A new, finalized `Composition` object representing the mixture. - * @pre Both `this` and `other` compositions must be finalized. - * @throws exceptions::CompositionNotFinalizedError if either composition is not finalized. - * @throws exceptions::InvalidCompositionError if the fraction is not between 0 and 1. - */ - [[nodiscard]] Composition mix(const Composition& other, double fraction) const; + [[nodiscard]] const std::set &getRegisteredSpecies() const override; /** * @brief Gets the mass fractions of all species in the composition. @@ -603,7 +336,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 override; + [[nodiscard]] std::unordered_map getMassFraction() const override; /** * @brief Gets the mass fraction for a given symbol. @@ -651,7 +384,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 override; + [[nodiscard]] std::unordered_map getNumberFraction() const override; /** * @brief Gets the molar abundance (X_i / A_i) for a given symbol. @@ -671,35 +404,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 override; - - /** - * @brief Gets the composition entry and global composition data for a given symbol. - * @pre The composition must be finalized. - * @param symbol The symbol to get the composition for. - * @return A pair containing the CompositionEntry and GlobalComposition for the given symbol. - * @throws exceptions::CompositionNotFinalizedError if the composition is not finalized. - * @throws exceptions::UnregisteredSymbolError if the symbol is not in the composition. - */ - [[nodiscard]] std::pair getComposition(const std::string& symbol) const; - - /** - * @brief Gets the composition entry and global composition data for a given species. - * @pre The composition must be finalized. - * @param species The species to get the composition for. - * @return A pair containing the CompositionEntry and GlobalComposition for the given species. - * @throws exceptions::CompositionNotFinalizedError if the composition is not finalized. - * @throws exceptions::UnregisteredSymbolError if the species is not in the composition. - */ - [[nodiscard]] std::pair getComposition(const fourdst::atomic::Species& species) const; - - /** - * @brief Gets all composition entries and the global composition data. - * @pre The composition must be finalized. - * @return A pair containing an unordered map of all CompositionEntries and the GlobalComposition. - * @throws exceptions::CompositionNotFinalizedError if the composition is not finalized. - */ - [[nodiscard]] std::pair, GlobalComposition> getComposition() const; + [[nodiscard]] double getMolarAbundance(const atomic::Species& species) const override; /** * @brief Compute the mean particle mass of the composition. @@ -709,14 +414,6 @@ namespace fourdst::composition { */ [[nodiscard]] double getMeanParticleMass() const override; - /** - * @brief Compute the mean atomic number of the composition. - * @pre The composition must be finalized. - * @return Mean atomic number . - * @throws exceptions::CompositionNotFinalizedError if the composition is not finalized. - */ - [[nodiscard]] double getMeanAtomicNumber() const override; - /** * @brief Compute the electron abundance of the composition. * @details Ye is defined as the sum over all species of (Z_i * X_i / A_i), where Z_i is the atomic number, X_i is the mass fraction, and A_i is the atomic mass of species i. @@ -725,49 +422,6 @@ namespace fourdst::composition { */ [[nodiscard]] double getElectronAbundance() const override; - /** - * @brief Creates a new Composition object containing a subset of species from this one. - * @param symbols The symbols to include in the subset. - * @param method The method for handling the abundances of the new subset. Can be "norm" (normalize abundances to sum to 1) or "none" (keep original abundances). - * @return A new `Composition` object containing the subset. - * @throws exceptions::UnregisteredSymbolError if any requested symbol is not in the original composition. - * @throws exceptions::InvalidMixingMode if an invalid method is provided. - * @throws exceptions::FailedToFinalizeCompositionError if normalization fails. - */ - [[nodiscard]] Composition subset(const std::vector& symbols, const std::string& method="norm") const; - - /** - * @brief Checks if a symbol is registered in the composition. - * @param symbol The symbol to check. - * @return True if the symbol is registered, false otherwise. - */ - [[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. - * @pre The composition must be finalized. - * @param isotope The isotope to check for. - * @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 override; - - /** - * @brief Sets the composition mode (mass fraction vs. number fraction). - * @details This function converts all entries in the composition to the specified mode. - * @pre The composition must be finalized before the mode can be switched. - * @param massFracMode True to switch to mass fraction mode, false for number fraction mode. - * @throws exceptions::CompositionNotFinalizedError if the composition is not finalized. - * @throws std::runtime_error if the conversion fails for an unknown reason. - */ - void setCompositionMode(bool massFracMode); /** * @brief Gets the current canonical composition (X, Y, Z). @@ -850,22 +504,12 @@ namespace fourdst::composition { */ friend std::ostream& operator<<(std::ostream& os, const Composition& composition); - /** - * @brief Overloads the + operator to mix two compositions with a 50/50 fraction. - * @details This is a convenience operator that calls `mix(other, 0.5)`. - * @param other The other composition to mix with. - * @return The new, mixed composition. - * @pre Both compositions must be finalized. - * @throws See `mix()` for exceptions. - */ - Composition operator+(const Composition& other) const; - /** * @brief Returns an iterator to the beginning of the composition map. * @return An iterator to the beginning. */ auto begin() { - return m_compositions.begin(); + return m_molarAbundances.begin(); } /** @@ -873,7 +517,7 @@ namespace fourdst::composition { * @return A const iterator to the beginning. */ [[nodiscard]] auto begin() const { - return m_compositions.cbegin(); + return m_molarAbundances.cbegin(); } /** @@ -881,7 +525,7 @@ namespace fourdst::composition { * @return An iterator to the end. */ auto end() { - return m_compositions.end(); + return m_molarAbundances.end(); } /** @@ -889,7 +533,7 @@ namespace fourdst::composition { * @return A const iterator to the end. */ [[nodiscard]] auto end() const { - return m_compositions.cend(); + return m_molarAbundances.cend(); } }; diff --git a/src/composition/include/fourdst/composition/composition_abstract.h b/src/composition/include/fourdst/composition/composition_abstract.h index 2d87c12..2c1b7cc 100644 --- a/src/composition/include/fourdst/composition/composition_abstract.h +++ b/src/composition/include/fourdst/composition/composition_abstract.h @@ -1,6 +1,6 @@ #pragma once -#include "fourdst/composition/atomicSpecies.h" +#include "fourdst/atomic/atomicSpecies.h" #include #include @@ -36,20 +36,6 @@ public: */ virtual ~CompositionAbstract() = default; - /** - * @brief Check if a chemical symbol is registered in the composition. - * @param symbol The chemical symbol to check (e.g., "H", "He"). - * @return True if the symbol is present, false otherwise. - */ - [[nodiscard]] virtual bool hasSymbol(const std::string& symbol) const = 0; - - /** - * @brief Check if a species is registered in the composition. - * @param species The atomic species to check. - * @return True if the species is present, false otherwise. - */ - [[nodiscard]] virtual bool hasSpecies(const fourdst::atomic::Species& species) const = 0; - /** * @brief Check if the composition contains the given species. * @param species The atomic species to check. @@ -57,6 +43,15 @@ public: */ [[nodiscard]] virtual bool contains(const fourdst::atomic::Species& species) const = 0; + /** + * @brief Check if the composition contains the given species. + * @param symbol The symbol of the atomic species to check. + * @return True if the species is contained, false otherwise. + */ + [[nodiscard]] virtual bool contains(const std::string& symbol) const = 0; + + [[nodiscard]] virtual size_t size() const = 0; + /** * @brief Get all registered chemical symbols in the composition. * @return A set of registered chemical symbols. @@ -67,19 +62,19 @@ public: * @brief Get all registered atomic species in the composition. * @return A set of registered atomic species. */ - [[nodiscard]] virtual std::set getRegisteredSpecies() const = 0; + [[nodiscard]] virtual const std::set &getRegisteredSpecies() const = 0; /** * @brief Get the mass fraction for all registered symbols. * @return An unordered map from symbol to mass fraction. */ - [[nodiscard]] virtual std::unordered_map getMassFraction() const = 0; + [[nodiscard]] virtual std::unordered_map getMassFraction() const = 0; /** * @brief Get the number fraction for all registered symbols. * @return An unordered map from symbol to number fraction. */ - [[nodiscard]] virtual std::unordered_map getNumberFraction() const = 0; + [[nodiscard]] virtual std::unordered_map getNumberFraction() const = 0; /** * @brief Get the mass fraction for a given symbol. @@ -129,12 +124,6 @@ public: */ [[nodiscard]] virtual double getMeanParticleMass() const = 0; - /** - * @brief Get the mean atomic number of the composition. - * @return The mean atomic number. - */ - [[nodiscard]] virtual double getMeanAtomicNumber() const = 0; - /** * @brief Get the electron abundance of the composition. * @return The electron abundance. diff --git a/src/composition/include/fourdst/composition/exceptions/exceptions_composition.h b/src/composition/include/fourdst/composition/exceptions/exceptions_composition.h index 019adde..c10754e 100644 --- a/src/composition/include/fourdst/composition/exceptions/exceptions_composition.h +++ b/src/composition/include/fourdst/composition/exceptions/exceptions_composition.h @@ -94,14 +94,6 @@ namespace fourdst::composition::exceptions { using CompositionError::CompositionError; }; - /** - * @class InvalidSymbolError - * @brief Exception thrown when a symbol used in a composition is invalid. - */ - class InvalidSymbolError final : public CompositionError { - using CompositionError::CompositionError; - }; - /** * @class UnregisteredSymbolError * @brief Exception thrown when a symbol is used that has not been registered. @@ -146,4 +138,20 @@ namespace fourdst::composition::exceptions { using CompositionEntryError::CompositionEntryError; }; + class SpeciesError : public std::exception { + protected: + std::string m_message; + public: + explicit SpeciesError(const std::string& message) + : m_message(std::move(message)) {} + + const char* what() const noexcept override { + return m_message.c_str(); + } + }; + + class UnknownSymbolError final : public SpeciesError { + using SpeciesError::SpeciesError; + }; + } \ No newline at end of file diff --git a/src/composition/include/fourdst/composition/utils.h b/src/composition/include/fourdst/composition/utils.h new file mode 100644 index 0000000..81969bc --- /dev/null +++ b/src/composition/include/fourdst/composition/utils.h @@ -0,0 +1,23 @@ +#pragma once + +#include "fourdst/composition/composition.h" +#include "fourdst/atomic/atomicSpecies.h" + +#include + +namespace fourdst::composition { + Composition buildCompositionFromMassFractions( + const std::vector& symbols, + const std::vector& massFractions + ); + + Composition buildCompositionFromMassFractions( + const std::vector& species, + const std::vector& massFractions + ); + + Composition buildCompositionFromMassFractions( + const std::set& species, + const std::vector& massFractions + ); +} \ No newline at end of file diff --git a/src/composition/lib/composition.cpp b/src/composition/lib/composition.cpp index 2a0a960..e3b4857 100644 --- a/src/composition/lib/composition.cpp +++ b/src/composition/lib/composition.cpp @@ -23,19 +23,18 @@ #include #include #include -#include #include #include +#include +#include #include -#include "fourdst/composition/atomicSpecies.h" -#include "fourdst/composition/species.h" +#include "fourdst/atomic/atomicSpecies.h" +#include "fourdst/atomic/species.h" #include "fourdst/composition/composition.h" -#include - #include "fourdst/composition/exceptions/exceptions_composition.h" namespace { @@ -62,127 +61,28 @@ namespace { return sorted; } + + std::optional getSpecies(const std::string& symbol) { + if (!fourdst::atomic::species.contains(symbol)) { + return std::nullopt; + } + return fourdst::atomic::species.at(symbol); + } + + void throw_unknown_symbol(quill::Logger* logger, const std::string& symbol) { + LOG_ERROR(logger, "Symbol {} is not a valid species symbol (not in the species database)", symbol); + throw fourdst::composition::exceptions::UnknownSymbolError("Symbol " + symbol + " is not a valid species symbol (not in the species database)"); + } + + void throw_unregistered_symbol(quill::Logger* logger, const std::string& symbol) { + LOG_ERROR(logger, "Symbol {} is not registered in the composition.", symbol); + throw fourdst::composition::exceptions::UnregisteredSymbolError("Symbol " + symbol + " is not registered in the composition."); + } } namespace fourdst::composition { - - CompositionEntry::CompositionEntry() = default; - - CompositionEntry::CompositionEntry( - const std::string& symbol, - const bool massFracMode - ) : - m_symbol(symbol), - m_isotope(atomic::species.at(symbol)), - m_massFracMode(massFracMode) { - setSpecies(symbol); - } - - CompositionEntry::CompositionEntry(const CompositionEntry& entry) = default; - - void CompositionEntry::setSpecies(const std::string& symbol) { - if (m_initialized) { - throw exceptions::EntryAlreadyInitializedError("Composition entry is already initialized."); - } - if (!fourdst::atomic::species.contains(symbol)) { - throw exceptions::InvalidSpeciesSymbolError("Invalid symbol."); - } - m_symbol = symbol; - m_isotope = atomic::species.at(symbol); - m_initialized = true; - } - - std::string CompositionEntry::symbol() const { - return m_symbol.value(); - } - - double CompositionEntry::mass_fraction() const { - if (!m_massFracMode) { - throw exceptions::CompositionModeError("Composition entry is in number fraction mode."); - } - // X_i = (moles_i / mass_total) * (mass_i / moles_i) = m_molesPerMass * A_i - return m_molesPerMass * m_isotope->mass(); - } - - double CompositionEntry::number_fraction() const { - if (m_massFracMode) { - throw exceptions::CompositionModeError("Composition entry is in mass fraction mode."); - } - // In number fraction mode, the value is cached during the mode switch. - return m_cachedNumberFraction; - } - - double CompositionEntry::number_fraction( - const double totalMolesPerMass - ) const { - // n_i = (moles_i / mass_total) / (moles_total / mass_total) - if (totalMolesPerMass == 0.0) return 0.0; - return m_molesPerMass / totalMolesPerMass; - } - - double CompositionEntry::rel_abundance() const { - return m_molesPerMass; - } - - atomic::Species CompositionEntry::isotope() const { - return m_isotope.value(); - } - - void CompositionEntry::setMassFraction( - const double mass_fraction - ) { - if (!m_massFracMode) { - throw exceptions::CompositionModeError("Composition entry is in number fraction mode."); - } - // Set the invariant from the given mass fraction - if (m_isotope->mass() == 0.0) { - m_molesPerMass = 0.0; - } else { - m_molesPerMass = mass_fraction / m_isotope->mass(); - } - } - - void CompositionEntry::setNumberFraction( - const double number_fraction - ) { - if (m_massFracMode) { - throw exceptions::CompositionModeError("Composition entry is in mass fraction mode."); - } - // In number fraction mode, we only cache the value. The invariant - // m_molesPerMass cannot be calculated until finalize() provides global context. - m_cachedNumberFraction = number_fraction; - } - - bool CompositionEntry::setMassFracMode( - [[maybe_unused]] const double meanMolarMass - ) { - if (m_massFracMode) { - return false; - } - m_massFracMode = true; - // The invariant m_molesPerMass does not change when switching mode. - // The cached number fraction is now stale, but that's okay. - return true; - } - - bool CompositionEntry::setNumberFracMode( - const double totalMolesPerMass - ) { - if (!m_massFracMode) { - return false; - } - m_massFracMode = false; - // Calculate and cache the number fraction for the new mode. - m_cachedNumberFraction = number_fraction(totalMolesPerMass); - return true; - } - - bool CompositionEntry::getMassFracMode() const { - return m_massFracMode; - } - - Composition::Composition( - const std::vector& symbols + Composition::Composition( + const std::vector& symbols ) { for (const auto& symbol : symbols) { registerSymbol(symbol); @@ -197,408 +97,158 @@ namespace fourdst::composition { } } + Composition::Composition( + const std::vector &species + ) { + for (const auto& s : species) { + registerSpecies(s); + } + } + + Composition::Composition( + const std::set &species + ) { + for (const auto& s : species) { + registerSpecies(s); + } + } + Composition::Composition( const std::vector& symbols, - const std::vector& fractions, - const bool massFracMode - ) : m_massFracMode(massFracMode) { - if (symbols.size() != fractions.size()) { - LOG_CRITICAL(getLogger(), "The number of symbols and fractions must be equal (got {} symbols and {} fractions).", symbols.size(), fractions.size()); - throw exceptions::InvalidCompositionError("The number of symbols and fractions must be equal. Got " + std::to_string(symbols.size()) + " symbols and " + std::to_string(fractions.size()) + " fractions."); + const std::vector& molarAbundances + ) { + if (symbols.size() != molarAbundances.size()) { + LOG_CRITICAL(getLogger(), "The number of symbols and molarAbundances must be equal (got {} symbols and {} molarAbundances).", symbols.size(), molarAbundances.size()); + throw exceptions::InvalidCompositionError("The number of symbols and fractions must be equal. Got " + std::to_string(symbols.size()) + " symbols and " + std::to_string(molarAbundances.size()) + " fractions."); } - validateComposition(fractions); - - for (const auto &symbol : symbols) { - registerSymbol(symbol, m_massFracMode); - } - - for (size_t i = 0; i < symbols.size(); ++i) { - if (m_massFracMode) { - setMassFraction(symbols[i], fractions[i]); - } else { - setNumberFraction(symbols[i], fractions[i]); - } - } - 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(getLogger(), "{}", msg); - throw exceptions::InvalidCompositionError(msg); + for (const auto &[symbol, y] : std::views::zip(symbols, molarAbundances)) { + registerSymbol(symbol); + setMolarAbundance(symbol, y); } } - Composition::Composition(const Composition &composition) { - m_finalized = composition.m_finalized; - m_specificNumberDensity = composition.m_specificNumberDensity; - m_meanParticleMass = composition.m_meanParticleMass; - m_massFracMode = composition.m_massFracMode; - m_registeredSymbols = composition.m_registeredSymbols; - m_compositions = composition.m_compositions; + Composition::Composition( + const std::vector &species, + const std::vector &molarAbundances + ) { + if (species.size() != molarAbundances.size()) { + LOG_CRITICAL(getLogger(), "The number of species and molarAbundances must be equal (got {} species and {} molarAbundances).", species.size(), molarAbundances.size()); + throw exceptions::InvalidCompositionError("The number of species and fractions must be equal. Got " + std::to_string(species.size()) + " species and " + std::to_string(molarAbundances.size()) + " fractions."); + } + + for (const auto& [s, y] : std::views::zip(species, molarAbundances)) { + registerSpecies(s); + setMolarAbundance(s, y); + } } - Composition& Composition::operator=(const Composition &other) { + Composition::Composition( + const std::set &symbols, + const std::vector &molarAbundances + ) { + if (symbols.size() != molarAbundances.size()) { + LOG_CRITICAL(getLogger(), "The number of symbols and molarAbundances must be equal (got {} symbols and {} molarAbundances).", symbols.size(), molarAbundances.size()); + throw exceptions::InvalidCompositionError("The number of symbols and fractions must be equal. Got " + std::to_string(symbols.size()) + " symbols and " + std::to_string(molarAbundances.size()) + " fractions."); + } + + for (const auto& [symbol, y] : std::views::zip(sortVectorBy(std::vector(symbols.begin(), symbols.end()), molarAbundances), molarAbundances)) { + registerSymbol(symbol); + setMolarAbundance(symbol, y); + } + } + + Composition::Composition( + const Composition &composition + ) { + m_registeredSpecies = composition.m_registeredSpecies; + m_molarAbundances = composition.m_molarAbundances; + } + + Composition& Composition::operator=( + const Composition &other + ) { if (this != &other) { - m_finalized = other.m_finalized; - m_specificNumberDensity = other.m_specificNumberDensity; - m_meanParticleMass = other.m_meanParticleMass; - m_massFracMode = other.m_massFracMode; - m_registeredSymbols = other.m_registeredSymbols; - m_compositions = other.m_compositions; + m_registeredSpecies = other.m_registeredSpecies; + m_molarAbundances = other.m_molarAbundances; } return *this; } void Composition::registerSymbol( - const std::string& symbol, - const bool massFracMode + const std::string& symbol ) { - if (!isValidSymbol(symbol)) { - LOG_ERROR(getLogger(), "Invalid symbol: {}", symbol); - throw exceptions::InvalidSymbolError("Invalid symbol: " + symbol); + const auto result = getSpecies(symbol); + if (!result) { + throw_unknown_symbol(getLogger(), symbol); } - if (m_registeredSymbols.empty()) { - m_massFracMode = massFracMode; - } else { - if (m_massFracMode != massFracMode) { - LOG_ERROR(getLogger(), "Composition is in {} fraction mode. Cannot register symbol ({}) in {} fraction mode.", m_massFracMode ? "mass" : "number", symbol, massFracMode ? "mass" : "number"); - throw exceptions::CompositionModeError("Composition mode mismatch."); - } - } - - if (m_registeredSymbols.contains(symbol)) { - LOG_WARNING(getLogger(), "Symbol {} is already registered.", symbol); - return; - } - - m_registeredSymbols.insert(symbol); - m_compositions[symbol] = CompositionEntry(symbol, m_massFracMode); - m_finalized = false; - LOG_TRACE_L3(getLogger(), "Registered symbol: {}", symbol); + registerSpecies(result.value()); } void Composition::registerSymbol( - const std::vector& symbols, - const bool massFracMode + const std::vector& symbols ) { for (const auto& symbol : symbols) { - registerSymbol(symbol, massFracMode); + registerSymbol(symbol); } } void Composition::registerSpecies( - const atomic::Species &species, - const bool massFracMode + const atomic::Species &species ) { - registerSymbol(std::string(species.name()), massFracMode); + m_registeredSpecies.insert(species); + if (!m_molarAbundances.contains(species)) { + m_molarAbundances.emplace(species, 0.0); + } } void Composition::registerSpecies( - const std::vector &species, - const bool massFracMode + const std::vector &species ) { for (const auto& s : species) { - registerSpecies(s, massFracMode); + registerSpecies(s); } } std::set Composition::getRegisteredSymbols() const { - return m_registeredSymbols; + std::set symbols; + for (const auto& species : m_registeredSpecies) { + symbols.insert(std::string(species.name())); + } + return symbols; } - std::set Composition::getRegisteredSpecies() const { - std::set result; - for (const auto& entry : m_compositions | std::views::values) { - result.insert(entry.isotope()); - } - return result; + const std::set &Composition::getRegisteredSpecies() const { + return m_registeredSpecies; } - bool Composition::isValidSymbol( - const std::string& symbol - ) { - return atomic::species.contains(symbol); - } - - void Composition::validateComposition(const std::vector& fractions) { - if (!isValidComposition(fractions)) { - LOG_ERROR(getLogger(), "Invalid composition."); - throw exceptions::InvalidCompositionError("Invalid composition."); - } - } - - bool Composition::isValidComposition(const std::vector& fractions) { - const double sum = std::accumulate(fractions.begin(), fractions.end(), 0.0); - if (sum < 0.999999 || sum > 1.000001) { - LOG_ERROR(getLogger(), "The sum of fractions must be equal to 1 (expected 1, got {}).", sum); - return false; - } - return true; - } - - double Composition::setMassFraction(const std::string& symbol, const double& mass_fraction) { - if (!m_registeredSymbols.contains(symbol)) { - LOG_ERROR(getLogger(), "Symbol {} is not registered.", symbol); - throw exceptions::UnregisteredSymbolError("Symbol (" + symbol + ") is not registered."); - } - if (!m_massFracMode) { - LOG_ERROR(getLogger(), "Composition is in number fraction mode."); - throw exceptions::CompositionModeError("Composition is in number fraction mode."); - } - if (mass_fraction < 0.0 || mass_fraction > 1.0) { - LOG_ERROR(getLogger(), "Mass fraction must be between 0 and 1 for symbol {}. Currently it is {}.", symbol, mass_fraction); - throw exceptions::InvalidCompositionError("Mass fraction must be between 0 and 1."); - } - m_finalized = false; - const double old_mass_fraction = m_compositions.at(symbol).mass_fraction(); - m_compositions.at(symbol).setMassFraction(mass_fraction); - return old_mass_fraction; - } - - std::vector Composition::setMassFraction(const std::vector& symbols, const std::vector& mass_fractions) { - if (symbols.size() != mass_fractions.size()) { - throw exceptions::InvalidCompositionError("The number of symbols and mass fractions must be equal."); - } - std::vector old_mass_fractions; - old_mass_fractions.reserve(symbols.size()); - for (size_t i = 0; i < symbols.size(); ++i) { - old_mass_fractions.push_back(setMassFraction(symbols[i], mass_fractions[i])); - } - return old_mass_fractions; - } - - double Composition::setNumberFraction( - const std::string& symbol, - const double& number_fraction - ) { - if (!m_registeredSymbols.contains(symbol)) { - LOG_ERROR(getLogger(), "Symbol {} is not registered.", symbol); - throw exceptions::UnregisteredSymbolError("Symbol (" + symbol + ") is not registered."); - } - if (m_massFracMode) { - LOG_ERROR(getLogger(), "Composition is in mass fraction mode."); - throw exceptions::CompositionModeError("Composition is in mass fraction mode."); - } - if (number_fraction < 0.0 || number_fraction > 1.0) { - LOG_ERROR(getLogger(), "Number fraction must be between 0 and 1 for symbol {}. Currently it is {}.", symbol, number_fraction); - throw exceptions::InvalidCompositionError("Number fraction must be between 0 and 1."); - } - m_finalized = false; - const double old_number_fraction = m_compositions.at(symbol).number_fraction(); - m_compositions.at(symbol).setNumberFraction(number_fraction); - return old_number_fraction; - } - - std::vector Composition::setNumberFraction( - const std::vector& symbols, - const std::vector& number_fractions - ) { - if (symbols.size() != number_fractions.size()) { - throw exceptions::InvalidCompositionError("The number of symbols and number fractions must be equal."); - } - std::vector old_number_fractions; - old_number_fractions.reserve(symbols.size()); - for (size_t i = 0; i < symbols.size(); ++i) { - old_number_fractions.push_back(setNumberFraction(symbols[i], number_fractions[i])); - } - return old_number_fractions; - } - - double Composition::setMassFraction( - const atomic::Species &species, - const double &mass_fraction - ) { - return setMassFraction(std::string(species.name()), mass_fraction); - } - - std::vector Composition::setMassFraction( - const std::vector &species, - const std::vector &mass_fractions - ) { - std::vector symbols; - symbols.reserve(species.size()); - for(const auto& s : species) symbols.emplace_back(s.name()); - return setMassFraction(symbols, mass_fractions); - } - - double Composition::setNumberFraction( - const atomic::Species &species, - const double &number_fraction - ) { - return setNumberFraction(std::string(species.name()), number_fraction); - } - - std::vector Composition::setNumberFraction( - const std::vector &species, - const std::vector &number_fractions - ) { - std::vector symbols; - symbols.reserve(species.size()); - for(const auto& s : species) symbols.emplace_back(s.name()); - return setNumberFraction(symbols, number_fractions); - } - - bool Composition::finalize(const bool norm) { - m_specificNumberDensity = 0.0; - m_meanParticleMass = 0.0; - m_finalized = m_massFracMode ? finalizeMassFracMode(norm) : finalizeNumberFracMode(norm); - m_cache.clear(); - return m_finalized; - } - - bool Composition::finalizeMassFracMode(const bool norm) { - std::vector mass_fractions; - mass_fractions.reserve(m_compositions.size()); - for (const auto &entry: m_compositions | std::views::values) { - mass_fractions.push_back(entry.mass_fraction()); - } - - double sum = std::accumulate(mass_fractions.begin(), mass_fractions.end(), 0.0); - if (norm && sum > 0) { - for (auto& [symbol, entry] : m_compositions) { - setMassFraction(symbol, entry.mass_fraction() / sum); - } - // Recalculate fractions vector after normalization for validation - mass_fractions.clear(); - for (const auto &entry: m_compositions | std::views::values) { - mass_fractions.push_back(entry.mass_fraction()); - } - } - - try { - validateComposition(mass_fractions); - } catch ([[maybe_unused]] const exceptions::InvalidCompositionError& e) { - LOG_ERROR(getLogger(), "Composition is invalid after mass frac finalization (Total mass {}).", sum); - return false; - } - - for (const auto &entry: m_compositions | std::views::values) { - m_specificNumberDensity += entry.rel_abundance(); // rel_abundance is now consistently moles/mass - } - - if (m_specificNumberDensity > 0) { - m_meanParticleMass = 1.0 / m_specificNumberDensity; - } - return true; - } - - bool Composition::finalizeNumberFracMode(const bool norm) { - std::vector number_fractions; - number_fractions.reserve(m_compositions.size()); - for (const auto &entry: m_compositions | std::views::values) { - number_fractions.push_back(entry.number_fraction()); - } - - double sum = std::accumulate(number_fractions.begin(), number_fractions.end(), 0.0); - if (norm && sum > 0) { - for (auto& [symbol, entry] : m_compositions) { - setNumberFraction(symbol, entry.number_fraction() / sum); - } - // Recalculate fractions vector after normalization for validation - number_fractions.clear(); - for (const auto &entry: m_compositions | std::views::values) { - number_fractions.push_back(entry.number_fraction()); - } - } - - try { - validateComposition(number_fractions); - } catch ([[maybe_unused]] const exceptions::InvalidCompositionError& e) { - LOG_ERROR(getLogger(), "Composition is invalid after number frac finalization (Total number frac {}).", sum); - return false; - } - - // Calculate mean particle mass = sum(n_i * A_i) - for (const auto &entry: m_compositions | std::views::values) { - m_meanParticleMass += entry.number_fraction() * entry.isotope().mass(); - } - - for (auto &entry: m_compositions | std::views::values) { - const double X_i = (m_meanParticleMass > 0) ? (entry.number_fraction() * entry.isotope().mass() / m_meanParticleMass) : 0.0; - entry.m_massFracMode = true; - entry.setMassFraction(X_i); - entry.m_massFracMode = false; - } - - if (m_meanParticleMass > 0) { - m_specificNumberDensity = 1.0 / m_meanParticleMass; - } - return true; - } - - Composition Composition::mix(const Composition& other, const double fraction) const { - if (!m_finalized || !other.m_finalized) { - LOG_ERROR(getLogger(), "Compositions have not both been finalized. Hint: Consider running .finalize() on both compositions before mixing."); - throw exceptions::CompositionNotFinalizedError("Compositions have not been finalized (Hint: Consider running .finalize() on both compositions before mixing)."); - } - - if (fraction < 0.0 || fraction > 1.0) { - LOG_ERROR(getLogger(), "Mixing fraction must be between 0 and 1. Currently it is {}.", fraction); - throw exceptions::InvalidCompositionError("Mixing fraction must be between 0 and 1. Currently it is " + std::to_string(fraction) + "."); - } - - std::set mixedSymbols = other.getRegisteredSymbols(); - // Get the union of the two sets of symbols to ensure all species are included in the new composition. - mixedSymbols.insert(m_registeredSymbols.begin(), m_registeredSymbols.end()); - - Composition mixedComposition(mixedSymbols); - for (const auto& symbol : mixedSymbols) { - double otherMassFrac = 0.0; - - const double thisMassFrac = hasSymbol(symbol) ? getMassFraction(symbol) : 0.0; - otherMassFrac = other.hasSymbol(symbol) ? other.getMassFraction(symbol) : 0.0; - - // The mixing formula is a linear interpolation of mass fractions. - double massFraction = fraction * thisMassFrac + otherMassFrac * (1-fraction); - mixedComposition.setMassFraction(symbol, massFraction); - } - 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(getLogger(), "{}", msg); - throw exceptions::InvalidCompositionError(msg); - } - return mixedComposition; - } double Composition::getMassFraction(const std::string& symbol) const { - if (!m_finalized) { - LOG_ERROR(getLogger(), "Composition has not been finalized. Hint: Consider running .finalize()."); - throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize()."); + const auto species = getSpecies(symbol); + if (!species) { + throw_unknown_symbol(getLogger(), symbol); } - if (!m_compositions.contains(symbol)) { - LOG_ERROR(getLogger(), "Symbol {} is not in the composition.", symbol); - std::string currentSymbols; - size_t count = 0; - for (const auto& sym : m_compositions | std::views::keys) { - currentSymbols += sym; - if (count < m_compositions.size() - 2) { - currentSymbols += ", "; - } else if (count == m_compositions.size() - 2) { - currentSymbols += ", and "; - } - count++; - } - throw exceptions::UnregisteredSymbolError("Symbol(" + symbol + ") is not in the current composition. Current composition has symbols: " + currentSymbols + "."); - } - if (m_massFracMode) { - return m_compositions.at(symbol).mass_fraction(); - } - - return m_compositions.at(symbol).mass_fraction(); + return getMassFraction(species.value()); } double Composition::getMassFraction( const atomic::Species &species ) const { - return getMassFraction(std::string(species.name())); + std::map raw_mass; + double totalMass = 0; + for (const auto& [sp, y] : m_molarAbundances) { + const double contrib = y * sp.mass(); + totalMass += contrib; + raw_mass.emplace(sp, contrib); + } + return raw_mass.at(species) / totalMass; } - std::unordered_map Composition::getMassFraction() const { - std::unordered_map mass_fractions; - for (const auto &symbol: m_compositions | std::views::keys) { - mass_fractions[symbol] = getMassFraction(symbol); + std::unordered_map Composition::getMassFraction() const { + std::unordered_map mass_fractions; + for (const auto &species: m_molarAbundances | std::views::keys) { + mass_fractions.emplace(species, getMassFraction(species)); } return mass_fractions; } @@ -607,30 +257,27 @@ namespace fourdst::composition { double Composition::getNumberFraction( const std::string& symbol ) const { - if (!m_finalized) { - LOG_ERROR(getLogger(), "Composition has not been finalized. Hint: Consider running .finalize()."); - throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize()."); + auto species = getSpecies(symbol); + if (!species) { + throw_unknown_symbol(getLogger(), symbol); } - if (!m_compositions.contains(symbol)) { - LOG_ERROR(getLogger(), "Symbol {} is not in the composition.", symbol); - throw exceptions::CompositionNotFinalizedError("Symbol " + symbol + " is not in the composition."); - } - if (!m_massFracMode) { - return m_compositions.at(symbol).number_fraction(); - } - return m_compositions.at(symbol).number_fraction(m_specificNumberDensity); + return getNumberFraction(species.value()); } double Composition::getNumberFraction( const atomic::Species &species ) const { - return getNumberFraction(std::string(species.name())); + double total_moles_per_gram = 0.0; + for (const auto &y: m_molarAbundances | std::views::values) { + total_moles_per_gram += y; + } + return m_molarAbundances.at(species) / total_moles_per_gram; } - std::unordered_map Composition::getNumberFraction() const { - std::unordered_map number_fractions; - for (const auto &symbol: m_compositions | std::views::keys) { - number_fractions[symbol] = getNumberFraction(symbol); + std::unordered_map Composition::getNumberFraction() const { + std::unordered_map number_fractions; + for (const auto &species: m_molarAbundances | std::views::keys) { + number_fractions.emplace(species, getNumberFraction(species)); } return number_fractions; } @@ -638,185 +285,75 @@ namespace fourdst::composition { double Composition::getMolarAbundance( const std::string &symbol ) const { - if (!m_finalized) { - LOG_ERROR(getLogger(), "Composition has not been finalized. Hint: Consider running .finalize()."); - throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize()."); + auto species = getSpecies(symbol); + if (!species) { + throw_unknown_symbol(getLogger(), symbol); } - if (!m_compositions.contains(symbol)) { - LOG_ERROR(getLogger(), "Symbol {} is not in the composition.", symbol); - throw exceptions::UnregisteredSymbolError("Symbol " + symbol + " is not in the composition."); - } - return getMassFraction(symbol) / m_compositions.at(symbol).isotope().mass(); + return getMolarAbundance(species.value()); } double Composition::getMolarAbundance( const atomic::Species &species ) const { - return getMolarAbundance(std::string(species.name())); - } - - std::pair Composition::getComposition( - const std::string& symbol - ) const { - if (!m_finalized) { - LOG_ERROR(getLogger(), "Composition has not been finalized. Hint: Consider running .finalize()."); - throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize()."); + if (!m_molarAbundances.contains(species)) { + LOG_CRITICAL(getLogger(), "Species {} is not registered in the composition.", species.name()); + throw exceptions::UnregisteredSymbolError("Species " + std::string(species.name()) + " is not registered in the composition."); } - if (!m_compositions.contains(symbol)) { - LOG_ERROR(getLogger(), "Symbol {} is not in the composition.", symbol); - throw exceptions::UnregisteredSymbolError("Symbol " + symbol + " is not in the composition."); - } - return {m_compositions.at(symbol), {m_specificNumberDensity, m_meanParticleMass}}; - } - - std::pair Composition::getComposition( - const atomic::Species &species - ) const { - return getComposition(std::string(species.name())); - } - - std::pair, GlobalComposition> Composition::getComposition() const { - if (!m_finalized) { - LOG_ERROR(getLogger(), "Composition has not been finalized. Hint: Consider running .finalize()."); - throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize()."); - } - return {m_compositions, {m_specificNumberDensity, m_meanParticleMass}}; + return m_molarAbundances.at(species); } double Composition::getMeanParticleMass() const { - if (!m_finalized) { - LOG_ERROR(getLogger(), "Composition has not been finalized. Hint: Consider running .finalize()."); - throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize()."); - } - return m_meanParticleMass; - } - - double Composition::getMeanAtomicNumber() const { - if (!m_finalized) { - LOG_ERROR(getLogger(), "Composition must be finalized before getting the mean atomic mass number. Hint: Consider running .finalize()."); - throw exceptions::CompositionNotFinalizedError("Composition not finalized. Cannot retrieve mean atomic mass number. Hint: Consider running .finalize()."); + std::vector X = getMassFractionVector(); + double sum = 0.0; + for (const auto& [species, x] : std::views::zip(m_registeredSpecies, X)) { + sum += x/species.mass(); } - double zSum = 0.0; - - for (const auto &val: m_compositions | std::views::values) { - // Sum of (X_i * Z_i / A_i) - zSum += (val.mass_fraction() * val.m_isotope->z())/val.m_isotope->a(); - } - - // = * sum(X_i * Z_i / A_i) - const double mean_A = m_meanParticleMass * zSum; - return mean_A; + return 1.0 / sum; } double Composition::getElectronAbundance() const { - if (!m_finalized) { - LOG_ERROR(getLogger(), "Composition must be finalized before getting the electron abundance. Hint: Consider running .finalize()."); - throw exceptions::CompositionNotFinalizedError("Composition not finalized. Cannot retrieve electron abundance. Hint: Consider running .finalize()."); - } - - if (m_cache.Ye.has_value()) { - return m_cache.Ye.value(); - } - double Ye = 0.0; - for (const auto &val: m_compositions | std::views::values) { - Ye += (val.mass_fraction() * val.m_isotope->z())/val.m_isotope->a(); + for (const auto& [species, y] : m_molarAbundances) { + Ye += species.z() * y; } - m_cache.Ye = Ye; return Ye; } - Composition Composition::subset( - const std::vector& symbols, - const std::string& method - ) const { - if (const std::array methods = {"norm", "none"}; std::ranges::find(methods, method) == methods.end()) { - const std::string errorMessage = "Invalid method: " + method + ". Valid methods are 'norm' and 'none'."; - LOG_ERROR(getLogger(), "Invalid method: {}. Valid methods are norm and none.", method); - throw exceptions::InvalidMixingMode(errorMessage); - } - - Composition subsetComposition; - for (const auto& symbol : symbols) { - if (!m_compositions.contains(symbol)) { - LOG_ERROR(getLogger(), "Symbol {} is not in the composition.", symbol); - throw exceptions::UnregisteredSymbolError("Symbol " + symbol + " is not in the composition."); - } - subsetComposition.registerSymbol(symbol); - subsetComposition.setMassFraction(symbol, m_compositions.at(symbol).mass_fraction()); - } - if (method == "norm") { - if (const bool isNorm = subsetComposition.finalize(true); !isNorm) { - LOG_ERROR(getLogger(), "Subset composition is invalid. (Unable to finalize with normalization)."); - throw exceptions::FailedToFinalizeCompositionError("Subset composition is invalid. (Unable to finalize with normalization)."); - } - } - return subsetComposition; - } - - void Composition::setCompositionMode( - const bool massFracMode - ) { - if (!m_finalized) { - LOG_ERROR(getLogger(), "Composition has not been finalized. Mode cannot be set unless composition is finalized. Hint: Consider running .finalize()."); - throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Mode cannot be set unless composition is finalized. Hint: Consider running .finalize()."); - } - - bool okay; - for (auto &entry: m_compositions | std::views::values) { - if (massFracMode) { - okay = entry.setMassFracMode(m_meanParticleMass); - } else { - okay = entry.setNumberFracMode(m_specificNumberDensity); - } - if (!okay) { - LOG_ERROR(getLogger(), "Composition mode could not be set due to some unknown error."); - throw std::runtime_error("Composition mode could not be set due to an unknown error."); - } - } - m_massFracMode = massFracMode; - } CanonicalComposition Composition::getCanonicalComposition( const bool harsh ) const { - if (!m_finalized) { - LOG_ERROR(getLogger(), "Composition has not been finalized. Hint: Consider running .finalize()."); - throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize()."); - } + using namespace fourdst::atomic; + if (m_cache.canonicalComp.has_value()) { return m_cache.canonicalComp.value(); // Short circuit if we have cached the canonical composition } CanonicalComposition canonicalComposition; - const std::array canonicalH = { - "H-1", "H-2", "H-3", "H-4", "H-5", "H-6", "H-7" - }; - const std::array canonicalHe = { - "He-3", "He-4", "He-5", "He-6", "He-7", "He-8", "He-9", "He-10" - }; + const std::set canonicalH = {H_1, H_2, H_3, H_4, H_5, H_6, H_7}; + const std::set canonicalHe = {He_3, He_4, He_5, He_6, He_7, He_8, He_9, He_10}; + for (const auto& symbol : canonicalH) { - if (hasSymbol(symbol)) { + if (contains(symbol)) { canonicalComposition.X += getMassFraction(symbol); } } for (const auto& symbol : canonicalHe) { - if (hasSymbol(symbol)) { + if (contains(symbol)) { canonicalComposition.Y += getMassFraction(symbol); } } - for (const auto& symbol : getRegisteredSymbols()) { - const bool isHSymbol = std::ranges::find(canonicalH, symbol) != std::end(canonicalH); - // ReSharper disable once CppTooWideScopeInitStatement - const bool isHeSymbol = std::ranges::find(canonicalHe, symbol) != std::end(canonicalHe); + for (const auto& species : m_molarAbundances | std::views::keys) { + const bool isHIsotope = canonicalH.contains(species); + const bool isHeIsotope = canonicalHe.contains(species); - if (isHSymbol || isHeSymbol) { + if (isHIsotope || isHeIsotope) { continue; // Skip canonical H and He symbols } - canonicalComposition.Z += getMassFraction(symbol); + canonicalComposition.Z += getMassFraction(species); } // ReSharper disable once CppTooWideScopeInitStatement @@ -835,10 +372,6 @@ namespace fourdst::composition { } std::vector Composition::getMassFractionVector() const { - if (!m_finalized) { - LOG_ERROR(getLogger(), "Composition has not been finalized. Hint: Consider running .finalize()."); - throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize()."); - } if (m_cache.massFractions.has_value()) { return m_cache.massFractions.value(); // Short circuit if we have cached the mass fractions } @@ -846,12 +379,12 @@ namespace fourdst::composition { std::vector massFractionVector; std::vector speciesMass; - massFractionVector.reserve(m_compositions.size()); - speciesMass.reserve(m_compositions.size()); + massFractionVector.reserve(m_molarAbundances.size()); + speciesMass.reserve(m_molarAbundances.size()); - for (const auto &entry: m_compositions | std::views::values) { - massFractionVector.push_back(entry.mass_fraction()); - speciesMass.push_back(entry.isotope().mass()); + for (const auto &species: m_molarAbundances | std::views::keys) { + massFractionVector.push_back(getMassFraction(species)); + speciesMass.push_back(species.mass()); } std::vector massFractions = sortVectorBy(massFractionVector, speciesMass); @@ -861,10 +394,6 @@ namespace fourdst::composition { } std::vector Composition::getNumberFractionVector() const { - if (!m_finalized) { - LOG_ERROR(getLogger(), "Composition has not been finalized. Hint: Consider running .finalize()."); - throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize()."); - } if (m_cache.numberFractions.has_value()) { return m_cache.numberFractions.value(); // Short circuit if we have cached the number fractions } @@ -872,12 +401,12 @@ namespace fourdst::composition { std::vector numberFractionVector; std::vector speciesMass; - numberFractionVector.reserve(m_compositions.size()); - speciesMass.reserve(m_compositions.size()); + numberFractionVector.reserve(m_molarAbundances.size()); + speciesMass.reserve(m_molarAbundances.size()); - for (const auto &entry: m_compositions | std::views::values) { - numberFractionVector.push_back(entry.number_fraction()); - speciesMass.push_back(entry.isotope().mass()); + for (const auto &species: m_molarAbundances | std::views::keys) { + numberFractionVector.push_back(getNumberFraction(species)); + speciesMass.push_back(species.mass()); } std::vector numberFractions = sortVectorBy(numberFractionVector, speciesMass); @@ -886,10 +415,6 @@ namespace fourdst::composition { } std::vector Composition::getMolarAbundanceVector() const { - if (!m_finalized) { - LOG_ERROR(getLogger(), "Composition has not been finalized. Hint: Consider running .finalize()."); - throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize()."); - } if (m_cache.molarAbundances.has_value()) { return m_cache.molarAbundances.value(); // Short circuit if we have cached the molar abundances } @@ -897,12 +422,12 @@ namespace fourdst::composition { std::vector molarAbundanceVector; std::vector speciesMass; - molarAbundanceVector.reserve(m_compositions.size()); - speciesMass.reserve(m_compositions.size()); + molarAbundanceVector.reserve(m_molarAbundances.size()); + speciesMass.reserve(m_molarAbundances.size()); - for (const auto &entry: m_compositions | std::views::values) { - molarAbundanceVector.push_back(getMolarAbundance(entry.isotope())); - speciesMass.push_back(entry.isotope().mass()); + for (const auto &[species, y]: m_molarAbundances) { + molarAbundanceVector.push_back(y); + speciesMass.push_back(species.mass()); } std::vector molarAbundances = sortVectorBy(molarAbundanceVector, speciesMass); @@ -914,49 +439,18 @@ namespace fourdst::composition { size_t Composition::getSpeciesIndex( const std::string &symbol ) const { - if (!m_finalized) { - LOG_ERROR(getLogger(), "Composition has not been finalized. Hint: Consider running .finalize()."); - throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize()."); - } - if (!m_compositions.contains(symbol)) { - LOG_ERROR(getLogger(), "Symbol {} is not in the composition.", symbol); - throw exceptions::UnregisteredSymbolError("Symbol " + symbol + " is not in the composition."); - } - if (m_cache.sortedSymbols.has_value()) { - return std::distance( - m_cache.sortedSymbols->begin(), - std::ranges::find( - m_cache.sortedSymbols.value().begin(), - m_cache.sortedSymbols.value().end(), - symbol - ) - ); + const auto species = getSpecies(symbol); + if (!species) { + throw_unknown_symbol(getLogger(), symbol); } - std::vector symbols; - std::vector speciesMass; - - symbols.reserve(m_compositions.size()); - speciesMass.reserve(m_compositions.size()); - - for (const auto &entry: m_compositions | std::views::values) { - symbols.emplace_back(entry.isotope().name()); - speciesMass.push_back(entry.isotope().mass()); - } - - std::vector sortedSymbols = sortVectorBy(symbols, speciesMass); - m_cache.sortedSymbols = sortedSymbols; - return std::distance(sortedSymbols.begin(), std::ranges::find(sortedSymbols, symbol)); + return getSpeciesIndex(species.value()); } size_t Composition::getSpeciesIndex( const atomic::Species &species ) const { - if (!m_finalized) { - LOG_ERROR(getLogger(), "Composition has not been finalized. Hint: Consider running .finalize()."); - throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize()."); - } - if (!m_compositions.contains(static_cast(species.name()))) { + if (!m_registeredSpecies.contains(species)) { LOG_ERROR(getLogger(), "Species {} is not in the composition.", species.name()); throw exceptions::UnregisteredSymbolError("Species " + std::string(species.name()) + " is not in the composition."); } @@ -974,12 +468,12 @@ namespace fourdst::composition { std::vector speciesVector; std::vector speciesMass; - speciesVector.reserve(m_compositions.size()); - speciesMass.reserve(m_compositions.size()); + speciesVector.reserve(m_molarAbundances.size()); + speciesMass.reserve(m_molarAbundances.size()); - for (const auto &entry: m_compositions | std::views::values) { - speciesVector.emplace_back(entry.isotope()); - speciesMass.push_back(entry.isotope().mass()); + for (const auto &s: m_registeredSpecies) { + speciesVector.emplace_back(s); + speciesMass.push_back(s.mass()); } std::vector sortedSpecies = sortVectorBy(speciesVector, speciesMass); @@ -988,16 +482,8 @@ namespace fourdst::composition { } atomic::Species Composition::getSpeciesAtIndex( - size_t index + const size_t index ) const { - if (!m_finalized) { - LOG_ERROR(getLogger(), "Composition has not been finalized. Hint: Consider running .finalize()."); - throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize()."); - } - if (index >= m_compositions.size()) { - LOG_ERROR(getLogger(), "Index {} is out of bounds for composition of size {}.", index, m_compositions.size()); - throw std::out_of_range("Index " + std::to_string(index) + " is out of bounds for composition of size " + std::to_string(m_compositions.size()) + "."); - } if (m_cache.sortedSpecies.has_value()) { return m_cache.sortedSpecies.value().at(index); } @@ -1005,87 +491,116 @@ namespace fourdst::composition { std::vector speciesVector; std::vector speciesMass; - speciesVector.reserve(m_compositions.size()); - speciesMass.reserve(m_compositions.size()); + speciesVector.reserve(m_molarAbundances.size()); + speciesMass.reserve(m_molarAbundances.size()); - for (const auto &entry: m_compositions | std::views::values) { - speciesVector.emplace_back(entry.isotope()); - speciesMass.push_back(entry.isotope().mass()); + for (const auto &species: m_registeredSpecies) { + speciesVector.emplace_back(species); + speciesMass.push_back(species.mass()); } std::vector sortedSymbols = sortVectorBy(speciesVector, speciesMass); return sortedSymbols.at(index); } - bool Composition::hasSymbol( - const std::string& symbol + bool Composition::contains( + const atomic::Species &species ) const { - return m_compositions.contains(symbol); - } - - bool Composition::hasSpecies(const fourdst::atomic::Species &species) const { - return std::ranges::any_of( - m_compositions | std::views::values, - [&species](const CompositionEntry &entry) { - return entry.isotope() == species; - } - ); + return m_registeredSpecies.contains(species); } bool Composition::contains( - const atomic::Species &isotope + const std::string &symbol ) const { - // Check if the isotope's symbol is in the composition - if (!m_finalized) { - LOG_ERROR(getLogger(), "Composition has not been finalized. Hint: Consider running .finalize()."); - throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize()."); + const auto species = getSpecies(symbol); + if (!species) { + throw_unknown_symbol(getLogger(), symbol); } - if (const auto symbol = static_cast(isotope.name()); m_compositions.contains(symbol)) { - return true; + return contains(species.value()); + } + + size_t Composition::size() const { + return m_registeredSpecies.size(); + } + + void Composition::setMolarAbundance( + const std::string &symbol, + const double &molar_abundance + ) { + const auto species = getSpecies(symbol); + if (!species) { + throw_unknown_symbol(getLogger(), symbol); + } + + if (!m_registeredSpecies.contains(species.value())) { + throw_unregistered_symbol(getLogger(), symbol); + } + + m_molarAbundances.at(species.value()) = molar_abundance; + } + + void Composition::setMolarAbundance( + const atomic::Species &species, + const double &molar_abundance + ) { + if (!m_registeredSpecies.contains(species)) { + throw_unregistered_symbol(getLogger(), std::string(species.name())); + } + m_molarAbundances.at(species) = molar_abundance; + } + + void Composition::setMolarAbundance( + const std::vector &symbols, + const std::vector &molar_abundances + ) { + for (const auto& [symbol, y] : std::views::zip(symbols, molar_abundances)) { + setMolarAbundance(symbol, y); + } + } + + void Composition::setMolarAbundance( + const std::vector &species, + const std::vector &molar_abundances + ) { + for (const auto& [s, y] : std::views::zip(species, molar_abundances)) { + setMolarAbundance(s, y); + } + } + + void Composition::setMolarAbundance( + const std::set &symbols, + const std::vector &molar_abundances + ) { + for (const auto& [symbol, y] : std::views::zip(symbols, molar_abundances)) { + setMolarAbundance(symbol, y); + } + } + + void Composition::setMolarAbundance( + const std::set &species, + const std::vector &molar_abundances + ) { + for (const auto& [s, y] : std::views::zip(species, molar_abundances)) { + setMolarAbundance(s, y); } - return false; } /// OVERLOADS - Composition Composition::operator+( - const Composition& other - ) const { - return mix(other, 0.5); - } - - std::ostream& operator<<( - std::ostream& os, - const GlobalComposition& comp - ) { - os << "Global Composition: \n"; - os << "\tSpecific Number Density: " << comp.specificNumberDensity << "\n"; - os << "\tMean Particle Mass: " << comp.meanParticleMass << "\n"; - return os; - } - - std::ostream& operator<<( - std::ostream& os, - const CompositionEntry& entry - ) { - os << "<" << entry.m_symbol.value() << " : m_frac = " << entry.mass_fraction() << ">"; - return os; - } - std::ostream& operator<<( std::ostream& os, const Composition& composition ) { - os << "Composition(finalized: " << (composition.m_finalized ? "true" : "false") << ", " ; + os << "Composition(Mass Fractions => ["; size_t count = 0; - for (const auto &entry: composition.m_compositions | std::views::values) { - os << entry; - if (count < composition.m_compositions.size() - 1) { + for (const auto &species : composition.m_registeredSpecies) { + os << species << ": " << composition.getMassFraction(species); + if (count < composition.size() - 1) { os << ", "; } count++; } - os << ")"; + os << "])"; return os; } diff --git a/src/composition/lib/utils.cpp b/src/composition/lib/utils.cpp new file mode 100644 index 0000000..819eda0 --- /dev/null +++ b/src/composition/lib/utils.cpp @@ -0,0 +1,58 @@ +#include "fourdst/composition/composition.h" +#include "fourdst/composition/exceptions/exceptions_composition.h" +#include "fourdst/atomic/atomicSpecies.h" +#include "fourdst/atomic/species.h" +#include "fourdst/composition/utils.h" + +#include +#include +#include +#include + +namespace { + std::optional getSpecies(const std::string& symbol) { + if (!fourdst::atomic::species.contains(symbol)) { + return std::nullopt; + } + return fourdst::atomic::species.at(symbol); + } + + void throw_unknown_symbol(quill::Logger* logger, const std::string& symbol) { + throw fourdst::composition::exceptions::UnknownSymbolError("Symbol " + symbol + " is not a valid species symbol (not in the species database)"); + } +} + +namespace fourdst::composition { + Composition buildCompositionFromMassFractions( + const std::set &species, + const std::vector &massFractions + ) { + Composition composition; + + for (const auto& [sp, xi] : std::views::zip(species, massFractions)) { + composition.registerSpecies(sp); + composition.setMolarAbundance(sp, xi/sp.mass()); + } + + return composition; + } + + Composition buildCompositionFromMassFractions(const std::vector &species, const std::vector &massFractions) { + return buildCompositionFromMassFractions(std::set(species.begin(), species.end()), massFractions); + } + + Composition buildCompositionFromMassFractions(const std::vector &symbols, const std::vector &massFractions) { + std::set species; + for (const auto& symbol : symbols) { + auto result = getSpecies(symbol); + if (!result) { + throw_unknown_symbol(nullptr, symbol); + } + species.insert(result.value()); + } + return buildCompositionFromMassFractions(species, massFractions); + } + + + +} \ No newline at end of file diff --git a/src/composition/meson.build b/src/composition/meson.build index 11c9413..6bed4e4 100644 --- a/src/composition/meson.build +++ b/src/composition/meson.build @@ -1,7 +1,10 @@ required_headers = [ 'fourdst/composition/atomicSpecies.h', 'fourdst/composition/species.h', - 'fourdst/composition/composition.h' + 'fourdst/composition/composition.h', + 'fourdst/composition/utils.h', + 'fourdst/composition/composition_abstract.h', + 'fourdst/composition/exceptions/exceptions_composition.h' ] foreach h : required_headers @@ -18,6 +21,7 @@ message('✅ libcomposition species_weight dependency declared') composition_sources = files( 'lib/composition.cpp', + 'lib/utils.cpp', ) @@ -44,12 +48,21 @@ composition_dep = declare_dependency( # Make headers accessible composition_headers = files( 'include/fourdst/composition/composition.h', - 'include/fourdst/composition/atomicSpecies.h', - 'include/fourdst/composition/species.h', - 'include/fourdst/composition/elements.h' ) install_headers(composition_headers, subdir : 'fourdst/fourdst/composition') +composition_headers_utils = files( + 'include/fourdst/composition/utils.h', +) +install_headers(composition_headers_utils, subdir : 'fourdst/fourdst/composition/utils') + +composition_headers_atomic = files( + 'include/fourdst/atomic/atomicSpecies.h', + 'include/fourdst/atomic/elements.h', + 'include/fourdst/atomic/species.h', +) +install_headers(composition_headers_atomic, subdir : 'fourdst/fourdst/atomic') + composition_exception_headers = files( 'include/fourdst/composition/exceptions/exceptions_composition.h', ) diff --git a/tests/composition/compositionTest.cpp b/tests/composition/compositionTest.cpp index 576147d..c483176 100644 --- a/tests/composition/compositionTest.cpp +++ b/tests/composition/compositionTest.cpp @@ -3,14 +3,14 @@ #include #include -#include "fourdst/composition/atomicSpecies.h" -#include "fourdst/composition/species.h" +#include "fourdst/atomic/atomicSpecies.h" +#include "fourdst/atomic/species.h" #include "fourdst/composition/composition.h" #include "fourdst/composition/exceptions/exceptions_composition.h" +#include "fourdst/composition/utils.h" #include "fourdst/config/config.h" -std::string EXAMPLE_FILENAME = std::string(getenv("MESON_SOURCE_ROOT")) + "/tests/config/example.yaml"; /** * @brief Test suite for the Composition class and related data structures. @@ -92,7 +92,6 @@ TEST_F(compositionTest, isotopeSpin) { * - The state of the constructed object or the correctness of its methods. */ TEST_F(compositionTest, constructor) { - fourdst::config::Config::getInstance().loadConfig(EXAMPLE_FILENAME); EXPECT_NO_THROW(fourdst::composition::Composition comp); } @@ -108,12 +107,11 @@ TEST_F(compositionTest, constructor) { * - The handling of mode conflicts (e.g., trying to register a symbol in number fraction mode when the composition is already in mass fraction mode). */ TEST_F(compositionTest, registerSymbol) { - fourdst::config::Config::getInstance().loadConfig(EXAMPLE_FILENAME); fourdst::composition::Composition comp; EXPECT_NO_THROW(comp.registerSymbol("H-1")); EXPECT_NO_THROW(comp.registerSymbol("He-4")); - EXPECT_THROW(comp.registerSymbol("H-19"), fourdst::composition::exceptions::InvalidSymbolError); - EXPECT_THROW(comp.registerSymbol("He-21"), fourdst::composition::exceptions::InvalidSymbolError); + EXPECT_THROW(comp.registerSymbol("H-19"), fourdst::composition::exceptions::UnknownSymbolError); + EXPECT_THROW(comp.registerSymbol("He-21"), fourdst::composition::exceptions::UnknownSymbolError); std::set registeredSymbols = comp.getRegisteredSymbols(); EXPECT_TRUE(registeredSymbols.contains("H-1")); @@ -136,276 +134,29 @@ TEST_F(compositionTest, registerSymbol) { * - The correctness of number fraction mode, which is tested separately. */ TEST_F(compositionTest, setGetComposition) { - fourdst::config::Config::getInstance().loadConfig(EXAMPLE_FILENAME); + // fourdst::config::Config::getInstance().loadConfig(EXAMPLE_FILENAME); fourdst::composition::Composition comp; - comp.registerSymbol("H-1"); - comp.registerSymbol("He-4"); + EXPECT_NO_THROW(comp.registerSymbol("H-1")); + EXPECT_NO_THROW(comp.registerSymbol("He-4")); - EXPECT_DOUBLE_EQ(comp.setMassFraction("H-1", 0.5), 0.0); - EXPECT_DOUBLE_EQ(comp.setMassFraction("He-4", 0.5), 0.0); - 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.setMolarAbundance("H-1", 0.6)); + EXPECT_NO_THROW(comp.setMolarAbundance("He-4", 0.4)); - EXPECT_NO_THROW(static_cast(comp.finalize())); - EXPECT_DOUBLE_EQ(comp.getMassFraction("H-1"), 0.6); + EXPECT_DOUBLE_EQ(comp.getMassFraction("H-1"), 0.27414655751871775); + EXPECT_DOUBLE_EQ(comp.getMassFraction("He-4"), 0.7258534424812823); - EXPECT_THROW(comp.setMassFraction("He-3", 0.3), fourdst::composition::exceptions::UnregisteredSymbolError); + EXPECT_THROW(comp.setMolarAbundance("He-3", 0.3), fourdst::composition::exceptions::UnregisteredSymbolError); - const std::vector symbols = {"H-1", "He-4"}; + EXPECT_NO_THROW(comp.registerSymbol("C-12")); + EXPECT_DOUBLE_EQ(comp.getMassFraction("H-1"), 0.27414655751871775); + EXPECT_DOUBLE_EQ(comp.getMassFraction("He-4"), 0.7258534424812823); - EXPECT_NO_THROW(comp.setMassFraction(symbols, {0.5, 0.5})); - EXPECT_THROW(auto r = comp.getComposition("H-1"), fourdst::composition::exceptions::CompositionNotFinalizedError); - EXPECT_TRUE(comp.finalize()); - EXPECT_DOUBLE_EQ(comp.getComposition("H-1").first.mass_fraction(), 0.5); + EXPECT_NO_THROW(comp.setMolarAbundance("C-12", 0.1)); - EXPECT_NO_THROW(comp.setMassFraction(symbols, {0.6, 0.6})); - EXPECT_FALSE(comp.finalize()); - EXPECT_THROW(auto r = comp.getComposition("H-1"), fourdst::composition::exceptions::CompositionNotFinalizedError); -} + EXPECT_DOUBLE_EQ(comp.getMassFraction("H-1"), 0.177551918933757); + EXPECT_DOUBLE_EQ(comp.getMassFraction("He-4"), 0.4701013674717613); + EXPECT_DOUBLE_EQ(comp.getMassFraction("C-12"), 0.3523467135944818); -/** - * @brief Tests the workflow of setting and getting number fractions. - * @details This test mirrors `setGetComposition` but for number fraction mode. It verifies - * that symbols can be registered in number fraction mode and that `setNumberFraction` and - * `getNumberFraction` work as expected. - * @par What this test proves: - * - The composition can be correctly initialized and operated in number fraction mode. - * - `setNumberFraction` and `getNumberFraction` function correctly. - * - An attempt to set a fraction for an unregistered symbol throws the correct exception. - * @par What this test does not prove: - * - The correctness of conversions between mass and number fraction modes. - */ -TEST_F(compositionTest, setGetNumberFraction) { - fourdst::config::Config::getInstance().loadConfig(EXAMPLE_FILENAME); - fourdst::composition::Composition comp; - comp.registerSymbol("H-1", false); - comp.registerSymbol("He-4", false); - - EXPECT_DOUBLE_EQ(comp.setNumberFraction("H-1", 0.5), 0.0); - EXPECT_DOUBLE_EQ(comp.setNumberFraction("He-4", 0.5), 0.0); - 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(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); -} - -/** - * @brief Tests the creation of a normalized subset of a composition. - * @details This test creates a composition, finalizes it, and then extracts a subset - * containing only one of the original elements. It verifies that the `subset` method with - * the "norm" option creates a new, valid composition where the single element's mass - * fraction is normalized to 1.0. - * @par What this test proves: - * - The `subset` method can extract a subset of symbols. - * - The "norm" method correctly renormalizes the fractions in the new subset to sum to 1.0. - * @par What this test does not prove: - * - The behavior of the `subset` method with the "none" option. - */ -TEST_F(compositionTest, subset) { - fourdst::config::Config::getInstance().loadConfig(EXAMPLE_FILENAME); - fourdst::composition::Composition comp; - comp.registerSymbol("H-1"); - comp.registerSymbol("He-4"); - comp.setMassFraction("H-1", 0.6); - comp.setMassFraction("He-4", 0.4); - EXPECT_NO_THROW(static_cast(comp.finalize())); - - std::vector symbols = {"H-1"}; - fourdst::composition::Composition subsetComp = comp.subset(symbols, "norm"); - EXPECT_TRUE(subsetComp.finalize()); - EXPECT_DOUBLE_EQ(subsetComp.getMassFraction("H-1"), 1.0); -} - -/** - * @brief Tests the auto-normalization feature of the `finalize` method. - * @details This test sets mass fractions that do not sum to 1.0 and then calls - * `finalize(true)`. It verifies that the composition is successfully finalized and that - * the mass fractions are correctly scaled to sum to 1.0. - * @par What this test proves: - * - `finalize(true)` correctly calculates the sum of fractions and normalizes each entry. - * - The resulting composition is valid and its normalized values can be retrieved. - * @par What this test does not prove: - * - The behavior of `finalize(false)`, which is tested separately. - */ -TEST_F(compositionTest, finalizeWithNormalization) { - fourdst::config::Config::getInstance().loadConfig(EXAMPLE_FILENAME); - fourdst::composition::Composition comp; - comp.registerSymbol("H-1"); - comp.registerSymbol("He-4"); - comp.setMassFraction("H-1", 0.3); - comp.setMassFraction("He-4", 0.3); - EXPECT_TRUE(comp.finalize(true)); - EXPECT_DOUBLE_EQ(comp.getMassFraction("H-1"), 0.5); - EXPECT_DOUBLE_EQ(comp.getMassFraction("He-4"), 0.5); -} - -/** - * @brief Tests the default (non-normalizing) behavior of the `finalize` method. - * @details This test sets mass fractions that already sum to 1.0 and calls `finalize(false)`. - * It verifies that the composition is successfully finalized and the fractions remain unchanged. - * @par What this test proves: - * - `finalize(false)` or `finalize()` correctly validates a pre-normalized composition without altering its values. - * @par What this test does not prove: - * - That `finalize(false)` would fail for a non-normalized composition (this is implicitly tested in `setGetComposition`). - */ -TEST_F(compositionTest, finalizeWithoutNormalization) { - fourdst::config::Config::getInstance().loadConfig(EXAMPLE_FILENAME); - fourdst::composition::Composition comp; - comp.registerSymbol("H-1"); - comp.registerSymbol("He-4"); - comp.setMassFraction("H-1", 0.5); - comp.setMassFraction("He-4", 0.5); - EXPECT_TRUE(comp.finalize(false)); - EXPECT_DOUBLE_EQ(comp.getMassFraction("H-1"), 0.5); - EXPECT_DOUBLE_EQ(comp.getMassFraction("He-4"), 0.5); -} - -/** - * @brief Tests the retrieval of global composition properties. - * @details After creating and finalizing a composition, this test retrieves the - * `CompositionEntry` and `GlobalComposition` data. It verifies that the mass fraction - * in the entry and the calculated global properties (mean particle mass, specific number density) - * are correct for the given input composition. - * @par What this test proves: - * - The `finalize` method correctly computes `meanParticleMass` and `specificNumberDensity`. - * - The `getComposition` method returns a pair containing the correct entry-level and global data. - * @par What this test does not prove: - * - The correctness of these calculations for all possible compositions, particularly complex ones. It validates the mechanism for a simple binary mixture. - */ -TEST_F(compositionTest, getComposition) { - fourdst::config::Config::getInstance().loadConfig(EXAMPLE_FILENAME); - fourdst::composition::Composition comp; - comp.registerSymbol("H-1"); - comp.registerSymbol("He-4"); - comp.setMassFraction("H-1", 0.6); - comp.setMassFraction("He-4", 0.4); - EXPECT_NO_THROW(static_cast(comp.finalize())); - - const auto compositionEntry = comp.getComposition("H-1"); - EXPECT_DOUBLE_EQ(compositionEntry.first.mass_fraction(), 0.6); - EXPECT_DOUBLE_EQ(compositionEntry.second.meanParticleMass, 1.4382769310381101); - EXPECT_DOUBLE_EQ(compositionEntry.second.specificNumberDensity, 1.0/1.4382769310381101); -} - -/** - * @brief Tests the ability to switch between mass and number fraction modes. - * @details This test creates a composition in mass fraction mode, finalizes it, and then - * switches to number fraction mode using `setCompositionMode(false)`. It then modifies the - * composition using number fractions and verifies that it must be re-finalized before switching back. - * @par What this test proves: - * - `setCompositionMode` can be called on a finalized composition. - * - After switching modes, the appropriate `set...Fraction` method can be used. - * - Switching modes requires the composition to be finalized, and modifying it after the switch un-finalizes it again. - * @par What this test does not prove: - * - The numerical correctness of the fraction conversions that happen internally when the mode is switched. - */ -TEST_F(compositionTest, setCompositionMode) { - fourdst::config::Config::getInstance().loadConfig(EXAMPLE_FILENAME); - fourdst::composition::Composition comp; - comp.registerSymbol("H-1"); - comp.registerSymbol("He-4"); - comp.setMassFraction("H-1", 0.6); - comp.setMassFraction("He-4", 0.4); - 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); - - EXPECT_NO_THROW(comp.setCompositionMode(false)); - - EXPECT_NO_THROW(comp.setNumberFraction("H-1", 0.9)); - EXPECT_NO_THROW(comp.setNumberFraction("He-4", 0.1)); - - EXPECT_THROW(comp.setCompositionMode(true), fourdst::composition::exceptions::CompositionNotFinalizedError); - EXPECT_NO_THROW(static_cast(comp.finalize())); - EXPECT_NO_THROW(comp.setCompositionMode(true)); -} - -/** - * @brief Tests the `hasSymbol` utility method. - * @details This test verifies that `hasSymbol` correctly reports the presence of registered - * symbols and the absence of non-registered symbols. - * @par What this test proves: - * - The `hasSymbol` method accurately checks for the existence of a key in the internal composition map. - * @par What this test does not prove: - * - Anything about the state (e.g., mass fraction) of the symbol, only its presence. - */ -TEST_F(compositionTest, hasSymbol) { - fourdst::config::Config::getInstance().loadConfig(EXAMPLE_FILENAME); - fourdst::composition::Composition comp; - comp.registerSymbol("H-1"); - comp.registerSymbol("He-4"); - comp.setMassFraction("H-1", 0.6); - comp.setMassFraction("He-4", 0.4); - EXPECT_NO_THROW(static_cast(comp.finalize())); - - EXPECT_TRUE(comp.hasSymbol("H-1")); - EXPECT_TRUE(comp.hasSymbol("He-4")); - EXPECT_FALSE(comp.hasSymbol("H-2")); - EXPECT_FALSE(comp.hasSymbol("He-3")); -} - -/** - * @brief Tests the mixing of two compositions. - * @details This test creates two distinct compositions, finalizes them, and then mixes them - * using both the `+` operator (50/50 mix) and the `mix` method with a specific fraction (25/75). - * It verifies that the resulting mass fractions in the new compositions are correct. - * @par What this test proves: - * - The `mix` method and the `+` operator correctly perform linear interpolation of mass fractions. - * - The resulting mixed composition is valid and its properties are correctly calculated. - * @par What this test does not prove: - * - The behavior when mixing compositions with non-overlapping sets of symbols. - */ -TEST_F(compositionTest, mix) { - fourdst::config::Config::getInstance().loadConfig(EXAMPLE_FILENAME); - fourdst::composition::Composition comp1; - comp1.registerSymbol("H-1"); - comp1.registerSymbol("He-4"); - comp1.setMassFraction("H-1", 0.6); - comp1.setMassFraction("He-4", 0.4); - 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(static_cast(comp2.finalize())); - - fourdst::composition::Composition mixedComp = comp1 + comp2; - EXPECT_TRUE(mixedComp.finalize()); - EXPECT_DOUBLE_EQ(mixedComp.getMassFraction("H-1"), 0.5); - EXPECT_DOUBLE_EQ(mixedComp.getMassFraction("He-4"), 0.5); - - fourdst::composition::Composition mixedComp2 = comp1.mix(comp2, 0.25); - EXPECT_TRUE(mixedComp2.finalize()); - EXPECT_DOUBLE_EQ(mixedComp2.getMassFraction("H-1"), 0.45); - EXPECT_DOUBLE_EQ(mixedComp2.getMassFraction("He-4"), 0.55); -} - -/** - * @brief Tests the calculation of molar abundance. - * @details This test creates a simple composition and verifies that `getMolarAbundance` - * returns the correct value, which is defined as (mass fraction / atomic mass). - * @par What this test proves: - * - The `getMolarAbundance` calculation is performed correctly. - * @par What this test does not prove: - * - The correctness of the underlying mass data, which is tested separately. - */ -TEST_F(compositionTest, molarAbundance) { - fourdst::composition::Composition comp1; - comp1.registerSymbol("H-1"); - comp1.registerSymbol("He-4"); - comp1.setMassFraction("H-1", 0.5); - comp1.setMassFraction("He-4", 0.5); - 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()); } /** @@ -421,12 +172,12 @@ TEST_F(compositionTest, molarAbundance) { */ TEST_F(compositionTest, getRegisteredSpecies) { fourdst::composition::Composition comp; - comp.registerSpecies({fourdst::atomic::Be_7, fourdst::atomic::H_1, fourdst::atomic::He_4}, true); + comp.registerSpecies({fourdst::atomic::Be_7, fourdst::atomic::H_1, fourdst::atomic::He_4}); auto registeredSpecies = comp.getRegisteredSpecies(); EXPECT_TRUE(registeredSpecies.contains(fourdst::atomic::H_1)); EXPECT_TRUE(registeredSpecies.contains(fourdst::atomic::He_4)); EXPECT_FALSE(registeredSpecies.contains(fourdst::atomic::Li_6)); - auto it1 = registeredSpecies.begin(); + const auto it1 = registeredSpecies.begin(); EXPECT_EQ(*it1, fourdst::atomic::H_1); } @@ -444,236 +195,6 @@ TEST_F(compositionTest, getSpeciesFromAZ) { EXPECT_EQ(fourdst::atomic::SpeciesErrorType::ELEMENT_SYMBOL_NOT_FOUND, fourdst::atomic::az_to_species(120, 500).error()); } -/** - * @brief Tests constructors that take vectors and sets of symbols. - * @details This test verifies that the Composition can be constructed from a vector or set of symbols, - * and that the resulting object correctly registers those symbols. - * @par What this test proves: - * - Constructors accepting std::vector and std::set of symbols work as expected. - * - Registered symbols are correctly tracked. - */ -TEST_F(compositionTest, constructorWithSymbolsVectorAndSet) { - fourdst::config::Config::getInstance().loadConfig(EXAMPLE_FILENAME); - std::vector vs = {"H-1", "He-4"}; - std::set ss = {"H-1", "He-4"}; - - fourdst::composition::Composition compVec(vs); - EXPECT_TRUE(compVec.hasSymbol("H-1")); - EXPECT_TRUE(compVec.hasSymbol("He-4")); - - fourdst::composition::Composition compSet(ss); - EXPECT_TRUE(compSet.hasSymbol("H-1")); - EXPECT_TRUE(compSet.hasSymbol("He-4")); -} - -/** - * @brief Tests constructors that take symbols and fractions for both mass and number fraction modes. - * @details This test verifies that the Composition can be constructed directly from vectors of symbols and fractions, - * and that the resulting mass and number fractions are correct. It also checks conversions between modes. - * @par What this test proves: - * - Constructors for both mass and number fraction modes work as expected. - * - Conversion between mass and number fraction modes is correct. - */ -TEST_F(compositionTest, constructorWithSymbolsAndFractionsMassAndNumber) { - fourdst::config::Config::getInstance().loadConfig(EXAMPLE_FILENAME); - using fourdst::composition::Composition; - using fourdst::atomic::species; - - // Mass-fraction constructor - std::vector symM = {"H-1", "He-4"}; - std::vector fracM = {0.6, 0.4}; - Composition compM(symM, fracM, true); - EXPECT_NEAR(compM.getMassFraction("H-1"), 0.6, 1e-12); - EXPECT_NEAR(compM.getMassFraction("He-4"), 0.4, 1e-12); - // Mean particle mass and specific number density are reciprocals - double sn = 0.6/species.at("H-1").mass() + 0.4/species.at("He-4").mass(); - double mp = 1.0/sn; - EXPECT_NEAR(compM.getMeanParticleMass(), mp, 1e-12); - - // Number-fraction constructor - std::vector symN = {"H-1", "He-4"}; - std::vector fracN = {0.9, 0.1}; - Composition compN(symN, fracN, false); - EXPECT_NEAR(compN.getNumberFraction("H-1"), 0.9, 1e-12); - EXPECT_NEAR(compN.getNumberFraction("He-4"), 0.1, 1e-12); - double meanA = 0.9*species.at("H-1").mass() + 0.1*species.at("He-4").mass(); - EXPECT_NEAR(compN.getMeanParticleMass(), meanA, 1e-12); - // Check converted mass fractions X_i = n_i * A_i / - double xH = 0.9*species.at("H-1").mass()/meanA; - double xHe = 0.1*species.at("He-4").mass()/meanA; - compN.setCompositionMode(true); - EXPECT_NEAR(compN.getMassFraction("H-1"), xH, 1e-12); - EXPECT_NEAR(compN.getMassFraction("He-4"), xHe, 1e-12); -} - -/** - * @brief Tests registering symbols via vector, single Species, and mode mismatch error. - * @details This test checks that symbols can be registered via a vector, that registering by Species works, - * and that attempting to register a symbol with a mismatched mode throws the correct exception. - * @par What this test proves: - * - registerSymbol works with vectors. - * - registerSpecies works with single Species. - * - Mode mismatch throws CompositionModeError. - */ -TEST_F(compositionTest, registerSymbolVectorAndSingleSpeciesAndModeMismatch) { - fourdst::config::Config::getInstance().loadConfig(EXAMPLE_FILENAME); - using fourdst::composition::Composition; - Composition comp; - comp.registerSymbol(std::vector{"H-1", "He-4"}); - EXPECT_TRUE(comp.hasSymbol("H-1")); - EXPECT_TRUE(comp.hasSymbol("He-4")); - - // Register by Species - Composition comp2; - comp2.registerSpecies(fourdst::atomic::H_1); - comp2.registerSpecies(fourdst::atomic::He_4); - EXPECT_TRUE(comp2.hasSymbol("H-1")); - EXPECT_TRUE(comp2.hasSymbol("He-4")); - - // Mode mismatch should throw - Composition comp3; - comp3.registerSymbol("H-1", true); // mass mode - EXPECT_THROW(comp3.registerSymbol("He-4", false), fourdst::composition::exceptions::CompositionModeError); -} - -/** - * @brief Tests setMassFraction overloads for Species and vectors. - * @details This test verifies that setMassFraction works for both single Species and vectors of Species, - * and that the returned old values are correct. - * @par What this test proves: - * - setMassFraction overloads work as expected. - * - Old values are returned correctly. - */ -TEST_F(compositionTest, setMassFractionBySpeciesAndVector) { - fourdst::config::Config::getInstance().loadConfig(EXAMPLE_FILENAME); - using fourdst::composition::Composition; - using fourdst::atomic::H_1; - using fourdst::atomic::He_4; - - Composition comp; - comp.registerSymbol("H-1"); - comp.registerSymbol("He-4"); - - // Single species overload - double old = comp.setMassFraction(H_1, 0.7); - EXPECT_NEAR(old, 0.0, 1e-15); - old = comp.setMassFraction(He_4, 0.3); - EXPECT_NEAR(old, 0.0, 1e-15); - - // Vector overload - std::vector sp = {H_1, He_4}; - std::vector xs = {0.6, 0.4}; - auto olds = comp.setMassFraction(sp, xs); - ASSERT_EQ(olds.size(), 2u); - EXPECT_NEAR(olds[0], 0.7, 1e-12); - EXPECT_NEAR(olds[1], 0.3, 1e-12); - - EXPECT_TRUE(comp.finalize()); - EXPECT_NEAR(comp.getMassFraction("H-1"), 0.6, 1e-12); - EXPECT_NEAR(comp.getMassFraction(He_4), 0.4, 1e-12); -} - -/** - * @brief Tests setNumberFraction overloads for symbols and Species. - * @details This test verifies that setNumberFraction works for both single symbols/Species and vectors, - * and that the returned old values are correct. - * @par What this test proves: - * - setNumberFraction overloads work as expected. - * - Old values are returned correctly. - */ -TEST_F(compositionTest, setNumberFractionOverloads) { - fourdst::config::Config::getInstance().loadConfig(EXAMPLE_FILENAME); - using fourdst::composition::Composition; - using fourdst::atomic::H_1; - using fourdst::atomic::He_4; - - Composition comp; - comp.registerSymbol("H-1", false); - comp.registerSymbol("He-4", false); - - // Single symbol - double old = comp.setNumberFraction("H-1", 0.8); - EXPECT_NEAR(old, 0.0, 1e-15); - // Vector of symbols - auto oldv = comp.setNumberFraction(std::vector{"H-1", "He-4"}, std::vector{0.75, 0.25}); - ASSERT_EQ(oldv.size(), 2u); - EXPECT_NEAR(oldv[0], 0.8, 1e-12); - EXPECT_NEAR(oldv[1], 0.0, 1e-12); - - // Species and vector - old = comp.setNumberFraction(H_1, 0.7); - EXPECT_NEAR(old, 0.75, 1e-12); - auto oldsv = comp.setNumberFraction(std::vector{H_1, He_4}, std::vector{0.6, 0.4}); - ASSERT_EQ(oldsv.size(), 2u); - EXPECT_NEAR(oldsv[0], 0.7, 1e-12); - EXPECT_NEAR(oldsv[1], 0.25, 1e-12); - - EXPECT_TRUE(comp.finalize()); - EXPECT_NEAR(comp.getNumberFraction("H-1"), 0.6, 1e-12); - EXPECT_NEAR(comp.getNumberFraction(He_4), 0.4, 1e-12); -} - -/** - * @brief Tests error cases for mixing compositions. - * @details This test checks that mixing with a non-finalized composition or with invalid fractions throws the correct exceptions. - * @par What this test proves: - * - Mixing with a non-finalized composition throws CompositionNotFinalizedError. - * - Mixing with invalid fractions throws InvalidCompositionError. - */ -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); - 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); - 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); -} - -/** - * @brief Tests getMassFraction and getNumberFraction maps and Species overloads. - * @details This test verifies that the getter methods for mass and number fractions return correct maps, - * and that overloads for Species work as expected. - * @par What this test proves: - * - getMassFraction and getNumberFraction return correct maps. - * - Overloads for Species work as expected. - */ -TEST_F(compositionTest, getMassAndNumberFractionMapsAndSpeciesOverloads) { - fourdst::config::Config::getInstance().loadConfig(EXAMPLE_FILENAME); - using fourdst::composition::Composition; - - Composition comp; comp.registerSymbol("H-1"); comp.registerSymbol("He-4"); - comp.setMassFraction("H-1", 0.6); comp.setMassFraction("He-4", 0.4); - ASSERT_TRUE(comp.finalize()); - - auto m = comp.getMassFraction(); - ASSERT_EQ(m.size(), 2u); - EXPECT_NEAR(m.at("H-1"), 0.6, 1e-12); - EXPECT_NEAR(m.at("He-4"), 0.4, 1e-12); - EXPECT_NEAR(comp.getMassFraction(fourdst::atomic::H_1), 0.6, 1e-12); - EXPECT_NEAR(comp.getMolarAbundance(fourdst::atomic::H_1), m.at("H-1")/fourdst::atomic::H_1.mass(), 1e-12); - - // Switch to number-fraction mode and verify number maps - comp.setCompositionMode(false); - // Must re-finalize after modifications (mode switch itself keeps values consistent but not finalized status changed? setCompositionMode requires to be finalized; here we just switched modes) - // Set specific number fractions and finalize - comp.setNumberFraction("H-1", 0.7); - comp.setNumberFraction("He-4", 0.3); - ASSERT_TRUE(comp.finalize()); - - auto n = comp.getNumberFraction(); - ASSERT_EQ(n.size(), 2u); - EXPECT_NEAR(n.at("H-1"), 0.7, 1e-12); - EXPECT_NEAR(n.at("He-4"), 0.3, 1e-12); - EXPECT_NEAR(comp.getNumberFraction(fourdst::atomic::He_4), 0.3, 1e-12); -} /** * @brief Tests mean atomic number and electron abundance calculations. @@ -682,255 +203,40 @@ TEST_F(compositionTest, getMassAndNumberFractionMapsAndSpeciesOverloads) { * @par What this test proves: * - getElectronAbundance and getMeanAtomicNumber are calculated correctly. */ -TEST_F(compositionTest, meanAtomicNumberAndElectronAbundance) { - fourdst::config::Config::getInstance().loadConfig(EXAMPLE_FILENAME); +TEST_F(compositionTest, meanElectronAbundance) { using fourdst::atomic::species; using fourdst::composition::Composition; - Composition comp; comp.registerSymbol("H-1"); comp.registerSymbol("He-4"); - comp.setMassFraction("H-1", 0.6); comp.setMassFraction("He-4", 0.4); - ASSERT_TRUE(comp.finalize()); + Composition comp; + comp.registerSymbol("H-1"); + comp.registerSymbol("He-4"); - // Compute expected Ye = sum(X_i * Z_i / A_i) - constexpr double xH = 0.6, xHe = 0.4; - const double aH = species.at("H-1").a(); - const double aHe = species.at("He-4").a(); - const double zH = species.at("H-1").z(); - const double zHe = species.at("He-4").z(); - const double expectedYe = xH*zH/aH + xHe*zHe/aHe; + comp.setMolarAbundance("H-1", 0.6); + comp.setMolarAbundance("He-4", 0.4); + + const double expectedYe = 0.6 * species.at("H-1").z() + 0.4 * species.at("He-4").z(); EXPECT_NEAR(comp.getElectronAbundance(), expectedYe, 1e-12); - - // = * sum(X_i * Z_i / A_i) - const double expectedZ = comp.getMeanParticleMass() * expectedYe; - EXPECT_NEAR(comp.getMeanAtomicNumber(), expectedZ, 1e-12); } -/** - * @brief Tests canonical composition and caching behavior. - * @details This test verifies that getCanonicalComposition returns correct X, Y, Z values, - * and that repeated calls use the cached result. - * @par What this test proves: - * - getCanonicalComposition returns correct canonical values. - * - Caching works as expected. - */ -TEST_F(compositionTest, canonicalCompositionAndCaching) { - fourdst::config::Config::getInstance().loadConfig(EXAMPLE_FILENAME); +TEST_F(compositionTest, buildFromMassFractions) { + using fourdst::atomic::Species; + using namespace fourdst::atomic; using fourdst::composition::Composition; - Composition comp; comp.registerSymbol("H-1"); comp.registerSymbol("He-4"); - comp.setMassFraction("H-1", 0.6); comp.setMassFraction("He-4", 0.4); - ASSERT_TRUE(comp.finalize()); + const std::vector sVec = {H_1, He_4, C_12}; + const std::vector massFractions = {0.7, 0.28, 0.02}; - auto canon1 = comp.getCanonicalComposition(); - EXPECT_NEAR(canon1.X, 0.6, 1e-12); - EXPECT_NEAR(canon1.Y, 0.4, 1e-12); - EXPECT_NEAR(canon1.Z, 0.0, 1e-12); + const Composition comp = fourdst::composition::buildCompositionFromMassFractions(sVec, massFractions); - // Call again to exercise caching code path - auto canon2 = comp.getCanonicalComposition(); - EXPECT_NEAR(canon2.X, 0.6, 1e-12); - EXPECT_NEAR(canon2.Y, 0.4, 1e-12); - EXPECT_NEAR(canon2.Z, 0.0, 1e-12); + EXPECT_DOUBLE_EQ(comp.getMassFraction(H_1), 0.7); + EXPECT_DOUBLE_EQ(comp.getMassFraction(He_4), 0.28); + EXPECT_DOUBLE_EQ(comp.getMassFraction(C_12), 0.02); - // Add a metal and re-check - Composition comp2; comp2.registerSymbol("H-1"); comp2.registerSymbol("He-4"); comp2.registerSymbol("O-16"); - comp2.setMassFraction("H-1", 0.6); comp2.setMassFraction("He-4", 0.35); comp2.setMassFraction("O-16", 0.05); - ASSERT_TRUE(comp2.finalize()); - auto canon3 = comp2.getCanonicalComposition(true); - EXPECT_NEAR(canon3.X, 0.6, 1e-12); - EXPECT_NEAR(canon3.Y, 0.35, 1e-12); - EXPECT_NEAR(canon3.Z, 0.05, 1e-12); } -/** - * @brief Tests vector getters, indexing, and species-at-index functionality. - * @details This test verifies that the vector getters for mass, number, and molar abundance fractions - * return correct values, and that species can be accessed by index. - * @par What this test proves: - * - Vector getters return correct values. - * - Indexing and species-at-index work as expected. - */ -TEST_F(compositionTest, vectorsAndIndexingAndSpeciesAtIndex) { - fourdst::config::Config::getInstance().loadConfig(EXAMPLE_FILENAME); - using fourdst::composition::Composition; - using fourdst::atomic::species; - 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()); - // Mass fraction vector sorted by mass: H-1, He-4, O-16 - auto mv = comp.getMassFractionVector(); - ASSERT_EQ(mv.size(), 3u); - EXPECT_NEAR(mv[0], 0.5, 1e-12); - EXPECT_NEAR(mv[1], 0.3, 1e-12); - EXPECT_NEAR(mv[2], 0.2, 1e-12); - - // Species indices ordering - size_t iH = comp.getSpeciesIndex("H-1"); - size_t iHe = comp.getSpeciesIndex("He-4"); - size_t iO = comp.getSpeciesIndex("O-16"); - EXPECT_LT(iH, iHe); - EXPECT_LT(iHe, iO); - EXPECT_EQ(comp.getSpeciesIndex(fourdst::atomic::H_1), iH); - EXPECT_EQ(comp.getSpeciesIndex(species.at("He-4")), iHe); - - // Species at index - auto sAtHe = comp.getSpeciesAtIndex(iHe); - EXPECT_EQ(std::string(sAtHe.name()), std::string("He-4")); - - // Number fraction vector after switching modes - comp.setCompositionMode(false); - // Tweak number fractions and finalize - // Compute expected number fractions from original mass fractions first - double denom = 0.5/species.at("H-1").mass() + 0.3/species.at("He-4").mass() + 0.2/species.at("O-16").mass(); - double nH_exp = (0.5/species.at("H-1").mass())/denom; - double nHe_exp = (0.3/species.at("He-4").mass())/denom; - double nO_exp = (0.2/species.at("O-16").mass())/denom; - - auto nv0 = comp.getNumberFractionVector(); - ASSERT_EQ(nv0.size(), 3u); - EXPECT_NEAR(nv0[iH], nH_exp, 1e-12); - EXPECT_NEAR(nv0[iHe], nHe_exp, 1e-12); - EXPECT_NEAR(nv0[iO], nO_exp, 1e-12); - - // Molar abundance vector X_i/A_i in mass mode; switch back to mass mode to verify - comp.setCompositionMode(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); - EXPECT_NEAR(av[iHe], 0.3/species.at("He-4").mass(), 1e-12); - EXPECT_NEAR(av[iO], 0.2/species.at("O-16").mass(), 1e-12); -} - -/** - * @brief Tests contains method and pre-finalization guards. - * @details This test verifies that contains throws before finalization and works correctly after. - * @par What this test proves: - * - contains throws before finalize. - * - contains works as expected after finalize. - */ -TEST_F(compositionTest, containsAndPreFinalizationGuards) { - fourdst::config::Config::getInstance().loadConfig(EXAMPLE_FILENAME); - fourdst::composition::Composition comp; - comp.registerSymbol("H-1"); comp.registerSymbol("He-4"); - comp.setMassFraction("H-1", 0.6); comp.setMassFraction("He-4", 0.4); - // contains should throw before finalize - EXPECT_THROW(static_cast(comp.contains(fourdst::atomic::H_1)), fourdst::composition::exceptions::CompositionNotFinalizedError); - - ASSERT_TRUE(comp.finalize()); - EXPECT_TRUE(comp.contains(fourdst::atomic::H_1)); - EXPECT_FALSE(comp.contains(fourdst::atomic::Li_6)); -} - -/** - * @brief Tests subset method with "none" normalization and normalization flow. - * @details This test verifies that subset with "none" does not normalize by default, - * and that normalization can be forced with finalize(true). - * @par What this test proves: - * - subset with "none" does not normalize by default. - * - finalize(true) normalizes the subset. - */ -TEST_F(compositionTest, subsetNoneMethodAndNormalizationFlow) { - 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()); - - fourdst::composition::Composition sub = comp.subset(std::vector{"H-1", "He-4"}, "none"); - // Not normalized: finalize without normalization should fail - EXPECT_FALSE(sub.finalize(false)); - // With normalization, it should succeed and scale to sum to 1 - EXPECT_TRUE(sub.finalize(true)); - double sum = sub.getMassFraction("H-1") + sub.getMassFraction("He-4"); - EXPECT_NEAR(sum, 1.0, 1e-12); - EXPECT_NEAR(sub.getMassFraction("H-1"), 0.5/(0.5+0.3), 1e-12); - EXPECT_NEAR(sub.getMassFraction("He-4"), 0.3/(0.5+0.3), 1e-12); -} - -/** - * @brief Tests copy constructor and assignment operator for independence. - * @details This test verifies that copies of a Composition object are independent of the original. - * @par What this test proves: - * - Copy constructor and assignment operator create independent objects. - */ -TEST_F(compositionTest, copyConstructorAndAssignmentIndependence) { - fourdst::config::Config::getInstance().loadConfig(EXAMPLE_FILENAME); - using fourdst::composition::Composition; - - Composition comp; comp.registerSymbol("H-1"); comp.registerSymbol("He-4"); - comp.setMassFraction("H-1", 0.6); comp.setMassFraction("He-4", 0.4); - ASSERT_TRUE(comp.finalize()); - - Composition copy(comp); // copy ctor - EXPECT_NEAR(copy.getMassFraction("H-1"), 0.6, 1e-12); - EXPECT_NEAR(copy.getMassFraction("He-4"), 0.4, 1e-12); - - Composition assigned; assigned = comp; // assignment - EXPECT_NEAR(assigned.getMassFraction("H-1"), 0.6, 1e-12); - EXPECT_NEAR(assigned.getMassFraction("He-4"), 0.4, 1e-12); - - // Modify original and ensure copies do not change - comp.setMassFraction("H-1", 0.7); comp.setMassFraction("He-4", 0.3); ASSERT_TRUE(comp.finalize()); - EXPECT_NEAR(copy.getMassFraction("H-1"), 0.6, 1e-12); - EXPECT_NEAR(assigned.getMassFraction("He-4"), 0.4, 1e-12); -} - -/** - * @brief Tests getComposition by Species and retrieval of all entries. - * @details This test verifies that getComposition works for both single Species and all entries, - * and that the returned values are correct. - * @par What this test proves: - * - getComposition works for both single Species and all entries. - */ -TEST_F(compositionTest, getCompositionBySpeciesAndAllEntries) { - fourdst::config::Config::getInstance().loadConfig(EXAMPLE_FILENAME); - using fourdst::composition::Composition; - - Composition comp; comp.registerSymbol("H-1"); comp.registerSymbol("He-4"); - comp.setMassFraction("H-1", 0.6); comp.setMassFraction("He-4", 0.4); - ASSERT_TRUE(comp.finalize()); - - auto pairBySpec = comp.getComposition(fourdst::atomic::H_1); - EXPECT_NEAR(pairBySpec.first.mass_fraction(), 0.6, 1e-12); - EXPECT_NEAR(pairBySpec.second.meanParticleMass, comp.getMeanParticleMass(), 1e-15); - - auto all = comp.getComposition(); - ASSERT_EQ(all.first.size(), 2u); - EXPECT_NEAR(all.first.at("H-1").mass_fraction(), 0.6, 1e-12); - EXPECT_NEAR(all.first.at("He-4").mass_fraction(), 0.4, 1e-12); - EXPECT_NEAR(all.second.meanParticleMass, comp.getMeanParticleMass(), 1e-15); -} - -/** - * @brief Tests iteration over composition and out-of-range index access. - * @details This test verifies that begin/end iteration covers all entries, - * and that accessing an out-of-range index throws an exception. - * @par What this test proves: - * - Iteration covers all entries. - * - Out-of-range index throws std::out_of_range. - */ -TEST_F(compositionTest, iterationBeginEndAndIndexOutOfRange) { - 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()); - - // Iterate and count entries - size_t count = 0; - for (auto it = comp.begin(); it != comp.end(); ++it) { - count++; - } - EXPECT_EQ(count, 3u); - - // Out-of-range access - EXPECT_THROW(static_cast(comp.getSpeciesAtIndex(100)), std::out_of_range); -} /** * @brief Tests inheritance from CompositionAbstract and overriding a getter. @@ -960,15 +266,12 @@ TEST_F(compositionTest, abstractBase) { } }; - 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()); + comp.setMolarAbundance("H-1", 0.6); comp.setMolarAbundance("He-4", 0.6); 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")); - } From 538fa78b37487d12e2def6569ee57015f1c61950 Mon Sep 17 00:00:00 2001 From: Emily Boudreaux Date: Fri, 7 Nov 2025 15:51:20 -0500 Subject: [PATCH 2/2] docs(version): version bump v1.9.1 -> v2.0.0 to reflect major API change --- Doxyfile | 2 +- meson.build | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 9761868..26fe876 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = fourdst::libcomposition # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = v1.9.1 +PROJECT_NUMBER = v2.0.0 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewers a diff --git a/meson.build b/meson.build index ada6f78..bb77b14 100644 --- a/meson.build +++ b/meson.build @@ -18,7 +18,7 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # *********************************************************************** # -project('libcomposition', 'cpp', version: 'v1.9.1', default_options: ['cpp_std=c++23'], meson_version: '>=1.5.0') +project('libcomposition', 'cpp', version: 'v2.0.0', default_options: ['cpp_std=c++23'], meson_version: '>=1.5.0') # Add default visibility for all C++ targets add_project_arguments('-fvisibility=default', language: 'cpp')