2025-07-10 09:36:05 -04:00
|
|
|
#include "gridfire/engine/procedures/priming.h"
|
2025-11-10 10:40:03 -05:00
|
|
|
|
|
|
|
|
#include "fourdst/atomic/species.h"
|
|
|
|
|
#include "fourdst/composition/utils.h"
|
2025-07-10 09:36:05 -04:00
|
|
|
#include "gridfire/engine/views/engine_priming.h"
|
2025-07-14 14:50:49 -04:00
|
|
|
#include "gridfire/engine/procedures/construction.h"
|
2025-07-10 09:36:05 -04:00
|
|
|
#include "gridfire/solver/solver.h"
|
|
|
|
|
|
|
|
|
|
#include "gridfire/engine/engine_abstract.h"
|
|
|
|
|
#include "gridfire/network.h"
|
|
|
|
|
|
2025-07-14 14:50:49 -04:00
|
|
|
#include "fourdst/logging/logging.h"
|
|
|
|
|
#include "quill/Logger.h"
|
|
|
|
|
#include "quill/LogMacros.h"
|
|
|
|
|
|
2025-10-14 13:37:48 -04:00
|
|
|
namespace {
|
|
|
|
|
bool isReactionIgnorable(
|
|
|
|
|
const gridfire::reaction::Reaction& reaction,
|
|
|
|
|
const std::optional<std::vector<gridfire::reaction::ReactionType>>& reactionsTypesToIgnore
|
|
|
|
|
) {
|
|
|
|
|
if (reactionsTypesToIgnore.has_value()) {
|
|
|
|
|
for (const auto& type : reactionsTypesToIgnore.value()) {
|
|
|
|
|
if (reaction.type() == type) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-10 09:36:05 -04:00
|
|
|
namespace gridfire {
|
2025-07-14 14:50:49 -04:00
|
|
|
using fourdst::composition::Composition;
|
|
|
|
|
using fourdst::atomic::Species;
|
|
|
|
|
|
|
|
|
|
const reaction::Reaction* findDominantCreationChannel (
|
|
|
|
|
const DynamicEngine& engine,
|
|
|
|
|
const Species& species,
|
2025-10-10 09:12:40 -04:00
|
|
|
const Composition &comp,
|
2025-07-14 14:50:49 -04:00
|
|
|
const double T9,
|
2025-10-14 13:37:48 -04:00
|
|
|
const double rho,
|
|
|
|
|
const std::optional<std::vector<reaction::ReactionType>> &reactionsTypesToIgnore
|
2025-07-14 14:50:49 -04:00
|
|
|
) {
|
|
|
|
|
const reaction::Reaction* dominateReaction = nullptr;
|
|
|
|
|
double maxFlow = -1.0;
|
|
|
|
|
for (const auto& reaction : engine.getNetworkReactions()) {
|
2025-10-14 13:37:48 -04:00
|
|
|
if (isReactionIgnorable(*reaction, reactionsTypesToIgnore)) continue;
|
|
|
|
|
|
2025-08-14 13:33:46 -04:00
|
|
|
if (reaction->contains(species) && reaction->stoichiometry(species) > 0) {
|
2025-10-07 15:16:03 -04:00
|
|
|
const double flow = engine.calculateMolarReactionFlow(*reaction, comp, T9, rho);
|
2025-07-14 14:50:49 -04:00
|
|
|
if (flow > maxFlow) {
|
|
|
|
|
maxFlow = flow;
|
2025-08-14 13:33:46 -04:00
|
|
|
dominateReaction = reaction.get();
|
2025-07-14 14:50:49 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return dominateReaction;
|
|
|
|
|
}
|
2025-07-10 09:36:05 -04:00
|
|
|
|
2025-10-22 09:54:10 -04:00
|
|
|
|
|
|
|
|
|
2025-09-19 15:14:46 -04:00
|
|
|
/**
|
|
|
|
|
* @brief Primes absent species in the network to their equilibrium abundances using a robust, two-stage approach.
|
|
|
|
|
*
|
|
|
|
|
* @details This function implements a robust network priming algorithm that avoids the pitfalls of
|
|
|
|
|
* sequential, one-by-one priming. The previous, brittle method could allow an early priming
|
|
|
|
|
* reaction to consume all of a shared reactant, starving later reactions. This new, two-stage
|
|
|
|
|
* method ensures that all priming reactions are considered collectively, competing for the
|
|
|
|
|
* same limited pool of initial reactants in a physically consistent manner.
|
|
|
|
|
*
|
|
|
|
|
* The algorithm proceeds in three main stages:
|
|
|
|
|
* 1. **Calculation Stage:** It first loops through all species that need priming. For each one,
|
|
|
|
|
* it calculates its theoretical equilibrium mass fraction and identifies the dominant
|
|
|
|
|
* creation channel. Crucially, it *does not* modify any abundances at this stage. Instead,
|
|
|
|
|
* it stores these calculations as a list of "mass transfer requests".
|
|
|
|
|
*
|
|
|
|
|
* 2. **Collective Scaling Stage:** It then processes the full list of requests to determine the
|
|
|
|
|
* total "debit" required from each reactant. By comparing these total debits to the
|
|
|
|
|
* initially available mass of each reactant, it calculates a single, global `scalingFactor`.
|
|
|
|
|
* If any reactant is overdrawn, this factor will be less than 1.0, ensuring that no
|
|
|
|
|
* reactant's abundance can go negative.
|
|
|
|
|
*
|
|
|
|
|
* 3. **Application Stage:** Finally, it loops through the requests again, applying the mass
|
|
|
|
|
* transfers. Each calculated equilibrium mass fraction and corresponding reactant debit is
|
|
|
|
|
* multiplied by the global `scalingFactor` before being applied to the final composition.
|
|
|
|
|
* This ensures that if resources are limited, all primed species are scaled down proportionally.
|
|
|
|
|
*
|
|
|
|
|
* @param netIn Input network data containing initial composition, temperature, and density.
|
|
|
|
|
* @param engine DynamicEngine used to build and evaluate the reaction network.
|
2025-10-14 13:37:48 -04:00
|
|
|
* @param ignoredReactionTypes Types of reactions to ignore during priming (e.g., weak reactions).
|
2025-09-19 15:14:46 -04:00
|
|
|
* @return PrimingReport encapsulating the results of the priming operation, including the new
|
|
|
|
|
* robustly primed composition.
|
|
|
|
|
*/
|
2025-10-14 13:37:48 -04:00
|
|
|
PrimingReport primeNetwork(
|
|
|
|
|
const NetIn& netIn,
|
2025-10-22 09:54:10 -04:00
|
|
|
GraphEngine& engine,
|
2025-10-14 13:37:48 -04:00
|
|
|
const std::optional<std::vector<reaction::ReactionType>>& ignoredReactionTypes
|
|
|
|
|
) {
|
2025-11-10 10:40:03 -05:00
|
|
|
auto logger = LogManager::getInstance().getLogger("log");
|
2025-07-14 14:50:49 -04:00
|
|
|
|
2025-09-19 15:14:46 -04:00
|
|
|
// --- Initial Setup ---
|
2025-11-10 10:40:03 -05:00
|
|
|
// Identify all species with zero initial abundance that need to be primed.
|
2025-07-10 09:36:05 -04:00
|
|
|
std::vector<Species> speciesToPrime;
|
2025-11-10 10:40:03 -05:00
|
|
|
for (const auto &[sp, y]: netIn.composition) {
|
|
|
|
|
if (y == 0.0) {
|
|
|
|
|
speciesToPrime.push_back(sp);
|
2025-07-10 09:36:05 -04:00
|
|
|
}
|
|
|
|
|
}
|
2025-10-14 13:37:48 -04:00
|
|
|
|
2025-11-10 10:40:03 -05:00
|
|
|
// Sort priming species by mass number, lightest to heaviest.
|
2025-10-14 13:37:48 -04:00
|
|
|
std::ranges::sort(speciesToPrime, [](const Species& a, const Species& b) {
|
|
|
|
|
return a.mass() < b.mass();
|
|
|
|
|
});
|
|
|
|
|
|
2025-07-22 12:48:24 -04:00
|
|
|
LOG_DEBUG(logger, "Priming {} species in the network.", speciesToPrime.size());
|
2025-07-10 09:36:05 -04:00
|
|
|
|
2025-09-19 15:14:46 -04:00
|
|
|
// If no species need priming, return immediately.
|
2025-07-14 14:50:49 -04:00
|
|
|
PrimingReport report;
|
2025-07-10 09:36:05 -04:00
|
|
|
if (speciesToPrime.empty()) {
|
2025-07-14 14:50:49 -04:00
|
|
|
report.primedComposition = netIn.composition;
|
|
|
|
|
report.success = true;
|
|
|
|
|
report.status = PrimingReportStatus::NO_SPECIES_TO_PRIME;
|
|
|
|
|
return report;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const double T9 = netIn.temperature / 1e9;
|
|
|
|
|
const double rho = netIn.density;
|
2025-07-22 12:48:24 -04:00
|
|
|
const auto initialReactionSet = engine.getNetworkReactions();
|
2025-11-10 10:40:03 -05:00
|
|
|
|
2025-07-14 14:50:49 -04:00
|
|
|
report.status = PrimingReportStatus::FULL_SUCCESS;
|
|
|
|
|
report.success = true;
|
|
|
|
|
|
2025-11-10 10:40:03 -05:00
|
|
|
// Build full set of species
|
|
|
|
|
std::set<Species> allSpecies;
|
|
|
|
|
for (const auto &sp: netIn.composition | std::views::keys) {
|
|
|
|
|
allSpecies.insert(sp);
|
2025-07-14 14:50:49 -04:00
|
|
|
}
|
2025-11-10 10:40:03 -05:00
|
|
|
for (const auto& sp : speciesToPrime) {
|
|
|
|
|
allSpecies.insert(sp);
|
2025-07-10 09:36:05 -04:00
|
|
|
}
|
|
|
|
|
|
2025-09-19 15:14:46 -04:00
|
|
|
// Rebuild the engine with the full network to ensure all possible creation channels are available.
|
2025-07-14 14:50:49 -04:00
|
|
|
engine.rebuild(netIn.composition, NetworkBuildDepth::Full);
|
|
|
|
|
|
2025-11-10 10:40:03 -05:00
|
|
|
// Initialize mutable molar abundances for all species
|
|
|
|
|
std::map<Species, double> molarAbundances;
|
|
|
|
|
for (const auto& sp : allSpecies) {
|
|
|
|
|
molarAbundances[sp] = netIn.composition.contains(sp) ? netIn.composition.getMolarAbundance(sp) : 0.0;
|
|
|
|
|
}
|
2025-09-19 15:14:46 -04:00
|
|
|
|
2025-11-10 10:40:03 -05:00
|
|
|
// --- Prime Each Species ---
|
|
|
|
|
// Since molar abundances are independent, we can directly calculate and apply changes
|
|
|
|
|
std::unordered_map<Species, double> totalMolarAbundanceChanges;
|
2025-07-14 14:50:49 -04:00
|
|
|
|
2025-09-19 15:14:46 -04:00
|
|
|
for (const auto& primingSpecies : speciesToPrime) {
|
|
|
|
|
// Create a temporary composition reflecting the current state for rate calculations.
|
2025-11-10 10:40:03 -05:00
|
|
|
Composition tempComp(allSpecies);
|
|
|
|
|
for (const auto& [sp, abundance] : molarAbundances) {
|
|
|
|
|
tempComp.setMolarAbundance(sp, abundance);
|
2025-10-14 13:37:48 -04:00
|
|
|
}
|
2025-07-14 14:50:49 -04:00
|
|
|
|
2025-07-10 09:36:05 -04:00
|
|
|
NetworkPrimingEngineView primer(primingSpecies, engine);
|
2025-07-14 14:50:49 -04:00
|
|
|
if (primer.getNetworkReactions().size() == 0) {
|
|
|
|
|
LOG_ERROR(logger, "No priming reactions found for species {}.", primingSpecies.name());
|
|
|
|
|
report.success = false;
|
|
|
|
|
report.status = PrimingReportStatus::FAILED_TO_FIND_PRIMING_REACTIONS;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-14 13:37:48 -04:00
|
|
|
const double destructionRateConstant = calculateDestructionRateConstant(
|
|
|
|
|
primer,
|
|
|
|
|
primingSpecies,
|
|
|
|
|
tempComp,
|
|
|
|
|
T9,
|
|
|
|
|
rho,
|
|
|
|
|
ignoredReactionTypes
|
|
|
|
|
);
|
2025-07-10 09:36:05 -04:00
|
|
|
|
2025-11-10 10:40:03 -05:00
|
|
|
double equilibriumMolarAbundance = 0.0;
|
|
|
|
|
std::vector<Species> reactants;
|
|
|
|
|
|
2025-07-14 14:50:49 -04:00
|
|
|
if (destructionRateConstant > 1e-99) {
|
2025-10-14 13:37:48 -04:00
|
|
|
const double creationRate = calculateCreationRate(
|
|
|
|
|
primer,
|
|
|
|
|
primingSpecies,
|
|
|
|
|
tempComp,
|
|
|
|
|
T9,
|
|
|
|
|
rho,
|
|
|
|
|
ignoredReactionTypes
|
|
|
|
|
);
|
|
|
|
|
|
2025-11-10 10:40:03 -05:00
|
|
|
// Equilibrium: creation rate = destruction rate constant * molar abundance
|
|
|
|
|
equilibriumMolarAbundance = creationRate / destructionRateConstant;
|
|
|
|
|
|
|
|
|
|
if (std::isnan(equilibriumMolarAbundance)) {
|
|
|
|
|
equilibriumMolarAbundance = 0.0;
|
|
|
|
|
}
|
2025-07-14 14:50:49 -04:00
|
|
|
|
2025-10-14 13:37:48 -04:00
|
|
|
if (const reaction::Reaction* dominantChannel = findDominantCreationChannel(
|
|
|
|
|
primer,
|
|
|
|
|
primingSpecies,
|
|
|
|
|
tempComp,
|
|
|
|
|
T9,
|
|
|
|
|
rho,
|
|
|
|
|
ignoredReactionTypes)
|
|
|
|
|
) {
|
2025-11-10 10:40:03 -05:00
|
|
|
reactants = dominantChannel->reactants();
|
2025-09-19 15:14:46 -04:00
|
|
|
} else {
|
2025-10-24 11:17:22 -04:00
|
|
|
LOG_TRACE_L1(logger, "Failed to find dominant creation channel for {}.", primingSpecies.name());
|
2025-09-19 15:14:46 -04:00
|
|
|
report.status = PrimingReportStatus::FAILED_TO_FIND_CREATION_CHANNEL;
|
2025-11-10 10:40:03 -05:00
|
|
|
reactants.clear(); // Use fallback
|
2025-09-19 15:14:46 -04:00
|
|
|
}
|
|
|
|
|
} else {
|
2025-10-24 11:17:22 -04:00
|
|
|
LOG_TRACE_L2(logger, "No destruction channel found for {}. Using fallback abundance.", primingSpecies.name());
|
2025-11-10 10:40:03 -05:00
|
|
|
// For species with no destruction, use a tiny fallback abundance
|
|
|
|
|
equilibriumMolarAbundance = 1e-40;
|
2025-09-19 15:14:46 -04:00
|
|
|
}
|
|
|
|
|
|
2025-11-10 10:40:03 -05:00
|
|
|
// Add the equilibrium molar abundance to the primed species
|
|
|
|
|
molarAbundances.at(primingSpecies) += equilibriumMolarAbundance;
|
|
|
|
|
totalMolarAbundanceChanges[primingSpecies] += equilibriumMolarAbundance;
|
2025-09-19 15:14:46 -04:00
|
|
|
|
2025-11-10 10:40:03 -05:00
|
|
|
// Subtract from reactants proportionally to their stoichiometry
|
2025-09-19 15:14:46 -04:00
|
|
|
if (!reactants.empty()) {
|
2025-11-10 10:40:03 -05:00
|
|
|
const double totalStoichiometry = static_cast<double>(reactants.size());
|
|
|
|
|
const double abundancePerReactant = equilibriumMolarAbundance / totalStoichiometry;
|
2025-07-14 14:50:49 -04:00
|
|
|
|
2025-09-19 15:14:46 -04:00
|
|
|
for (const auto& reactant : reactants) {
|
2025-11-10 10:40:03 -05:00
|
|
|
LOG_TRACE_L1(logger, "Transferring {:.4e} molar abundance from {} to {} during priming.",
|
|
|
|
|
abundancePerReactant, reactant.name(), primingSpecies.name());
|
|
|
|
|
|
|
|
|
|
if (!molarAbundances.contains(reactant)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
molarAbundances.at(reactant) -= abundancePerReactant;
|
|
|
|
|
totalMolarAbundanceChanges[reactant] -= abundancePerReactant;
|
|
|
|
|
|
|
|
|
|
// Ensure non-negative abundances
|
|
|
|
|
if (molarAbundances.at(reactant) < 0.0) {
|
|
|
|
|
LOG_WARNING(logger, "Species {} went negative during priming. Clamping to zero.", reactant.name());
|
|
|
|
|
totalMolarAbundanceChanges[reactant] += molarAbundances.at(reactant); // Adjust change to reflect clamp
|
|
|
|
|
molarAbundances.at(reactant) = 0.0;
|
2025-07-14 14:50:49 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Final Composition Construction ---
|
2025-11-10 10:40:03 -05:00
|
|
|
std::vector<Species> final_species;
|
|
|
|
|
std::vector<double> final_molar_abundances;
|
|
|
|
|
|
|
|
|
|
for (const auto& [species, abundance] : molarAbundances) {
|
|
|
|
|
final_species.emplace_back(species);
|
|
|
|
|
final_molar_abundances.push_back(std::max(0.0, abundance));
|
|
|
|
|
|
|
|
|
|
LOG_TRACE_L1(logger, "After priming, species {} has molar abundance {:.4e} (had {:0.4e} prior to priming).",
|
|
|
|
|
species.name(),
|
|
|
|
|
std::max(0.0, abundance),
|
|
|
|
|
netIn.composition.getMolarAbundance(species));
|
2025-07-14 14:50:49 -04:00
|
|
|
}
|
|
|
|
|
|
2025-11-10 10:40:03 -05:00
|
|
|
Composition primedComposition(final_species, final_molar_abundances);
|
2025-07-14 14:50:49 -04:00
|
|
|
|
|
|
|
|
report.primedComposition = primedComposition;
|
2025-11-10 10:40:03 -05:00
|
|
|
|
|
|
|
|
// Convert molar abundance changes to mass fraction changes for reporting
|
|
|
|
|
for (const auto& [species, molarChange] : totalMolarAbundanceChanges) {
|
|
|
|
|
double massFractionChange = molarChange * species.mass();
|
|
|
|
|
report.massFractionChanges.emplace_back(species, massFractionChange);
|
2025-07-10 09:36:05 -04:00
|
|
|
}
|
|
|
|
|
|
2025-09-19 15:14:46 -04:00
|
|
|
// Restore the engine to its original, smaller network state.
|
2025-07-22 12:48:24 -04:00
|
|
|
engine.setNetworkReactions(initialReactionSet);
|
2025-11-10 10:40:03 -05:00
|
|
|
|
2025-07-14 14:50:49 -04:00
|
|
|
return report;
|
2025-07-10 09:36:05 -04:00
|
|
|
}
|
|
|
|
|
|
2025-07-14 14:50:49 -04:00
|
|
|
double calculateDestructionRateConstant(
|
2025-10-14 13:37:48 -04:00
|
|
|
const DynamicEngine& engine,
|
|
|
|
|
const Species& species,
|
|
|
|
|
const Composition& composition,
|
|
|
|
|
const double T9,
|
|
|
|
|
const double rho,
|
|
|
|
|
const std::optional<std::vector<reaction::ReactionType>> &reactionTypesToIgnore
|
|
|
|
|
) {
|
2025-11-10 10:40:03 -05:00
|
|
|
Composition unrestrictedComp(composition);
|
|
|
|
|
unrestrictedComp.setMolarAbundance(species, 1.0);
|
2025-07-10 09:36:05 -04:00
|
|
|
double destructionRateConstant = 0.0;
|
|
|
|
|
for (const auto& reaction: engine.getNetworkReactions()) {
|
2025-10-14 13:37:48 -04:00
|
|
|
if (isReactionIgnorable(*reaction, reactionTypesToIgnore)) continue;
|
|
|
|
|
|
|
|
|
|
const int stoichiometry = reaction->stoichiometry(species);
|
|
|
|
|
if (stoichiometry < 0) {
|
|
|
|
|
destructionRateConstant += std::abs(stoichiometry) * engine.calculateMolarReactionFlow(*reaction, unrestrictedComp, T9, rho);
|
2025-07-10 09:36:05 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return destructionRateConstant;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
double calculateCreationRate(
|
|
|
|
|
const DynamicEngine& engine,
|
2025-08-15 12:11:32 -04:00
|
|
|
const Species& species,
|
2025-10-14 13:37:48 -04:00
|
|
|
const Composition& composition,
|
2025-07-10 09:36:05 -04:00
|
|
|
const double T9,
|
2025-10-14 13:37:48 -04:00
|
|
|
const double rho,
|
|
|
|
|
const std::optional<std::vector<reaction::ReactionType>> &reactionTypesToIgnore
|
2025-07-10 09:36:05 -04:00
|
|
|
) {
|
|
|
|
|
double creationRate = 0.0;
|
|
|
|
|
for (const auto& reaction: engine.getNetworkReactions()) {
|
2025-10-14 13:37:48 -04:00
|
|
|
if (isReactionIgnorable(*reaction, reactionTypesToIgnore)) continue;
|
|
|
|
|
|
2025-08-14 13:33:46 -04:00
|
|
|
const int stoichiometry = reaction->stoichiometry(species);
|
2025-07-10 09:36:05 -04:00
|
|
|
if (stoichiometry > 0) {
|
2025-10-14 13:37:48 -04:00
|
|
|
creationRate += stoichiometry * engine.calculateMolarReactionFlow(*reaction, composition, T9, rho);
|
2025-07-10 09:36:05 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return creationRate;
|
|
|
|
|
}
|
|
|
|
|
}
|