feat(dynamic-engine): added derivitves for energy generation rate. dε/dT and dε/dρ have been added to NetOut and computed with auto diff
This commit is contained in:
@@ -35,10 +35,42 @@ namespace gridfire {
|
||||
return dominateReaction;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @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.
|
||||
* @return PrimingReport encapsulating the results of the priming operation, including the new
|
||||
* robustly primed composition.
|
||||
*/
|
||||
PrimingReport primeNetwork(const NetIn& netIn, DynamicEngine& engine) {
|
||||
auto logger = LogManager::getInstance().getLogger("log");
|
||||
|
||||
// --- Initial Setup ---
|
||||
// Identify all species with zero initial mass fraction that need to be primed.
|
||||
std::vector<Species> speciesToPrime;
|
||||
for (const auto &entry: netIn.composition | std::views::values) {
|
||||
if (entry.mass_fraction() == 0.0) {
|
||||
@@ -47,6 +79,7 @@ namespace gridfire {
|
||||
}
|
||||
LOG_DEBUG(logger, "Priming {} species in the network.", speciesToPrime.size());
|
||||
|
||||
// If no species need priming, return immediately.
|
||||
PrimingReport report;
|
||||
if (speciesToPrime.empty()) {
|
||||
report.primedComposition = netIn.composition;
|
||||
@@ -58,43 +91,46 @@ namespace gridfire {
|
||||
const double T9 = netIn.temperature / 1e9;
|
||||
const double rho = netIn.density;
|
||||
const auto initialReactionSet = engine.getNetworkReactions();
|
||||
|
||||
report.status = PrimingReportStatus::FULL_SUCCESS;
|
||||
report.success = true;
|
||||
|
||||
// --- 1: pack composition into internal map ---
|
||||
// Create a mutable map of the mass fractions that we will modify.
|
||||
std::unordered_map<Species, double> currentMassFractions;
|
||||
for (const auto& entry : netIn.composition | std::views::values) {
|
||||
currentMassFractions[entry.isotope()] = entry.mass_fraction();
|
||||
}
|
||||
// Ensure all species to be primed exist in the map, initialized to zero.
|
||||
for (const auto& entry : speciesToPrime) {
|
||||
currentMassFractions[entry] = 0.0; // Initialize priming species with 0 mass fraction
|
||||
currentMassFractions[entry] = 0.0;
|
||||
}
|
||||
|
||||
std::unordered_map<Species, double> totalMassFractionChanges;
|
||||
|
||||
// Rebuild the engine with the full network to ensure all possible creation channels are available.
|
||||
engine.rebuild(netIn.composition, NetworkBuildDepth::Full);
|
||||
|
||||
for (const auto& primingSpecies : speciesToPrime) {
|
||||
LOG_TRACE_L3(logger, "Priming species: {}", primingSpecies.name());
|
||||
// --- STAGE 1: Calculation and Bookkeeping (No Modifications) ---
|
||||
// In this stage, we calculate all required mass transfers but do not apply them yet.
|
||||
|
||||
// Create a temporary composition from the current internal state for the primer
|
||||
// A struct to hold the result of each individual priming calculation.
|
||||
struct MassTransferRequest {
|
||||
Species species_to_prime;
|
||||
double equilibrium_mass_fraction;
|
||||
std::vector<Species> reactants;
|
||||
};
|
||||
std::vector<MassTransferRequest> requests;
|
||||
|
||||
for (const auto& primingSpecies : speciesToPrime) {
|
||||
// Create a temporary composition reflecting the current state for rate calculations.
|
||||
Composition tempComp;
|
||||
for(const auto& [sp, mf] : currentMassFractions) {
|
||||
tempComp.registerSymbol(std::string(sp.name()));
|
||||
if (mf < 0.0 && std::abs(mf) < 1e-16) {
|
||||
tempComp.setMassFraction(sp, 0.0); // Avoid negative mass fractions
|
||||
} else {
|
||||
tempComp.setMassFraction(sp, mf);
|
||||
}
|
||||
tempComp.setMassFraction(sp, std::max(0.0, mf));
|
||||
}
|
||||
tempComp.finalize(true); // Finalize with normalization
|
||||
tempComp.finalize(true);
|
||||
|
||||
NetIn tempNetIn = netIn;
|
||||
tempNetIn.composition = tempComp;
|
||||
|
||||
NetworkPrimingEngineView primer(primingSpecies, engine);
|
||||
|
||||
if (primer.getNetworkReactions().size() == 0) {
|
||||
LOG_ERROR(logger, "No priming reactions found for species {}.", primingSpecies.name());
|
||||
report.success = false;
|
||||
@@ -106,60 +142,87 @@ namespace gridfire {
|
||||
const double destructionRateConstant = calculateDestructionRateConstant(primer, primingSpecies, Y, T9, rho);
|
||||
|
||||
if (destructionRateConstant > 1e-99) {
|
||||
double equilibriumMassFraction = 0.0;
|
||||
const double creationRate = calculateCreationRate(primer, primingSpecies, Y, T9, rho);
|
||||
equilibriumMassFraction = (creationRate / destructionRateConstant) * primingSpecies.mass();
|
||||
if (std::isnan(equilibriumMassFraction)) {
|
||||
LOG_WARNING(logger, "Equilibrium mass fraction for {} is NaN. Setting to 0.0. This is likely not an issue. It probably originates from all reactions leading to creation and destruction being frozen out. In that case 0.0 should be a good approximation. Hint: This happens often when the network temperature is very the low. ", primingSpecies.name());
|
||||
equilibriumMassFraction = 0.0;
|
||||
}
|
||||
LOG_TRACE_L3(logger, "Found equilibrium for {}: X_eq = {:.4e}", primingSpecies.name(), equilibriumMassFraction);
|
||||
double equilibriumMassFraction = (creationRate / destructionRateConstant) * primingSpecies.mass();
|
||||
if (std::isnan(equilibriumMassFraction)) equilibriumMassFraction = 0.0;
|
||||
|
||||
if (const reaction::Reaction* dominantChannel = findDominantCreationChannel(primer, primingSpecies, Y, T9, rho)) {
|
||||
LOG_TRACE_L3(logger, "Dominant creation channel for {}: {}", primingSpecies.name(), dominantChannel->id());
|
||||
|
||||
double totalReactantMass = 0.0;
|
||||
for (const auto& reactant : dominantChannel->reactants()) {
|
||||
totalReactantMass += reactant.mass();
|
||||
}
|
||||
|
||||
double scalingFactor = 1.0;
|
||||
for (const auto& reactant : dominantChannel->reactants()) {
|
||||
const double massToSubtract = equilibriumMassFraction * (reactant.mass() / totalReactantMass);
|
||||
double availableMass = 0.0;
|
||||
if (currentMassFractions.contains(reactant)) {
|
||||
availableMass = currentMassFractions.at(reactant);
|
||||
}
|
||||
if (massToSubtract > availableMass && availableMass > 0) {
|
||||
scalingFactor = std::min(scalingFactor, availableMass / massToSubtract);
|
||||
}
|
||||
}
|
||||
|
||||
if (scalingFactor < 1.0) {
|
||||
LOG_WARNING(logger, "Priming for {} was limited by reactant availability. Scaling transfer by {:.4e}", primingSpecies.name(), scalingFactor);
|
||||
equilibriumMassFraction *= scalingFactor;
|
||||
}
|
||||
|
||||
// Update the internal mass fraction map and accumulate total changes
|
||||
totalMassFractionChanges[primingSpecies] += equilibriumMassFraction;
|
||||
currentMassFractions[primingSpecies] += equilibriumMassFraction;
|
||||
|
||||
for (const auto& reactant : dominantChannel->reactants()) {
|
||||
const double massToSubtract = equilibriumMassFraction * (reactant.mass() / totalReactantMass);
|
||||
totalMassFractionChanges[reactant] -= massToSubtract;
|
||||
currentMassFractions[reactant] -= massToSubtract;
|
||||
}
|
||||
// Store the request instead of applying it immediately.
|
||||
requests.push_back({primingSpecies, equilibriumMassFraction, dominantChannel->reactants()});
|
||||
} else {
|
||||
LOG_ERROR(logger, "Failed to find dominant creation channel for {}.", primingSpecies.name());
|
||||
report.status = PrimingReportStatus::FAILED_TO_FIND_CREATION_CHANNEL;
|
||||
totalMassFractionChanges[primingSpecies] += 1e-40;
|
||||
currentMassFractions[primingSpecies] += 1e-40;
|
||||
LOG_ERROR(logger, "Failed to find dominant creation channel for {}.", primingSpecies.name());
|
||||
report.status = PrimingReportStatus::FAILED_TO_FIND_CREATION_CHANNEL;
|
||||
}
|
||||
} else {
|
||||
LOG_WARNING(logger, "No destruction channel found for {}. Using fallback abundance.", primingSpecies.name());
|
||||
totalMassFractionChanges[primingSpecies] += 1e-40;
|
||||
currentMassFractions[primingSpecies] += 1e-40;
|
||||
report.status = PrimingReportStatus::BASE_NETWORK_TOO_SHALLOW;
|
||||
// For species with no destruction, we can't calculate an equilibrium.
|
||||
// We add a request with a tiny fallback abundance to ensure it exists in the network.
|
||||
requests.push_back({primingSpecies, 1e-40, {}});
|
||||
}
|
||||
}
|
||||
|
||||
// --- STAGE 2: Collective Scaling Based on Reactant Availability ---
|
||||
// Now, we determine the total demand for each reactant and find a global scaling factor.
|
||||
|
||||
std::unordered_map<Species, double> total_mass_debits;
|
||||
for (const auto& req : requests) {
|
||||
if (req.reactants.empty()) continue; // Skip fallbacks which don't consume reactants.
|
||||
|
||||
double totalReactantMass = 0.0;
|
||||
for (const auto& reactant : req.reactants) {
|
||||
totalReactantMass += reactant.mass();
|
||||
}
|
||||
if (totalReactantMass == 0.0) continue;
|
||||
|
||||
for (const auto& reactant : req.reactants) {
|
||||
const double massToSubtract = req.equilibrium_mass_fraction * (reactant.mass() / totalReactantMass);
|
||||
total_mass_debits[reactant] += massToSubtract;
|
||||
}
|
||||
}
|
||||
|
||||
double globalScalingFactor = 1.0;
|
||||
for (const auto& [reactant, total_debit] : total_mass_debits) {
|
||||
double availableMass;
|
||||
if (currentMassFractions.contains(reactant)) {
|
||||
availableMass = currentMassFractions.at(reactant);
|
||||
} else {
|
||||
availableMass = 0.0;
|
||||
}
|
||||
if (total_debit > availableMass && availableMass > 0) {
|
||||
globalScalingFactor = std::min(globalScalingFactor, availableMass / total_debit);
|
||||
}
|
||||
}
|
||||
|
||||
if (globalScalingFactor < 1.0) {
|
||||
LOG_WARNING(logger, "Priming was limited by reactant availability. All transfers will be scaled by {:.4e}", globalScalingFactor);
|
||||
}
|
||||
|
||||
// --- STAGE 3: Application of Scaled Mass Transfers ---
|
||||
// Finally, apply all the transfers, scaled by our global factor.
|
||||
|
||||
std::unordered_map<Species, double> totalMassFractionChanges;
|
||||
for (const auto&[species_to_prime, equilibrium_mass_fraction, reactants] : requests) {
|
||||
const double scaled_equilibrium_mf = equilibrium_mass_fraction * globalScalingFactor;
|
||||
|
||||
// Add the scaled mass to the primed species.
|
||||
currentMassFractions.at(species_to_prime) += scaled_equilibrium_mf;
|
||||
totalMassFractionChanges[species_to_prime] += scaled_equilibrium_mf;
|
||||
|
||||
// Subtract the scaled mass from the reactants.
|
||||
if (!reactants.empty()) {
|
||||
double totalReactantMass = 0.0;
|
||||
for (const auto& reactant : reactants) {
|
||||
totalReactantMass += reactant.mass();
|
||||
}
|
||||
if (totalReactantMass == 0.0) continue;
|
||||
|
||||
for (const auto& reactant : reactants) {
|
||||
const double massToSubtract = scaled_equilibrium_mf * (reactant.mass() / totalReactantMass);
|
||||
if (massToSubtract != 0) {
|
||||
currentMassFractions.at(reactant) -= massToSubtract;
|
||||
totalMassFractionChanges[reactant] -= massToSubtract;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,14 +231,9 @@ namespace gridfire {
|
||||
std::vector<double> final_mass_fractions;
|
||||
for(const auto& [species, mass_fraction] : currentMassFractions) {
|
||||
final_symbols.emplace_back(species.name());
|
||||
if (mass_fraction < 0.0 && std::abs(mass_fraction) < 1e-16) {
|
||||
final_mass_fractions.push_back(0.0); // Avoid negative mass fractions
|
||||
} else {
|
||||
final_mass_fractions.push_back(mass_fraction);
|
||||
}
|
||||
final_mass_fractions.push_back(std::max(0.0, mass_fraction)); // Ensure no negative mass fractions.
|
||||
}
|
||||
|
||||
// Create the final composition object from the pre-normalized mass fractions
|
||||
Composition primedComposition(final_symbols, final_mass_fractions, true);
|
||||
|
||||
report.primedComposition = primedComposition;
|
||||
@@ -183,10 +241,166 @@ namespace gridfire {
|
||||
report.massFractionChanges.emplace_back(species, change);
|
||||
}
|
||||
|
||||
// Restore the engine to its original, smaller network state.
|
||||
engine.setNetworkReactions(initialReactionSet);
|
||||
return report;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// PrimingReport primeNetwork(const NetIn& netIn, DynamicEngine& engine) {
|
||||
// auto logger = LogManager::getInstance().getLogger("log");
|
||||
//
|
||||
// std::vector<Species> speciesToPrime;
|
||||
// for (const auto &entry: netIn.composition | std::views::values) {
|
||||
// std::cout << "Checking species: " << entry.isotope().name() << " with mass fraction: " << entry.mass_fraction() << std::endl;
|
||||
// if (entry.mass_fraction() == 0.0) {
|
||||
// speciesToPrime.push_back(entry.isotope());
|
||||
// }
|
||||
// }
|
||||
// LOG_DEBUG(logger, "Priming {} species in the network.", speciesToPrime.size());
|
||||
//
|
||||
// PrimingReport report;
|
||||
// if (speciesToPrime.empty()) {
|
||||
// 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;
|
||||
// const auto initialReactionSet = engine.getNetworkReactions();
|
||||
//
|
||||
// report.status = PrimingReportStatus::FULL_SUCCESS;
|
||||
// report.success = true;
|
||||
//
|
||||
// // --- 1: pack composition into internal map ---
|
||||
// std::unordered_map<Species, double> currentMassFractions;
|
||||
// for (const auto& entry : netIn.composition | std::views::values) {
|
||||
// currentMassFractions[entry.isotope()] = entry.mass_fraction();
|
||||
// }
|
||||
// for (const auto& entry : speciesToPrime) {
|
||||
// currentMassFractions[entry] = 0.0; // Initialize priming species with 0 mass fraction
|
||||
// }
|
||||
//
|
||||
// std::unordered_map<Species, double> totalMassFractionChanges;
|
||||
//
|
||||
// engine.rebuild(netIn.composition, NetworkBuildDepth::Full);
|
||||
//
|
||||
// for (const auto& primingSpecies : speciesToPrime) {
|
||||
// LOG_TRACE_L3(logger, "Priming species: {}", primingSpecies.name());
|
||||
//
|
||||
// // Create a temporary composition from the current internal state for the primer
|
||||
// Composition tempComp;
|
||||
// for(const auto& [sp, mf] : currentMassFractions) {
|
||||
// tempComp.registerSymbol(std::string(sp.name()));
|
||||
// if (mf < 0.0 && std::abs(mf) < 1e-16) {
|
||||
// tempComp.setMassFraction(sp, 0.0); // Avoid negative mass fractions
|
||||
// } else {
|
||||
// tempComp.setMassFraction(sp, mf);
|
||||
// }
|
||||
// }
|
||||
// tempComp.finalize(true); // Finalize with normalization
|
||||
//
|
||||
// NetIn tempNetIn = netIn;
|
||||
// tempNetIn.composition = tempComp;
|
||||
//
|
||||
// NetworkPrimingEngineView primer(primingSpecies, engine);
|
||||
//
|
||||
// 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;
|
||||
// }
|
||||
//
|
||||
// const auto Y = primer.mapNetInToMolarAbundanceVector(tempNetIn);
|
||||
// const double destructionRateConstant = calculateDestructionRateConstant(primer, primingSpecies, Y, T9, rho);
|
||||
//
|
||||
// if (destructionRateConstant > 1e-99) {
|
||||
// double equilibriumMassFraction = 0.0;
|
||||
// const double creationRate = calculateCreationRate(primer, primingSpecies, Y, T9, rho);
|
||||
// equilibriumMassFraction = (creationRate / destructionRateConstant) * primingSpecies.mass();
|
||||
// if (std::isnan(equilibriumMassFraction)) {
|
||||
// LOG_WARNING(logger, "Equilibrium mass fraction for {} is NaN. Setting to 0.0. This is likely not an issue. It probably originates from all reactions leading to creation and destruction being frozen out. In that case 0.0 should be a good approximation. Hint: This happens often when the network temperature is very the low. ", primingSpecies.name());
|
||||
// equilibriumMassFraction = 0.0;
|
||||
// }
|
||||
// LOG_TRACE_L3(logger, "Found equilibrium for {}: X_eq = {:.4e}", primingSpecies.name(), equilibriumMassFraction);
|
||||
//
|
||||
// if (const reaction::Reaction* dominantChannel = findDominantCreationChannel(primer, primingSpecies, Y, T9, rho)) {
|
||||
// LOG_TRACE_L3(logger, "Dominant creation channel for {}: {}", primingSpecies.name(), dominantChannel->id());
|
||||
//
|
||||
// double totalReactantMass = 0.0;
|
||||
// for (const auto& reactant : dominantChannel->reactants()) {
|
||||
// totalReactantMass += reactant.mass();
|
||||
// }
|
||||
//
|
||||
// double scalingFactor = 1.0;
|
||||
// for (const auto& reactant : dominantChannel->reactants()) {
|
||||
// const double massToSubtract = equilibriumMassFraction * (reactant.mass() / totalReactantMass);
|
||||
// double availableMass = 0.0;
|
||||
// if (currentMassFractions.contains(reactant)) {
|
||||
// availableMass = currentMassFractions.at(reactant);
|
||||
// }
|
||||
// if (massToSubtract > availableMass && availableMass > 0) {
|
||||
// scalingFactor = std::min(scalingFactor, availableMass / massToSubtract);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if (scalingFactor < 1.0) {
|
||||
// LOG_WARNING(logger, "Priming for {} was limited by reactant availability. Scaling transfer by {:.4e}", primingSpecies.name(), scalingFactor);
|
||||
// equilibriumMassFraction *= scalingFactor;
|
||||
// }
|
||||
//
|
||||
// // Update the internal mass fraction map and accumulate total changes
|
||||
// totalMassFractionChanges[primingSpecies] += equilibriumMassFraction;
|
||||
// currentMassFractions.at(primingSpecies) += equilibriumMassFraction;
|
||||
//
|
||||
// for (const auto& reactant : dominantChannel->reactants()) {
|
||||
// const double massToSubtract = equilibriumMassFraction * (reactant.mass() / totalReactantMass);
|
||||
// std::cout << "[Priming: " << primingSpecies.name() << ", Channel: " << dominantChannel->id() << "] Subtracting " << massToSubtract << " from reactant " << reactant.name() << std::endl;
|
||||
// totalMassFractionChanges[reactant] -= massToSubtract;
|
||||
// currentMassFractions[reactant] -= massToSubtract;
|
||||
// }
|
||||
// } else {
|
||||
// LOG_ERROR(logger, "Failed to find dominant creation channel for {}.", primingSpecies.name());
|
||||
// report.status = PrimingReportStatus::FAILED_TO_FIND_CREATION_CHANNEL;
|
||||
// totalMassFractionChanges[primingSpecies] += 1e-40;
|
||||
// currentMassFractions.at(primingSpecies) += 1e-40;
|
||||
// }
|
||||
// } else {
|
||||
// LOG_WARNING(logger, "No destruction channel found for {}. Using fallback abundance.", primingSpecies.name());
|
||||
// totalMassFractionChanges.at(primingSpecies) += 1e-40;
|
||||
// currentMassFractions.at(primingSpecies) += 1e-40;
|
||||
// report.status = PrimingReportStatus::BASE_NETWORK_TOO_SHALLOW;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // --- Final Composition Construction ---
|
||||
// std::vector<std::string> final_symbols;
|
||||
// std::vector<double> final_mass_fractions;
|
||||
// for(const auto& [species, mass_fraction] : currentMassFractions) {
|
||||
// final_symbols.emplace_back(species.name());
|
||||
// if (mass_fraction < 0.0 && std::abs(mass_fraction) < 1e-16) {
|
||||
// final_mass_fractions.push_back(0.0); // Avoid negative mass fractions
|
||||
// } else {
|
||||
// final_mass_fractions.push_back(mass_fraction);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Create the final composition object from the pre-normalized mass fractions
|
||||
// Composition primedComposition(final_symbols, final_mass_fractions, true);
|
||||
//
|
||||
// report.primedComposition = primedComposition;
|
||||
// for (const auto& [species, change] : totalMassFractionChanges) {
|
||||
// report.massFractionChanges.emplace_back(species, change);
|
||||
// }
|
||||
//
|
||||
// engine.setNetworkReactions(initialReactionSet);
|
||||
// return report;
|
||||
// }
|
||||
|
||||
double calculateDestructionRateConstant(
|
||||
const DynamicEngine& engine,
|
||||
const fourdst::atomic::Species& species,
|
||||
|
||||
Reference in New Issue
Block a user