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') 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")); - }