Files
libcomposition/src/composition/lib/composition.cpp
Emily Boudreaux 539c3b567e feat(composition): added uniform tools to get vector representation of mass fraction, number fraction, and molar abundance
this is useful for external tools that need to ensure uniformity in which species are at what index in a vector representation
2025-09-16 11:23:01 -04:00

951 lines
41 KiB
C++

/* ***********************************************************************
//
// Copyright (C) 2025 -- The 4D-STAR Collaboration
// File Author: Emily Boudreaux
// Last Modified: March 26, 2025
//
// 4DSSE is free software; you can use it and/or modify
// it under the terms and restrictions the GNU General Library Public
// License version 3 (GPLv3) as published by the Free Software Foundation.
//
// 4DSSE is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU Library General Public License for more details.
//
// You should have received a copy of the GNU Library General Public License
// along with this software; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
// *********************************************************************** */
#include "quill/LogMacros.h"
#include <stdexcept>
#include <unordered_map>
#include <vector>
#include <array>
#include <ranges>
#include <algorithm>
#include <utility>
#include "fourdst/composition/atomicSpecies.h"
#include "fourdst/composition/species.h"
#include "fourdst/composition/composition.h"
#include "fourdst/composition/exceptions/exceptions_composition.h"
namespace {
template<typename A, typename B>
std::vector<A> sortVectorBy(std::vector<A> toSort, const std::vector<B>& by) {
std::vector<std::size_t> indices(by.size());
for (size_t i = 0; i < indices.size(); i++) {
indices[i] = i;
}
std::ranges::sort(indices, [&](size_t a, size_t b) {
return by[a] < by[b];
});
std::vector<A> sorted;
sorted.reserve(indices.size());
for (const auto idx: indices) {
sorted.push_back(toSort[idx]);
}
return sorted;
}
}
namespace fourdst::composition {
CompositionEntry::CompositionEntry() :
m_symbol("H-1"),
m_isotope(fourdst::atomic::species.at("H-1")),
m_initialized(false) {} // Note: Default entry is uninitialized, must be explicitly set.
CompositionEntry::CompositionEntry(const std::string& symbol, const bool massFracMode) : m_symbol(symbol), m_isotope(fourdst::atomic::species.at(symbol)), m_massFracMode(massFracMode) {
setSpecies(symbol);
}
CompositionEntry::CompositionEntry(const CompositionEntry& entry) :
m_symbol(entry.m_symbol),
m_isotope(entry.m_isotope),
m_massFracMode(entry.m_massFracMode),
m_massFraction(entry.m_massFraction),
m_numberFraction(entry.m_numberFraction),
m_relAbundance(entry.m_relAbundance),
m_initialized(entry.m_initialized) {}
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 = fourdst::atomic::species.at(symbol);
m_initialized = true;
}
std::string CompositionEntry::symbol() const {
return m_symbol;
}
double CompositionEntry::mass_fraction() const {
if (!m_massFracMode) {
throw exceptions::CompositionModeError("Composition entry is in number fraction mode.");
}
return m_massFraction;
}
double CompositionEntry::mass_fraction(const double meanMolarMass) const {
if (m_massFracMode) {
return m_massFraction;
}
// Convert from number fraction to mass fraction using: X_i = n_i * A_i / <A>
// where m_relAbundance is n_i * A_i and meanMolarMass is <A>.
return m_relAbundance / meanMolarMass;
}
double CompositionEntry::number_fraction() const {
if (m_massFracMode) {
throw exceptions::CompositionModeError("Composition entry is in mass fraction mode.");
}
return m_numberFraction;
}
double CompositionEntry::number_fraction(const double totalMoles) const {
if (m_massFracMode) {
// Convert from mass fraction to number fraction using: n_i = (X_i / A_i) / sum(X_j / A_j)
// where m_relAbundance is X_i / A_i and totalMoles is sum(X_j / A_j).
return m_relAbundance / totalMoles;
}
return m_numberFraction;
}
double CompositionEntry::rel_abundance() const {
return m_relAbundance;
}
fourdst::atomic::Species CompositionEntry::isotope() const {
return m_isotope;
}
void CompositionEntry::setMassFraction(const double mass_fraction) {
if (!m_massFracMode) {
throw exceptions::CompositionModeError("Composition entry is in number fraction mode.");
}
m_massFraction = mass_fraction;
m_relAbundance = m_massFraction / m_isotope.mass();
}
void CompositionEntry::setNumberFraction(const double number_fraction) {
if (m_massFracMode) {
throw exceptions::CompositionModeError("Composition entry is in mass fraction mode.");
}
m_numberFraction = number_fraction;
m_relAbundance = m_numberFraction * m_isotope.mass();
}
bool CompositionEntry::setMassFracMode(const double meanParticleMass) {
if (m_massFracMode) {
return false;
}
m_massFracMode = true;
m_massFraction = m_relAbundance / meanParticleMass;
return true;
}
bool CompositionEntry::setNumberFracMode(const double specificNumberDensity) {
if (!m_massFracMode) {
return false;
}
m_massFracMode = false;
m_numberFraction = m_relAbundance / specificNumberDensity;
return true;
}
bool CompositionEntry::getMassFracMode() const {
return m_massFracMode;
}
Composition::Composition(const std::vector<std::string>& symbols) {
for (const auto& symbol : symbols) {
registerSymbol(symbol);
}
}
Composition::Composition(const std::set<std::string>& symbols) {
for (const auto& symbol : symbols) {
registerSymbol(symbol);
}
}
Composition::Composition(const std::vector<std::string>& symbols, const std::vector<double>& fractions, const bool massFracMode) : m_massFracMode(massFracMode) {
if (symbols.size() != fractions.size()) {
LOG_CRITICAL(m_logger, "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.");
}
validateComposition(fractions);
for (const auto &symbol : symbols) {
registerSymbol(symbol);
}
for (size_t i = 0; i < symbols.size(); ++i) {
if (m_massFracMode) {
setMassFraction(symbols[i], fractions[i]);
} else {
setNumberFraction(symbols[i], fractions[i]);
}
}
finalize();
}
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::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;
// note: m_config remains bound to the same singleton, so we skip it
}
return *this;
}
void Composition::registerSymbol(const std::string& symbol, bool massFracMode) {
if (!isValidSymbol(symbol)) {
LOG_ERROR(m_logger, "Invalid symbol: {}", symbol);
throw exceptions::InvalidSymbolError("Invalid symbol: " + symbol);
}
// If no symbols have been registered allow mode to be set
if (m_registeredSymbols.empty()) {
m_massFracMode = massFracMode;
} else {
if (m_massFracMode != massFracMode) {
LOG_ERROR(m_logger, "Composition is in mass fraction mode. Cannot register symbol ({}) in number fraction mode.", symbol);
throw exceptions::CompositionModeError("Composition is in mass fraction mode. Cannot register symbol (" + symbol + ") in number fraction mode.");
}
}
if (m_registeredSymbols.contains(symbol)) {
LOG_WARNING(m_logger, "Symbol {} is already registered.", symbol);
return;
}
m_registeredSymbols.insert(symbol);
const CompositionEntry entry(symbol, m_massFracMode);
m_compositions[symbol] = entry;
LOG_TRACE_L3(m_logger, "Registered symbol: {}", symbol);
}
void Composition::registerSymbol(const std::vector<std::string>& symbols, bool massFracMode) {
for (const auto& symbol : symbols) {
registerSymbol(symbol, massFracMode);
}
}
void Composition::registerSpecies(const fourdst::atomic::Species &species, bool massFracMode) {
registerSymbol(std::string(species.name()));
}
void Composition::registerSpecies(const std::vector<fourdst::atomic::Species> &species, bool massFracMode) {
for (const auto& s : species) {
registerSpecies(s, massFracMode);
}
}
std::set<std::string> Composition::getRegisteredSymbols() const {
return m_registeredSymbols;
}
std::set<fourdst::atomic::Species> Composition::getRegisteredSpecies() const {
std::set<fourdst::atomic::Species> result;
for (const auto& entry : m_compositions | std::views::values) {
result.insert(entry.isotope());
}
return result;
}
void Composition::validateComposition(const std::vector<double>& fractions) const {
if (!isValidComposition(fractions)) {
LOG_ERROR(m_logger, "Invalid composition.");
throw exceptions::InvalidCompositionError("Invalid composition.");
}
}
bool Composition::isValidComposition(const std::vector<double>& fractions) const {
double sum = 0.0;
for (const auto& fraction : fractions) {
sum += fraction;
}
if (sum < 0.999999 || sum > 1.000001) {
LOG_ERROR(m_logger, "The sum of fractions must be equal to 1 (expected 1, got {}).", sum);
return false;
}
return true;
}
bool Composition::isValidSymbol(const std::string& symbol) {
return fourdst::atomic::species.contains(symbol);
}
double Composition::setMassFraction(const std::string& symbol, const double& mass_fraction) {
if (!m_registeredSymbols.contains(symbol)) {
LOG_ERROR(m_logger, "Symbol {} is not registered.", symbol);
throw exceptions::UnregisteredSymbolError("Symbol (" + symbol + ") is not registered.");
}
if (!m_massFracMode) {
LOG_ERROR(m_logger, "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(m_logger, "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 for symbol " + symbol + ". Currently it is " + std::to_string(mass_fraction) + ".");
}
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<double> Composition::setMassFraction(const std::vector<std::string>& symbols, const std::vector<double>& mass_fractions) {
if (symbols.size() != mass_fractions.size()) {
LOG_ERROR(m_logger, "The number of symbols and mass fractions must be equal (currently {} symbols and {} mass fractions).", symbols.size(), mass_fractions.size());
throw exceptions::InvalidCompositionError("The number of symbols and mass fractions must be equal (currently " + std::to_string(symbols.size()) + " symbols and " + std::to_string(mass_fractions.size()) + " mass fractions).");
}
std::vector<double> 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::setMassFraction(const fourdst::atomic::Species &species, const double &mass_fraction) {
return setMassFraction(std::string(species.name()), mass_fraction);
}
std::vector<double> Composition::setMassFraction(const std::vector<fourdst::atomic::Species> &species,
const std::vector<double> &mass_fractions) {
std::vector<double> old_mass_fractions;
old_mass_fractions.reserve(species.size());
for (const auto& spec : species) {
old_mass_fractions.push_back(setMassFraction(spec, mass_fractions[&spec - &species[0]]));
}
return old_mass_fractions;
}
double Composition::setNumberFraction(const std::string& symbol, const double& number_fraction) {
if (!m_registeredSymbols.contains(symbol)) {
LOG_ERROR(m_logger, "Symbol {} is not registered.", symbol);
throw exceptions::UnregisteredSymbolError("Symbol (" + symbol + ") is not registered.");
}
if (m_massFracMode) {
LOG_ERROR(m_logger, "Composition is in mass fraction mode, should be in number fraction mode to call setNumberFraction. Hint: The mode can be switched by first finalizing and then calling setCompositionMode(false).");
throw exceptions::CompositionModeError("Composition is in mass fraction mode, should be in number fraction mode to call setNumberFraction. Hint: The mode can be switched by first finalizing and then calling setCompositionMode(false).");
}
if (number_fraction < 0.0 || number_fraction > 1.0) {
LOG_ERROR(m_logger, "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 for symbol " + symbol + ". Currently it is " + std::to_string(number_fraction) + ".");
}
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<double> Composition::setNumberFraction(const std::vector<std::string>& symbols, const std::vector<double>& number_fractions) {
if (symbols.size() != number_fractions.size()) {
LOG_ERROR(m_logger, "The number of symbols and number fractions must be equal. (Currently {} symbols and {} number fractions).", symbols.size(), number_fractions.size());
throw exceptions::InvalidCompositionError("The number of symbols and number fractions must be equal. (Currently " + std::to_string(symbols.size()) + " symbols and " + std::to_string(number_fractions.size()) + " number fractions).");
}
std::vector<double> 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::setNumberFraction(const fourdst::atomic::Species &species, const double &number_fraction) {
return setNumberFraction(std::string(species.name()), number_fraction);
}
std::vector<double> Composition::setNumberFraction(const std::vector<fourdst::atomic::Species> &species,
const std::vector<double> &number_fractions) {
std::vector<double> old_number_fractions;
old_number_fractions.reserve(species.size());
for (const auto& spec : species) {
old_number_fractions.push_back(setNumberFraction(spec, number_fractions[&spec - &species[0]]));
}
return old_number_fractions;
}
bool Composition::finalize(const bool norm) {
bool finalized = false;
if (m_massFracMode) {
finalized = finalizeMassFracMode(norm);
} else {
finalized = finalizeNumberFracMode(norm);
}
if (finalized) {
m_finalized = true;
}
return finalized;
}
bool Composition::finalizeMassFracMode(bool norm) {
std::vector<double> mass_fractions;
mass_fractions.reserve(m_compositions.size());
for (const auto &entry: m_compositions | std::views::values) {
mass_fractions.push_back(entry.mass_fraction());
}
if (norm) {
double sum = 0.0;
for (const auto& mass_fraction : mass_fractions) {
sum += mass_fraction;
}
for (double & mass_fraction : mass_fractions) {
mass_fraction /= sum;
}
for (auto& [symbol, entry] : m_compositions) {
setMassFraction(symbol, entry.mass_fraction() / sum);
}
}
try {
validateComposition(mass_fractions);
} catch ([[maybe_unused]] const exceptions::InvalidCompositionError& e) {
double massSum = 0.0;
for (const auto &entry: m_compositions | std::views::values) {
massSum += entry.mass_fraction();
}
LOG_ERROR(m_logger, "Composition is invalid (Total mass {}).", massSum);
m_finalized = false;
return false;
}
// After validation, calculate the specific number density (total moles per unit mass).
for (const auto &entry: m_compositions | std::views::values) {
m_specificNumberDensity += entry.rel_abundance();
}
m_meanParticleMass = 1.0/m_specificNumberDensity;
return true;
}
bool Composition::finalizeNumberFracMode(bool norm) {
std::vector<double> number_fractions;
number_fractions.reserve(m_compositions.size());
for (const auto &entry: m_compositions | std::views::values) {
number_fractions.push_back(entry.number_fraction());
}
if (norm) {
double sum = 0.0;
for (const auto& number_fraction : number_fractions) {
sum += number_fraction;
}
for (auto& [symbol, entry] : m_compositions) {
setNumberFraction(symbol, entry.number_fraction() / sum);
}
}
try {
validateComposition(number_fractions);
} catch ([[maybe_unused]] const std::runtime_error& e) {
double numberSum = 0.0;
for (const auto &entry: m_compositions | std::views::values) {
numberSum += entry.number_fraction();
}
LOG_ERROR(m_logger, "Composition is invalid (Total number {}).", numberSum);
m_finalized = false;
return false;
}
// After validation, calculate the mean particle mass.
for (const auto &entry: m_compositions | std::views::values) {
m_meanParticleMass += entry.rel_abundance();
}
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(m_logger, "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(m_logger, "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<std::string> 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);
}
mixedComposition.finalize();
return mixedComposition;
}
double Composition::getMassFraction(const std::string& symbol) const {
if (!m_finalized) {
LOG_ERROR(m_logger, "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(m_logger, "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();
} else {
return m_compositions.at(symbol).mass_fraction(m_meanParticleMass);
}
}
double Composition::getMassFraction(const fourdst::atomic::Species &species) const {
return getMassFraction(std::string(species.name()));
}
std::unordered_map<std::string, double> Composition::getMassFraction() const {
std::unordered_map<std::string, double> mass_fractions;
for (const auto &symbol: m_compositions | std::views::keys) {
mass_fractions[symbol] = getMassFraction(symbol);
}
return mass_fractions;
}
double Composition::getNumberFraction(const std::string& symbol) const {
if (!m_finalized) {
LOG_ERROR(m_logger, "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(m_logger, "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();
} else {
return m_compositions.at(symbol).number_fraction(m_specificNumberDensity);
}
}
double Composition::getNumberFraction(const fourdst::atomic::Species &species) const {
return getNumberFraction(std::string(species.name()));
}
std::unordered_map<std::string, double> Composition::getNumberFraction() const {
std::unordered_map<std::string, double> number_fractions;
for (const auto &symbol: m_compositions | std::views::keys) {
number_fractions[symbol] = getNumberFraction(symbol);
}
return number_fractions;
}
double Composition::getMolarAbundance(const std::string &symbol) const {
if (!m_finalized) {
LOG_ERROR(m_logger, "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(m_logger, "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();
}
double Composition::getMolarAbundance(const fourdst::atomic::Species &species) const {
return getMolarAbundance(std::string(species.name()));
}
std::pair<CompositionEntry, GlobalComposition> Composition::getComposition(const std::string& symbol) const {
if (!m_finalized) {
LOG_ERROR(m_logger, "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(m_logger, "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<CompositionEntry, GlobalComposition> Composition::getComposition(
const fourdst::atomic::Species &species) const {
return getComposition(std::string(species.name()));
}
std::pair<std::unordered_map<std::string, CompositionEntry>, GlobalComposition> Composition::getComposition() const {
if (!m_finalized) {
LOG_ERROR(m_logger, "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}};
}
double Composition::getMeanParticleMass() const {
if (!m_finalized) {
LOG_ERROR(m_logger, "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(m_logger, "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().");
}
double zSum = 0.0;
// Loop through all registered species in the composition.
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();
}
// Calculate mean atomic number <Z> = <A> * sum(X_i * Z_i / A_i)
const double mean_A = m_meanParticleMass * zSum;
return mean_A;
}
Composition Composition::subset(const std::vector<std::string>& symbols, const std::string& method) const {
const std::array<std::string, 2> methods = {"norm", "none"};
if (std::ranges::find(methods, method) == methods.end()) {
const std::string errorMessage = "Invalid method: " + method + ". Valid methods are 'norm' and 'none'.";
LOG_ERROR(m_logger, "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(m_logger, "Symbol {} is not in the composition.", symbol);
throw exceptions::UnregisteredSymbolError("Symbol " + symbol + " is not in the composition.");
} else {
subsetComposition.registerSymbol(symbol);
}
subsetComposition.setMassFraction(symbol, m_compositions.at(symbol).mass_fraction());
}
if (method == "norm") {
const bool isNorm = subsetComposition.finalize(true);
if (!isNorm) {
LOG_ERROR(m_logger, "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(m_logger, "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 = true;
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(m_logger, "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(bool harsh) const {
if (!m_finalized) {
LOG_ERROR(m_logger, "Composition has not been finalized. Hint: Consider running .finalize().");
throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize().");
}
CanonicalComposition canonicalComposition;
const std::array<std::string, 7> canonicalH = {
"H-1", "H-2", "H-3", "H-4", "H-5", "H-6", "H-7"
};
const std::array<std::string, 8> 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)) {
canonicalComposition.X += getMassFraction(symbol);
}
}
for (const auto& symbol : canonicalHe) {
if (hasSymbol(symbol)) {
canonicalComposition.Y += getMassFraction(symbol);
}
}
for (const auto& symbol : getRegisteredSymbols()) {
const bool isHSymbol = std::ranges::find(canonicalH, symbol) != std::end(canonicalH);
const bool isHeSymbol = std::ranges::find(canonicalHe, symbol) != std::end(canonicalHe);
if (isHSymbol || isHeSymbol) {
continue; // Skip canonical H and He symbols
}
canonicalComposition.Z += getMassFraction(symbol);
}
const double Z = 1.0 - (canonicalComposition.X + canonicalComposition.Y);
if (std::abs(Z - canonicalComposition.Z) > 1e-6) {
if (!harsh) {
LOG_WARNING(m_logger, "Validation composition Z (X-Y = {}) is different than canonical composition Z ({}) (∑a_i where a_i != H/He).", Z, canonicalComposition.Z);
}
else {
LOG_ERROR(m_logger, "Validation composition Z (X-Y = {}) is different than canonical composition Z ({}) (∑a_i where a_i != H/He).", Z, canonicalComposition.Z);
throw std::runtime_error("Validation composition Z (X-Y = " + std::to_string(Z) + ") is different than canonical composition Z (" + std::to_string(canonicalComposition.Z) + ") (∑a_i where a_i != H/He).");
}
}
return canonicalComposition;
}
std::vector<double> Composition::getMassFractionVector() const {
if (!m_finalized) {
LOG_ERROR(m_logger, "Composition has not been finalized. Hint: Consider running .finalize().");
throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize().");
}
std::vector<double> massFractionVector;
std::vector<double> speciesMass;
massFractionVector.reserve(m_compositions.size());
speciesMass.reserve(m_compositions.size());
for (const auto &entry: m_compositions | std::views::values) {
massFractionVector.push_back(entry.mass_fraction());
speciesMass.push_back(entry.isotope().mass());
}
return sortVectorBy(massFractionVector, speciesMass);
}
std::vector<double> Composition::getNumberFractionVector() const {
if (!m_finalized) {
LOG_ERROR(m_logger, "Composition has not been finalized. Hint: Consider running .finalize().");
throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize().");
}
std::vector<double> numberFractionVector;
std::vector<double> speciesMass;
numberFractionVector.reserve(m_compositions.size());
speciesMass.reserve(m_compositions.size());
for (const auto &entry: m_compositions | std::views::values) {
numberFractionVector.push_back(entry.number_fraction());
speciesMass.push_back(entry.isotope().mass());
}
return sortVectorBy(numberFractionVector, speciesMass);
}
std::vector<double> Composition::getMolarAbundanceVector() const {
if (!m_finalized) {
LOG_ERROR(m_logger, "Composition has not been finalized. Hint: Consider running .finalize().");
throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize().");
}
std::vector<double> molarAbundanceVector;
std::vector<double> speciesMass;
molarAbundanceVector.reserve(m_compositions.size());
speciesMass.reserve(m_compositions.size());
for (const auto &entry: m_compositions | std::views::values) {
molarAbundanceVector.push_back(getMolarAbundance(entry.isotope()));
speciesMass.push_back(entry.isotope().mass());
}
return sortVectorBy(molarAbundanceVector, speciesMass);
}
size_t Composition::getSpeciesIndex(const std::string &symbol) const {
if (!m_finalized) {
LOG_ERROR(m_logger, "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(m_logger, "Symbol {} is not in the composition.", symbol);
throw exceptions::UnregisteredSymbolError("Symbol " + symbol + " is not in the composition.");
}
std::vector<std::string> symbols;
std::vector<double> 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<std::string> sortedSymbols = sortVectorBy(symbols, speciesMass);
return std::distance(sortedSymbols.begin(), std::ranges::find(sortedSymbols, symbol));
}
size_t Composition::getSpeciesIndex(const atomic::Species &species) const {
if (!m_finalized) {
LOG_ERROR(m_logger, "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<std::string>(species.name()))) {
LOG_ERROR(m_logger, "Species {} is not in the composition.", species.name());
throw exceptions::UnregisteredSymbolError("Species " + std::string(species.name()) + " is not in the composition.");
}
std::vector<atomic::Species> speciesVector;
std::vector<double> speciesMass;
speciesVector.reserve(m_compositions.size());
speciesMass.reserve(m_compositions.size());
for (const auto &entry: m_compositions | std::views::values) {
speciesVector.emplace_back(entry.isotope());
speciesMass.push_back(entry.isotope().mass());
}
std::vector<atomic::Species> sortedSpecies = sortVectorBy(speciesVector, speciesMass);
return std::distance(sortedSpecies.begin(), std::ranges::find(sortedSpecies, species));
}
atomic::Species Composition::getSpeciesAtIndex(size_t index) const {
if (!m_finalized) {
LOG_ERROR(m_logger, "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(m_logger, "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 (index < 0) {
LOG_ERROR(m_logger, "Index {} is negative. Cannot get species at negative index.", index);
throw std::out_of_range("Index " + std::to_string(index) + " is negative. Cannot get species at negative index.");
}
std::vector<atomic::Species> speciesVector;
std::vector<double> speciesMass;
speciesVector.reserve(m_compositions.size());
speciesMass.reserve(m_compositions.size());
for (const auto &entry: m_compositions | std::views::values) {
speciesVector.emplace_back(entry.isotope());
speciesMass.push_back(entry.isotope().mass());
}
std::vector<atomic::Species> sortedSymbols = sortVectorBy(speciesVector, speciesMass);
return sortedSymbols.at(index);
}
bool Composition::hasSymbol(const std::string& symbol) const {
return m_compositions.contains(symbol);
}
bool Composition::contains(const fourdst::atomic::Species &isotope) const {
// Check if the isotope's symbol is in the composition
if (!m_finalized) {
LOG_ERROR(m_logger, "Composition has not been finalized. Hint: Consider running .finalize().");
throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize().");
}
const auto symbol = static_cast<std::string>(isotope.name());
if (m_compositions.contains(symbol)) {
return true;
}
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 << " : m_frac = " << entry.mass_fraction() << ">";
return os;
}
std::ostream& operator<<(std::ostream& os, const Composition& composition) {
os << "Composition(finalized: " << (composition.m_finalized ? "true" : "false") << ", " ;
size_t count = 0;
for (const auto &entry: composition.m_compositions | std::views::values) {
os << entry;
if (count < composition.m_compositions.size() - 1) {
os << ", ";
}
}
os << ")";
return os;
}
} // namespace fourdst::composition