perf(thread saftey): All Engines are now thread safe

Previously engines were not thread safe, a seperate engine would be
needed for every thread. This is no longer the case. This allows for
much more efficient parallel execution
This commit is contained in:
2025-12-12 12:08:47 -05:00
parent c7574a2f3d
commit e114c0e240
46 changed files with 3685 additions and 1604 deletions

View File

@@ -4,6 +4,10 @@
#include "gridfire/utils/sundials.h"
#include "gridfire/utils/logging.h"
#include "gridfire/engine/scratchpads/blob.h"
#include "gridfire/engine/scratchpads/utils.h"
#include "gridfire/engine/scratchpads/engine_multiscale_scratchpad.h"
#include <stdexcept>
#include <vector>
#include <ranges>
@@ -151,18 +155,6 @@ namespace {
return reactantSample != productSample;
}
void QuietErrorRouter(int line, const char *func, const char *file, const char *msg,
SUNErrCode err_code, void *err_user_data, SUNContext sunctx) {
// LIST OF ERRORS TO IGNORE
if (err_code == KIN_LINESEARCH_NONCONV) {
return;
}
// For everything else, use the default SUNDIALS logger (or your own)
SUNLogErrHandlerFn(line, func, file, msg, err_code, err_user_data, sunctx);
}
struct DisjointSet {
std::vector<size_t> parent;
explicit DisjointSet(const size_t n) {
@@ -170,7 +162,7 @@ namespace {
std::iota(parent.begin(), parent.end(), 0);
}
size_t find(const size_t i) {
size_t find(const size_t i) { // NOLINT(*-no-recursion)
if (parent.at(i) == i) return i;
return parent.at(i) = find(parent.at(i)); // Path compression
}
@@ -192,28 +184,16 @@ namespace gridfire::engine {
MultiscalePartitioningEngineView::MultiscalePartitioningEngineView(
DynamicEngine& baseEngine
) : m_baseEngine(baseEngine) {
const int flag = SUNContext_Create(SUN_COMM_NULL, &m_sun_ctx);
if (flag != 0) {
LOG_CRITICAL(m_logger, "Error while creating SUNContext in MultiscalePartitioningEngineView");
throw std::runtime_error("Error creating SUNContext in MultiscalePartitioningEngineView");
}
SUNContext_PushErrHandler(m_sun_ctx, QuietErrorRouter, nullptr);
}
MultiscalePartitioningEngineView::~MultiscalePartitioningEngineView() {
LOG_TRACE_L1(m_logger, "Cleaning up MultiscalePartitioningEngineView...");
m_qse_solvers.clear();
if (m_sun_ctx) {
SUNContext_Free(&m_sun_ctx);
m_sun_ctx = nullptr;
}
}
const std::vector<Species> & MultiscalePartitioningEngineView::getNetworkSpecies() const {
return m_baseEngine.getNetworkSpecies();
const std::vector<Species> & MultiscalePartitioningEngineView::getNetworkSpecies(
scratch::StateBlob& ctx
) const {
return m_baseEngine.getNetworkSpecies(ctx);
}
std::expected<StepDerivatives<double>, EngineStatus> MultiscalePartitioningEngineView::calculateRHSAndEnergy(
scratch::StateBlob& ctx,
const fourdst::composition::CompositionAbstract &comp,
const double T9,
const double rho,
@@ -232,7 +212,7 @@ namespace gridfire::engine {
}
return ss.str();
}());
const fourdst::composition::Composition qseComposition = getNormalizedEquilibratedComposition(comp, T9, rho, trust);
const fourdst::composition::Composition qseComposition = getNormalizedEquilibratedComposition(ctx, comp, T9, rho, trust);
LOG_TRACE_L2(m_logger, "Equilibrated composition prior to calling base engine is {}", [&qseComposition, &comp]() -> std::string {
std::stringstream ss;
size_t i = 0;
@@ -249,7 +229,7 @@ namespace gridfire::engine {
return ss.str();
}());
const auto result = m_baseEngine.calculateRHSAndEnergy(qseComposition, T9, rho, false);
const auto result = m_baseEngine.calculateRHSAndEnergy(ctx, qseComposition, T9, rho, false);
LOG_TRACE_L2(m_logger, "Base engine calculation of RHS and Energy complete.");
if (!result) {
@@ -258,9 +238,10 @@ namespace gridfire::engine {
}
auto deriv = result.value();
const auto* state = scratch::get_state<scratch::MultiscalePartitioningEngineViewScratchPad, true>(ctx);
LOG_TRACE_L2(m_logger, "Zeroing out algebraic species derivatives.");
for (const auto& species : m_algebraic_species) {
for (const auto& species : state->algebraic_species) {
deriv.dydt[species] = 0.0; // Fix the algebraic species to the equilibrium abundances we calculate.
}
LOG_TRACE_L2(m_logger, "Done Zeroing out algebraic species derivatives.");
@@ -268,24 +249,28 @@ namespace gridfire::engine {
}
EnergyDerivatives MultiscalePartitioningEngineView::calculateEpsDerivatives(
scratch::StateBlob& ctx,
const fourdst::composition::CompositionAbstract &comp,
const double T9,
const double rho
) const {
const fourdst::composition::Composition qseComposition = getNormalizedEquilibratedComposition(comp, T9, rho, false);
return m_baseEngine.calculateEpsDerivatives(qseComposition, T9, rho);
const fourdst::composition::Composition qseComposition = getNormalizedEquilibratedComposition(ctx, comp, T9, rho, false);
return m_baseEngine.calculateEpsDerivatives(ctx, qseComposition, T9, rho);
}
NetworkJacobian MultiscalePartitioningEngineView::generateJacobianMatrix(
scratch::StateBlob& ctx,
const fourdst::composition::CompositionAbstract &comp,
const double T9,
const double rho
) const {
const fourdst::composition::Composition qseComposition = getNormalizedEquilibratedComposition(comp, T9, rho, false);
return m_baseEngine.generateJacobianMatrix(qseComposition, T9, rho, m_dynamic_species);
const auto* state = scratch::get_state<scratch::MultiscalePartitioningEngineViewScratchPad, true>(ctx);
const fourdst::composition::Composition qseComposition = getNormalizedEquilibratedComposition(ctx, comp, T9, rho, false);
return m_baseEngine.generateJacobianMatrix(ctx, qseComposition, T9, rho, state->dynamic_species);
}
NetworkJacobian MultiscalePartitioningEngineView::generateJacobianMatrix(
scratch::StateBlob& ctx,
const fourdst::composition::CompositionAbstract &comp,
const double T9,
const double rho,
@@ -293,7 +278,7 @@ namespace gridfire::engine {
) const {
bool activeSpeciesIsSubset = true;
for (const auto& species : activeSpecies) {
if (!involvesSpecies(species)) activeSpeciesIsSubset = false;
if (!involvesSpecies(ctx, species)) activeSpeciesIsSubset = false;
}
if (!activeSpeciesIsSubset) {
std::string msg = std::format(
@@ -301,7 +286,7 @@ namespace gridfire::engine {
[&]() -> std::string {
std::stringstream ss;
for (const auto& species : activeSpecies) {
if (!this->involvesSpecies(species)) {
if (!involvesSpecies(ctx, species)) {
ss << species << " ";
}
}
@@ -314,114 +299,104 @@ namespace gridfire::engine {
std::vector<Species> dynamicActiveSpeciesIntersection;
for (const auto& species : activeSpecies) {
if (involvesSpeciesInDynamic(species)) {
if (involvesSpeciesInDynamic(ctx, species)) {
dynamicActiveSpeciesIntersection.push_back(species);
}
}
const fourdst::composition::Composition qseComposition = getNormalizedEquilibratedComposition(comp, T9, rho, false);
const fourdst::composition::Composition qseComposition = getNormalizedEquilibratedComposition(ctx, comp, T9, rho, false);
return m_baseEngine.generateJacobianMatrix(qseComposition, T9, rho, dynamicActiveSpeciesIntersection);
return m_baseEngine.generateJacobianMatrix(ctx, qseComposition, T9, rho, dynamicActiveSpeciesIntersection);
}
NetworkJacobian MultiscalePartitioningEngineView::generateJacobianMatrix(
scratch::StateBlob& ctx,
const fourdst::composition::CompositionAbstract &comp,
const double T9,
const double rho,
const SparsityPattern &sparsityPattern
) const {
const fourdst::composition::Composition qseComposition = getNormalizedEquilibratedComposition(comp, T9, rho, false);
return m_baseEngine.generateJacobianMatrix(qseComposition, T9, rho, sparsityPattern);
}
void MultiscalePartitioningEngineView::generateStoichiometryMatrix() {
m_baseEngine.generateStoichiometryMatrix();
}
int MultiscalePartitioningEngineView::getStoichiometryMatrixEntry(
const Species& species,
const reaction::Reaction& reaction
) const {
return m_baseEngine.getStoichiometryMatrixEntry(species, reaction);
const fourdst::composition::Composition qseComposition = getNormalizedEquilibratedComposition(ctx, comp, T9, rho, false);
return m_baseEngine.generateJacobianMatrix(ctx, qseComposition, T9, rho, sparsityPattern);
}
double MultiscalePartitioningEngineView::calculateMolarReactionFlow(
scratch::StateBlob& ctx,
const reaction::Reaction &reaction,
const fourdst::composition::CompositionAbstract &comp,
const double T9,
const double rho
) const {
const fourdst::composition::Composition qseComposition = getNormalizedEquilibratedComposition(comp, T9, rho, false);
const fourdst::composition::Composition qseComposition = getNormalizedEquilibratedComposition(ctx, comp, T9, rho, false);
return m_baseEngine.calculateMolarReactionFlow(reaction, qseComposition, T9, rho);
return m_baseEngine.calculateMolarReactionFlow(ctx, reaction, qseComposition, T9, rho);
}
const reaction::ReactionSet & MultiscalePartitioningEngineView::getNetworkReactions() const {
return m_baseEngine.getNetworkReactions();
}
void MultiscalePartitioningEngineView::setNetworkReactions(const reaction::ReactionSet &reactions) {
LOG_CRITICAL(m_logger, "setNetworkReactions is not supported in MultiscalePartitioningEngineView. Did you mean to call this on the base engine?");
throw exceptions::UnableToSetNetworkReactionsError("setNetworkReactions is not supported in MultiscalePartitioningEngineView. Did you mean to call this on the base engine?");
const reaction::ReactionSet & MultiscalePartitioningEngineView::getNetworkReactions(
scratch::StateBlob& ctx
) const {
return m_baseEngine.getNetworkReactions(ctx);
}
std::expected<std::unordered_map<Species, double>, EngineStatus> MultiscalePartitioningEngineView::getSpeciesTimescales(
scratch::StateBlob& ctx,
const fourdst::composition::CompositionAbstract &comp,
const double T9,
const double rho
) const {
const fourdst::composition::Composition qseComposition = getNormalizedEquilibratedComposition(comp, T9, rho, false);
const auto result = m_baseEngine.getSpeciesTimescales(qseComposition, T9, rho);
const fourdst::composition::Composition qseComposition = getNormalizedEquilibratedComposition(ctx, comp, T9, rho, false);
const auto result = m_baseEngine.getSpeciesTimescales(ctx, qseComposition, T9, rho);
if (!result) {
return std::unexpected{result.error()};
}
const auto* state = scratch::get_state<scratch::MultiscalePartitioningEngineViewScratchPad, true>(ctx);
std::unordered_map<Species, double> speciesTimescales = result.value();
for (const auto& algebraicSpecies : m_algebraic_species) {
for (const auto& algebraicSpecies : state->algebraic_species) {
speciesTimescales[algebraicSpecies] = std::numeric_limits<double>::infinity(); // Algebraic species have infinite timescales.
}
return speciesTimescales;
}
std::expected<std::unordered_map<Species, double>, EngineStatus> MultiscalePartitioningEngineView::getSpeciesDestructionTimescales(
scratch::StateBlob& ctx,
const fourdst::composition::CompositionAbstract &comp,
const double T9,
const double rho
) const {
const fourdst::composition::Composition qseComposition = getNormalizedEquilibratedComposition(comp, T9, rho, false);
const auto result = m_baseEngine.getSpeciesDestructionTimescales(qseComposition, T9, rho);
const fourdst::composition::Composition qseComposition = getNormalizedEquilibratedComposition(ctx, comp, T9, rho, false);
const auto result = m_baseEngine.getSpeciesDestructionTimescales(ctx, qseComposition, T9, rho);
if (!result) {
return std::unexpected{result.error()};
}
std::unordered_map<Species, double> speciesDestructionTimescales = result.value();
for (const auto& algebraicSpecies : m_algebraic_species) {
const auto* state = scratch::get_state<scratch::MultiscalePartitioningEngineViewScratchPad, true>(ctx);
for (const auto& algebraicSpecies : state->algebraic_species) {
speciesDestructionTimescales[algebraicSpecies] = std::numeric_limits<double>::infinity(); // Algebraic species have infinite destruction timescales.
}
return speciesDestructionTimescales;
}
fourdst::composition::Composition MultiscalePartitioningEngineView::update(const NetIn &netIn) {
const fourdst::composition::Composition baseUpdatedComposition = m_baseEngine.update(netIn);
fourdst::composition::Composition MultiscalePartitioningEngineView::project(
scratch::StateBlob& ctx,
const NetIn &netIn
) const {
const fourdst::composition::Composition baseUpdatedComposition = m_baseEngine.project(ctx, netIn);
NetIn baseUpdatedNetIn = netIn;
baseUpdatedNetIn.composition = baseUpdatedComposition;
fourdst::composition::Composition equilibratedComposition = partitionNetwork(baseUpdatedNetIn);
m_composition_cache.clear();
fourdst::composition::Composition equilibratedComposition = partitionNetwork(ctx, baseUpdatedNetIn);
auto* state = scratch::get_state<scratch::MultiscalePartitioningEngineViewScratchPad, true>(ctx);
state->composition_cache.clear();
return equilibratedComposition;
}
bool MultiscalePartitioningEngineView::isStale(const NetIn &netIn) {
return m_baseEngine.isStale(netIn);
}
void MultiscalePartitioningEngineView::setScreeningModel(
const screening::ScreeningType model
) {
m_baseEngine.setScreeningModel(model);
}
screening::ScreeningType MultiscalePartitioningEngineView::getScreeningModel() const {
return m_baseEngine.getScreeningModel();
screening::ScreeningType MultiscalePartitioningEngineView::getScreeningModel(
scratch::StateBlob& ctx
) const {
return m_baseEngine.getScreeningModel(ctx);
}
const DynamicEngine & MultiscalePartitioningEngineView::getBaseEngine() const {
@@ -429,8 +404,11 @@ namespace gridfire::engine {
}
std::vector<std::vector<Species>> MultiscalePartitioningEngineView::analyzeTimescalePoolConnectivity(
const std::vector<std::vector<Species>> &timescale_pools, const fourdst::composition::Composition &comp, double T9, double
rho
scratch::StateBlob& ctx,
const std::vector<std::vector<Species>> &timescale_pools,
const fourdst::composition::Composition &comp,
double T9,
double rho
) const {
std::vector<std::vector<Species>> final_connected_pools;
@@ -440,7 +418,7 @@ namespace gridfire::engine {
}
// For each timescale pool, we need to analyze connectivity.
auto connectivity_graph = buildConnectivityGraph(pool, comp, T9, rho);
auto connectivity_graph = buildConnectivityGraph(ctx, pool, comp, T9, rho);
auto components = findConnectedComponentsBFS(connectivity_graph, pool);
final_connected_pools.insert(final_connected_pools.end(), components.begin(), components.end());
}
@@ -449,20 +427,21 @@ namespace gridfire::engine {
}
std::vector<MultiscalePartitioningEngineView::QSEGroup> MultiscalePartitioningEngineView::pruneValidatedGroups(
scratch::StateBlob& ctx,
const std::vector<QSEGroup> &groups,
const std::vector<reaction::ReactionSet> &groupReactions,
const fourdst::composition::Composition &comp,
const double T9,
const double rho
) const {
const auto result = m_baseEngine.getSpeciesTimescales(comp, T9, rho);
const auto result = m_baseEngine.getSpeciesTimescales(ctx, comp, T9, rho);
if (!result) {
throw std::runtime_error("Base engine returned stale error during pruneValidatedGroups timescale retrieval.");
}
std::unordered_map<Species, double> speciesTimescales = result.value();
const std::unordered_map<Species, double>& speciesTimescales = result.value();
std::vector<QSEGroup> newGroups;
for (const auto &[group, reactions] : std::views::zip(groups, groupReactions)) {
if (reactions.size() == 0) { // If a QSE group has gotten here it should have reactions associated with it. If it doesn't that is a serious error.
if (reactions.empty()) { // If a QSE group has gotten here it should have reactions associated with it. If it doesn't that is a serious error.
LOG_CRITICAL(m_logger, "No reactions specified for QSE group {} during pruning analysis.", group.toString(false));
throw std::runtime_error("No reactions specified for QSE group " + group.toString(false) + " during pruneValidatedGroups flux analysis.");
}
@@ -475,7 +454,7 @@ namespace gridfire::engine {
for (const auto& species : group.algebraic_species) {
mean_molar_abundance += comp.getMolarAbundance(species);
}
mean_molar_abundance /= group.algebraic_species.size();
mean_molar_abundance /= static_cast<double>(group.algebraic_species.size());
{ // Safety Valve to ensure valid log scaling
if (mean_molar_abundance <= 0) {
LOG_CRITICAL(m_logger, "Non-positive mean molar abundance {} calculated for QSE group during pruning analysis.", mean_molar_abundance);
@@ -484,7 +463,7 @@ namespace gridfire::engine {
}
for (const auto& reaction : reactions) {
const double flux = m_baseEngine.calculateMolarReactionFlow(*reaction, comp, T9, rho);
const double flux = m_baseEngine.calculateMolarReactionFlow(ctx, *reaction, comp, T9, rho);
size_t hash = reaction->hash(0);
if (reactionFluxes.contains(hash)) {
throw std::runtime_error("Duplicate reaction hash found during pruneValidatedGroups flux analysis.");
@@ -624,7 +603,7 @@ namespace gridfire::engine {
for (const auto &species : g.algebraic_species) {
meanTimescale += speciesTimescales.at(species);
}
meanTimescale /= g.algebraic_species.size();
meanTimescale /= static_cast<double>(g.algebraic_species.size());
g.mean_timescale = meanTimescale;
newGroups.push_back(g);
}
@@ -634,6 +613,7 @@ namespace gridfire::engine {
}
std::vector<MultiscalePartitioningEngineView::QSEGroup> MultiscalePartitioningEngineView::merge_coupled_groups(
scratch::StateBlob& ctx,
const std::vector<QSEGroup> &groups,
const std::vector<reaction::ReactionSet> &groupReactions
) {
@@ -688,10 +668,12 @@ namespace gridfire::engine {
}
fourdst::composition::Composition MultiscalePartitioningEngineView::partitionNetwork(
scratch::StateBlob& ctx,
const NetIn &netIn
) {
) const {
auto* state = scratch::get_state<scratch::MultiscalePartitioningEngineViewScratchPad, true>(ctx);
// --- Step 0. Prime the network ---
const PrimingReport primingReport = m_baseEngine.primeEngine(netIn);
const PrimingReport primingReport = m_baseEngine.primeEngine(ctx, netIn);
const fourdst::composition::Composition& comp = primingReport.primedComposition;
const double T9 = netIn.temperature / 1e9;
const double rho = netIn.density;
@@ -699,25 +681,25 @@ namespace gridfire::engine {
// --- Step 0.5 Clear previous state ---
LOG_TRACE_L1(m_logger, "Partitioning network...");
LOG_TRACE_L1(m_logger, "Clearing previous state...");
m_qse_groups.clear();
m_qse_solvers.clear();
m_dynamic_species.clear();
m_algebraic_species.clear();
m_composition_cache.clear(); // We need to clear the cache now cause the same comp, temp, and density may result in a different value
state->qse_groups.clear();
state->qse_solvers.clear();
state->dynamic_species.clear();
state->algebraic_species.clear();
state->composition_cache.clear(); // We need to clear the cache now cause the same comp, temp, and density may result in a different value
// --- Step 1. Identify distinct timescale regions ---
LOG_TRACE_L1(m_logger, "Identifying fast reactions...");
const std::vector<std::vector<Species>> timescale_pools = partitionByTimescale(comp, T9, rho);
const std::vector<std::vector<Species>> timescale_pools = partitionByTimescale(ctx, comp, T9, rho);
LOG_TRACE_L1(m_logger, "Found {} timescale pools.", timescale_pools.size());
// --- Step 2. Select the mean slowest pool as the base dynamical group ---
LOG_TRACE_L1(m_logger, "Identifying mean slowest pool...");
const size_t mean_slowest_pool_index = identifyMeanSlowestPool(timescale_pools, comp, T9, rho);
const size_t mean_slowest_pool_index = identifyMeanSlowestPool(ctx, timescale_pools, comp, T9, rho);
LOG_TRACE_L1(m_logger, "Mean slowest pool index: {}", mean_slowest_pool_index);
// --- Step 3. Push the slowest pool into the dynamic species list ---
for (const auto& slowSpecies : timescale_pools[mean_slowest_pool_index]) {
m_dynamic_species.push_back(slowSpecies);
state->dynamic_species.push_back(slowSpecies);
}
// --- Step 4. Pack Candidate QSE Groups ---
@@ -729,40 +711,40 @@ namespace gridfire::engine {
}
LOG_TRACE_L1(m_logger, "Preforming connectivity analysis on timescale pools...");
const std::vector<std::vector<Species>> connected_pools = analyzeTimescalePoolConnectivity(candidate_pools, comp, T9, rho);
const std::vector<std::vector<Species>> connected_pools = analyzeTimescalePoolConnectivity(ctx, candidate_pools, comp, T9, rho);
LOG_TRACE_L1(m_logger, "Found {} connected pools (compared to {} timescale pools) for QSE analysis.", connected_pools.size(), timescale_pools.size());
// --- Step 5. Identify potential seed species for each candidate pool ---
LOG_TRACE_L1(m_logger, "Identifying potential seed species for candidate pools...");
const std::vector<QSEGroup> candidate_groups = constructCandidateGroups(connected_pools, comp, T9, rho);
const std::vector<QSEGroup> candidate_groups = constructCandidateGroups(ctx, connected_pools, comp, T9, rho);
LOG_TRACE_L1(m_logger, "Found {} candidate QSE groups for further analysis ({})", candidate_groups.size(), utils::iterable_to_delimited_string(candidate_groups));
LOG_TRACE_L1(m_logger, "Validating candidate groups with flux analysis...");
const auto [validated_groups, invalidate_groups, validated_group_reactions] = validateGroupsWithFluxAnalysis(candidate_groups, comp, T9, rho);
const auto [validated_groups, invalidate_groups, validated_group_reactions] = validateGroupsWithFluxAnalysis(ctx, candidate_groups, comp, T9, rho);
LOG_TRACE_L1(m_logger, "Validated {} group(s) QSE groups. {}", validated_groups.size(), utils::iterable_to_delimited_string(validated_groups));
LOG_TRACE_L1(m_logger, "Pruning groups based on log abundance-normalized flux analysis...");
const std::vector<QSEGroup> prunedGroups = pruneValidatedGroups(validated_groups, validated_group_reactions, comp, T9, rho);
const std::vector<QSEGroup> prunedGroups = pruneValidatedGroups(ctx, validated_groups, validated_group_reactions, comp, T9, rho);
LOG_TRACE_L1(m_logger, "After Pruning remaining groups are: {}", utils::iterable_to_delimited_string(prunedGroups));
LOG_TRACE_L1(m_logger, "Re-validating pruned groups with flux analysis...");
auto [pruned_validated_groups, _, pruned_validated_reactions] = validateGroupsWithFluxAnalysis(prunedGroups, comp, T9, rho);
auto [pruned_validated_groups, _, pruned_validated_reactions] = validateGroupsWithFluxAnalysis(ctx, prunedGroups, comp, T9, rho);
LOG_TRACE_L1(m_logger, "After re-validation, {} QSE groups remain. ({})",pruned_validated_groups.size(), utils::iterable_to_delimited_string(pruned_validated_groups));
LOG_TRACE_L1(m_logger, "Merging coupled QSE groups...");
const std::vector<QSEGroup> merged_groups = merge_coupled_groups(pruned_validated_groups, pruned_validated_reactions);
const std::vector<QSEGroup> merged_groups = merge_coupled_groups(ctx, pruned_validated_groups, pruned_validated_reactions);
LOG_TRACE_L1(m_logger, "After merging, {} QSE groups remain. ({})", merged_groups.size(), utils::iterable_to_delimited_string(merged_groups));
m_qse_groups = pruned_validated_groups;
state->qse_groups = pruned_validated_groups;
LOG_TRACE_L1(m_logger, "Pushing all identified algebraic species into algebraic set...");
for (const auto& group : m_qse_groups) {
for (const auto& group : state->qse_groups) {
// Add algebraic species to the algebraic set
for (const auto& species : group.algebraic_species) {
if (std::ranges::find(m_algebraic_species, species) == m_algebraic_species.end()) {
m_algebraic_species.push_back(species);
if (std::ranges::find(state->algebraic_species, species) == state->algebraic_species.end()) {
state->algebraic_species.push_back(species);
}
}
}
@@ -771,46 +753,47 @@ namespace gridfire::engine {
LOG_INFO(
m_logger,
"Partitioning complete. Found {} dynamic species, {} algebraic (QSE) species ({}) spread over {} QSE group{}.",
m_dynamic_species.size(),
m_algebraic_species.size(),
utils::iterable_to_delimited_string(m_algebraic_species),
m_qse_groups.size(),
m_qse_groups.size() == 1 ? "" : "s"
state->dynamic_species.size(),
state->algebraic_species.size(),
utils::iterable_to_delimited_string(state->algebraic_species),
state->qse_groups.size(),
state->qse_groups.size() == 1 ? "" : "s"
);
// Sort the QSE groups by mean timescale so that fastest groups get equilibrated first (as these may feed slower groups)
LOG_TRACE_L1(m_logger, "Sorting algebraic set by mean timescale...");
std::ranges::sort(m_qse_groups, [](const QSEGroup& a, const QSEGroup& b) {
std::ranges::sort(state->qse_groups, [](const QSEGroup& a, const QSEGroup& b) {
return a.mean_timescale < b.mean_timescale;
});
LOG_TRACE_L1(m_logger, "Finalizing dynamic species list...");
for (const auto& species : m_baseEngine.getNetworkSpecies()) {
const bool involvesAlgebraic = involvesSpeciesInQSE(species);
if (std::ranges::find(m_dynamic_species, species) == m_dynamic_species.end() && !involvesAlgebraic) {
m_dynamic_species.push_back(species);
for (const auto& species : m_baseEngine.getNetworkSpecies(ctx)) {
const bool involvesAlgebraic = involvesSpeciesInQSE(ctx, species);
if (std::ranges::find(state->dynamic_species, species) == state->dynamic_species.end() && !involvesAlgebraic) {
state->dynamic_species.push_back(species);
}
}
LOG_TRACE_L1(m_logger, "Final dynamic species set: {}", utils::iterable_to_delimited_string(m_dynamic_species));
LOG_TRACE_L1(m_logger, "Creating QSE solvers for each identified QSE group...");
for (const auto& group : m_qse_groups) {
for (const auto& group : state->qse_groups) {
std::vector<Species> groupAlgebraicSpecies;
for (const auto& species : group.algebraic_species) {
groupAlgebraicSpecies.push_back(species);
}
m_qse_solvers.push_back(std::make_unique<QSESolver>(groupAlgebraicSpecies, m_baseEngine, m_sun_ctx));
state->qse_solvers.push_back(std::make_unique<QSESolver>(groupAlgebraicSpecies, m_baseEngine, state->sun_ctx));
}
LOG_TRACE_L1(m_logger, "{} QSE solvers created.", m_qse_solvers.size());
LOG_TRACE_L1(m_logger, "Calculating final equilibrated composition...");
fourdst::composition::Composition result = getNormalizedEquilibratedComposition(comp, T9, rho, false);
fourdst::composition::Composition result = getNormalizedEquilibratedComposition(ctx, comp, T9, rho, false);
LOG_TRACE_L1(m_logger, "Final equilibrated composition calculated...");
return result;
}
void MultiscalePartitioningEngineView::exportToDot(
scratch::StateBlob &ctx,
const std::string &filename,
const fourdst::composition::Composition &comp,
const double T9,
@@ -822,22 +805,24 @@ namespace gridfire::engine {
throw std::runtime_error("Failed to open file for writing: " + filename);
}
const auto& all_species = m_baseEngine.getNetworkSpecies();
const auto& all_reactions = m_baseEngine.getNetworkReactions();
const auto* state = scratch::get_state<scratch::MultiscalePartitioningEngineViewScratchPad, true>(ctx);
const auto& all_species = m_baseEngine.getNetworkSpecies(ctx);
const auto& all_reactions = m_baseEngine.getNetworkReactions(ctx);
// --- 1. Pre-computation and Categorization ---
// Categorize species into algebraic, seed, and core dynamic
std::unordered_set<Species> algebraic_species;
std::unordered_set<Species> seed_species;
for (const auto& group : m_qse_groups) {
for (const auto& group : state->qse_groups) {
if (group.is_in_equilibrium) {
algebraic_species.insert(group.algebraic_species.begin(), group.algebraic_species.end());
seed_species.insert(group.seed_species.begin(), group.seed_species.end());
}
}
const fourdst::composition::Composition qseComposition = getNormalizedEquilibratedComposition(comp, T9, rho, false);
const fourdst::composition::Composition qseComposition = getNormalizedEquilibratedComposition(ctx, comp, T9, rho, false);
// Calculate reaction flows and find min/max for logarithmic scaling of transparency
std::vector<double> reaction_flows;
reaction_flows.reserve(all_reactions.size());
@@ -845,7 +830,7 @@ namespace gridfire::engine {
double max_log_flow = std::numeric_limits<double>::lowest();
for (const auto& reaction : all_reactions) {
double flow = std::abs(m_baseEngine.calculateMolarReactionFlow(*reaction, qseComposition, T9, rho));
double flow = std::abs(m_baseEngine.calculateMolarReactionFlow(ctx, *reaction, qseComposition, T9, rho));
reaction_flows.push_back(flow);
if (flow > 1e-99) { // Avoid log(0)
double log_flow = std::log10(flow);
@@ -875,7 +860,7 @@ namespace gridfire::engine {
fillcolor = "#e0f2fe"; // Light Blue: Algebraic (in QSE)
} else if (seed_species.contains(species)) {
fillcolor = "#a7f3d0"; // Light Green: Seed (Dynamic, feeds a QSE group)
} else if (std::ranges::contains(m_dynamic_species, species)) {
} else if (std::ranges::contains(state->dynamic_species, species)) {
fillcolor = "#dcfce7"; // Pale Green: Core Dynamic
}
dotFile << " \"" << species.name() << "\" [label=\"" << species.name() << "\", fillcolor=\"" << fillcolor << "\"];\n";
@@ -918,7 +903,7 @@ namespace gridfire::engine {
// Draw a prominent box around the algebraic species of each valid QSE group.
dotFile << " // --- QSE Group Clusters ---\n";
int group_counter = 0;
for (const auto& group : m_qse_groups) {
for (const auto& group : state->qse_groups) {
if (!group.is_in_equilibrium || group.algebraic_species.empty()) {
continue;
}
@@ -1019,58 +1004,64 @@ namespace gridfire::engine {
dotFile.close();
}
std::vector<double> MultiscalePartitioningEngineView::mapNetInToMolarAbundanceVector(const NetIn &netIn) const {
std::vector<double> Y(netIn.composition.size(), 0.0); // Initialize with zeros
for (const auto& [sp, y] : netIn.composition) {
Y[getSpeciesIndex(sp)] = y; // Map species to their molar abundance
}
return Y; // Return the vector of molar abundances
}
std::vector<Species> MultiscalePartitioningEngineView::getFastSpecies() const {
const auto& all_species = m_baseEngine.getNetworkSpecies();
std::vector<Species> MultiscalePartitioningEngineView::getFastSpecies(
scratch::StateBlob& ctx
) const {
const auto& all_species = m_baseEngine.getNetworkSpecies(ctx);
const auto* state = scratch::get_state<scratch::MultiscalePartitioningEngineViewScratchPad, true>(ctx);
std::vector<Species> fast_species;
fast_species.reserve(all_species.size() - m_dynamic_species.size());
fast_species.reserve(all_species.size() - state->dynamic_species.size());
for (const auto& species : all_species) {
auto it = std::ranges::find(m_dynamic_species, species);
if (it == m_dynamic_species.end()) {
auto it = std::ranges::find(state->dynamic_species, species);
if (it == state->dynamic_species.end()) {
fast_species.push_back(species);
}
}
return fast_species;
}
const std::vector<Species> & MultiscalePartitioningEngineView::getDynamicSpecies() const {
return m_dynamic_species;
const std::vector<Species> & MultiscalePartitioningEngineView::getDynamicSpecies(
scratch::StateBlob& ctx
) {
const auto* state = scratch::get_state<scratch::MultiscalePartitioningEngineViewScratchPad, true>(ctx);
return state->dynamic_species;
}
PrimingReport MultiscalePartitioningEngineView::primeEngine(const NetIn &netIn) {
return m_baseEngine.primeEngine(netIn);
PrimingReport MultiscalePartitioningEngineView::primeEngine(
scratch::StateBlob& ctx,
const NetIn &netIn
) const {
return m_baseEngine.primeEngine(ctx, netIn);
}
bool MultiscalePartitioningEngineView::involvesSpecies(
scratch::StateBlob& ctx,
const Species &species
) const {
if (involvesSpeciesInQSE(species)) return true; // check this first since the vector is likely to be smaller so short circuit cost is less on average
if (involvesSpeciesInDynamic(species)) return true;
) {
if (involvesSpeciesInQSE(ctx, species)) return true; // check this first since the vector is likely to be smaller so short circuit cost is less on average
if (involvesSpeciesInDynamic(ctx, species)) return true;
return false;
}
bool MultiscalePartitioningEngineView::involvesSpeciesInQSE(
scratch::StateBlob& ctx,
const Species &species
) const {
return std::ranges::find(m_algebraic_species, species) != m_algebraic_species.end();
) {
const auto* state = scratch::get_state<scratch::MultiscalePartitioningEngineViewScratchPad, true>(ctx);
return std::ranges::find(state->algebraic_species, species) != state->algebraic_species.end();
}
bool MultiscalePartitioningEngineView::involvesSpeciesInDynamic(
scratch::StateBlob& ctx,
const Species &species
) const {
return std::ranges::find(m_dynamic_species, species) != m_dynamic_species.end();
) {
const auto* state = scratch::get_state<scratch::MultiscalePartitioningEngineViewScratchPad, true>(ctx);
return std::ranges::find(state->dynamic_species, species) != state->dynamic_species.end();
}
fourdst::composition::Composition MultiscalePartitioningEngineView::getNormalizedEquilibratedComposition(
scratch::StateBlob& ctx,
const fourdst::composition::CompositionAbstract& comp,
const double T9,
const double rho,
@@ -1086,54 +1077,69 @@ namespace gridfire::engine {
std::hash<double>()(rho)
};
auto* state = scratch::get_state<scratch::MultiscalePartitioningEngineViewScratchPad, true>(ctx);
const uint64_t composite_hash = XXHash64::hash(hashes.begin(), sizeof(uint64_t) * 3, 0);
if (m_composition_cache.contains(composite_hash)) {
if (state->composition_cache.contains(composite_hash)) {
LOG_TRACE_L3(m_logger, "Cache Hit in Multiscale Partitioning Engine View for composition at T9 = {}, rho = {}.", T9, rho);
return m_composition_cache.at(composite_hash);
return state->composition_cache.at(composite_hash);
}
LOG_TRACE_L3(m_logger, "Cache Miss in Multiscale Partitioning Engine View for composition at T9 = {}, rho = {}. Solving QSE abundances...", T9, rho);
// Only solve if the composition and thermodynamic conditions have not been cached yet
fourdst::composition::Composition qseComposition(solveQSEAbundances(comp, T9, rho));
fourdst::composition::Composition qseComposition(solveQSEAbundances(ctx, comp, T9, rho));
m_composition_cache[composite_hash] = qseComposition;
state->composition_cache[composite_hash] = qseComposition;
return qseComposition;
}
fourdst::composition::Composition MultiscalePartitioningEngineView::collectComposition(
scratch::StateBlob& ctx,
const fourdst::composition::CompositionAbstract &comp,
const double T9,
const double rho
) const {
const fourdst::composition::Composition result = m_baseEngine.collectComposition(comp, T9, rho);
const fourdst::composition::Composition result = m_baseEngine.collectComposition(ctx, comp, T9, rho);
fourdst::composition::Composition qseComposition = getNormalizedEquilibratedComposition(result, T9, rho, false);
fourdst::composition::Composition qseComposition = getNormalizedEquilibratedComposition(ctx, result, T9, rho, false);
return qseComposition;
}
SpeciesStatus MultiscalePartitioningEngineView::getSpeciesStatus(const Species &species) const {
const SpeciesStatus status = m_baseEngine.getSpeciesStatus(species);
if (status == SpeciesStatus::ACTIVE && involvesSpeciesInQSE(species)) {
SpeciesStatus MultiscalePartitioningEngineView::getSpeciesStatus(
scratch::StateBlob& ctx,
const Species &species
) const {
const SpeciesStatus status = m_baseEngine.getSpeciesStatus(ctx, species);
if (status == SpeciesStatus::ACTIVE && involvesSpeciesInQSE(ctx, species)) {
return SpeciesStatus::EQUILIBRIUM;
}
return status;
}
size_t MultiscalePartitioningEngineView::getSpeciesIndex(const Species &species) const {
return m_baseEngine.getSpeciesIndex(species);
std::optional<StepDerivatives<double>> MultiscalePartitioningEngineView::getMostRecentRHSCalculation(
scratch::StateBlob& ctx
) const {
return m_baseEngine.getMostRecentRHSCalculation(ctx);
}
size_t MultiscalePartitioningEngineView::getSpeciesIndex(
scratch::StateBlob& ctx,
const Species &species
) const {
return m_baseEngine.getSpeciesIndex(ctx, species);
}
std::vector<std::vector<Species>> MultiscalePartitioningEngineView::partitionByTimescale(
scratch::StateBlob& ctx,
const fourdst::composition::Composition &comp,
const double T9,
const double rho
) const {
LOG_TRACE_L1(m_logger, "Partitioning by timescale...");
const auto destructionTimescale= m_baseEngine.getSpeciesDestructionTimescales(comp, T9, rho);
const auto netTimescale = m_baseEngine.getSpeciesTimescales(comp, T9, rho);
const auto destructionTimescale= m_baseEngine.getSpeciesDestructionTimescales(ctx, comp, T9, rho);
const auto netTimescale = m_baseEngine.getSpeciesTimescales(ctx, comp, T9, rho);
if (!destructionTimescale || !netTimescale) {
LOG_CRITICAL(m_logger, "Failed to compute species timescales for partitioning due to base engine error.");
@@ -1155,7 +1161,7 @@ namespace gridfire::engine {
}()
);
const auto& all_species = m_baseEngine.getNetworkSpecies();
const auto& all_species = m_baseEngine.getNetworkSpecies(ctx);
std::vector<std::pair<double, Species>> sorted_destruction_timescales;
for (const auto & species : all_species) {
@@ -1311,6 +1317,7 @@ namespace gridfire::engine {
}
std::pair<bool, reaction::ReactionSet> MultiscalePartitioningEngineView::group_is_a_qse_cluster(
scratch::StateBlob& ctx,
const fourdst::composition::Composition &comp,
const double T9,
const double rho,
@@ -1332,8 +1339,8 @@ namespace gridfire::engine {
double coupling_flux = 0.0;
double leakage_flux = 0.0;
for (const auto& reaction: m_baseEngine.getNetworkReactions()) {
const double flow = std::abs(m_baseEngine.calculateMolarReactionFlow(*reaction, comp, T9, rho));
for (const auto& reaction: m_baseEngine.getNetworkReactions(ctx)) {
const double flow = std::abs(m_baseEngine.calculateMolarReactionFlow(ctx, *reaction, comp, T9, rho));
if (flow == 0.0) {
continue; // Skip reactions with zero flow
}
@@ -1422,10 +1429,11 @@ namespace gridfire::engine {
}
bool MultiscalePartitioningEngineView::group_is_a_qse_pipeline(
const fourdst::composition::Composition &comp,
const double T9,
const double rho,
const QSEGroup &group
scratch::StateBlob& ctx,
const fourdst::composition::Composition &comp,
const double T9,
const double rho,
const QSEGroup &group
) const {
// Total fluxes (Standard check)
double total_prod = 0.0;
@@ -1435,8 +1443,8 @@ namespace gridfire::engine {
double charged_prod = 0.0;
double charged_dest = 0.0;
for (const auto& reaction : m_baseEngine.getNetworkReactions()) {
const double flow = m_baseEngine.calculateMolarReactionFlow(*reaction, comp, T9, rho);
for (const auto& reaction : m_baseEngine.getNetworkReactions(ctx)) {
const double flow = m_baseEngine.calculateMolarReactionFlow(ctx, *reaction, comp, T9, rho);
if (std::abs(flow) < 1.0e-99) continue;
int groupNetStoichiometry = 0;
@@ -1476,6 +1484,7 @@ namespace gridfire::engine {
MultiscalePartitioningEngineView::FluxValidationResult MultiscalePartitioningEngineView::validateGroupsWithFluxAnalysis(
scratch::StateBlob& ctx,
const std::vector<QSEGroup> &candidate_groups,
const fourdst::composition::Composition &comp,
const double T9, const double rho
@@ -1487,10 +1496,10 @@ namespace gridfire::engine {
group_reactions.reserve(candidate_groups.size());
for (auto& group : candidate_groups) {
// Values for measuring the flux coupling vs leakage
auto [leakage_coupled, group_reaction_set] = group_is_a_qse_cluster(comp, T9, rho, group);
auto [leakage_coupled, group_reaction_set] = group_is_a_qse_cluster(ctx, comp, T9, rho, group);
bool is_flow_balanced = group_is_a_qse_pipeline(comp, T9, rho, group);
bool is_flow_balanced = group_is_a_qse_pipeline(ctx, comp, T9, rho, group);
if (leakage_coupled) {
LOG_TRACE_L1(m_logger, "{} is in equilibrium due to high coupling flux", group.toString(false));
@@ -1516,21 +1525,23 @@ namespace gridfire::engine {
}
fourdst::composition::Composition MultiscalePartitioningEngineView::solveQSEAbundances(
scratch::StateBlob& ctx,
const fourdst::composition::CompositionAbstract &comp,
const double T9,
const double rho
) const {
LOG_TRACE_L2(m_logger, "Solving for QSE abundances...");
auto* state = scratch::get_state<scratch::MultiscalePartitioningEngineViewScratchPad, true>(ctx);
fourdst::composition::Composition outputComposition(comp);
std::vector<Species> species;
std::vector<double> abundances;
species.reserve(m_algebraic_species.size());
abundances.reserve(m_algebraic_species.size());
species.reserve(state->algebraic_species.size());
abundances.reserve(state->algebraic_species.size());
for (const auto& [group, solver]: std::views::zip(m_qse_groups, m_qse_solvers)) {
const fourdst::composition::Composition& groupResult = solver->solve(outputComposition, T9, rho);
for (const auto& [group, solver]: std::views::zip(state->qse_groups, state->qse_solvers)) {
const fourdst::composition::Composition& groupResult = solver->solve(ctx, outputComposition, T9, rho);
for (const auto& [sp, y] : groupResult) {
if (!std::isfinite(y)) {
LOG_CRITICAL(m_logger, "Non-finite abundance {} computed for species {} in QSE group solve at T9 = {}, rho = {}.",
@@ -1553,12 +1564,13 @@ namespace gridfire::engine {
}
size_t MultiscalePartitioningEngineView::identifyMeanSlowestPool(
scratch::StateBlob& ctx,
const std::vector<std::vector<Species>> &pools,
const fourdst::composition::Composition &comp,
const double T9,
const double rho
) const {
const auto& result = m_baseEngine.getSpeciesDestructionTimescales(comp, T9, rho);
const auto& result = m_baseEngine.getSpeciesDestructionTimescales(ctx, comp, T9, rho);
if (!result) {
LOG_CRITICAL(m_logger, "Failed to get species destruction timescales due base engine failure");
m_logger->flush_log();
@@ -1603,6 +1615,7 @@ namespace gridfire::engine {
}
std::unordered_map<Species, std::vector<Species>> MultiscalePartitioningEngineView::buildConnectivityGraph(
scratch::StateBlob& ctx,
const std::vector<Species> &species_pool,
const fourdst::composition::Composition &comp,
double T9,
@@ -1622,7 +1635,7 @@ namespace gridfire::engine {
std::map<size_t, std::vector<reaction::LogicalReaclibReaction*>> speciesReactionMap;
std::vector<const reaction::LogicalReaclibReaction*> candidate_reactions;
for (const auto& reaction : m_baseEngine.getNetworkReactions()) {
for (const auto& reaction : m_baseEngine.getNetworkReactions(ctx)) {
const std::vector<Species> &reactants = reaction->reactants();
const std::vector<Species> &products = reaction->products();
@@ -1660,13 +1673,14 @@ namespace gridfire::engine {
}
std::vector<MultiscalePartitioningEngineView::QSEGroup> MultiscalePartitioningEngineView::constructCandidateGroups(
scratch::StateBlob& ctx,
const std::vector<std::vector<Species>> &candidate_pools,
const fourdst::composition::Composition &comp,
const double T9,
const double rho
) const {
const auto& all_reactions = m_baseEngine.getNetworkReactions();
const auto& result = m_baseEngine.getSpeciesDestructionTimescales(comp, T9, rho);
const auto& all_reactions = m_baseEngine.getNetworkReactions(ctx);
const auto& result = m_baseEngine.getSpeciesDestructionTimescales(ctx, comp, T9, rho);
if (!result) {
LOG_ERROR(m_logger, "Failed to get species destruction timescales due base engine failure");
m_logger->flush_log();
@@ -1694,7 +1708,7 @@ namespace gridfire::engine {
}
}
if (has_external_reactant) {
double flow = std::abs(m_baseEngine.calculateMolarReactionFlow(*reaction, comp, T9, rho));
double flow = std::abs(m_baseEngine.calculateMolarReactionFlow(ctx, *reaction, comp, T9, rho));
LOG_TRACE_L3(m_logger, "Found bridge reaction {} with flow {} for species {}.", reaction->id(), flow, ash.name());
bridge_reactions.emplace_back(reaction.get(), flow);
}
@@ -1872,6 +1886,7 @@ namespace gridfire::engine {
}
fourdst::composition::Composition MultiscalePartitioningEngineView::QSESolver::solve(
scratch::StateBlob& ctx,
const fourdst::composition::Composition &comp,
const double T9,
const double rho
@@ -1885,7 +1900,8 @@ namespace gridfire::engine {
result,
m_speciesMap,
m_species,
*this
*this,
ctx
};
utils::check_sundials_flag(KINSetUserData(m_kinsol_mem, &data), "KINSetUserData", utils::SUNDIALS_RET_CODE_TYPES::KINSOL);
@@ -1905,9 +1921,9 @@ namespace gridfire::engine {
}
StepDerivatives<double> rhsGuess;
auto cached_rhs = m_engine.getMostRecentRHSCalculation();
auto cached_rhs = m_engine.getMostRecentRHSCalculation(ctx);
if (!cached_rhs) {
const auto initial_rhs = m_engine.calculateRHSAndEnergy(result, T9, rho, false);
const auto initial_rhs = m_engine.calculateRHSAndEnergy(ctx, result, T9, rho, false);
if (!initial_rhs) {
throw std::runtime_error("In QSE solver failed to calculate initial RHS for caching");
}
@@ -2063,6 +2079,16 @@ namespace gridfire::engine {
getLogger()->flush_log(true);
}
std::unique_ptr<MultiscalePartitioningEngineView::QSESolver> MultiscalePartitioningEngineView::QSESolver::clone() const {
auto new_solver = std::make_unique<QSESolver>(m_species, m_engine, m_sun_ctx);
return new_solver;
}
std::unique_ptr<MultiscalePartitioningEngineView::QSESolver> MultiscalePartitioningEngineView::QSESolver::clone(SUNContext sun_ctx) const {
auto new_solver = std::make_unique<QSESolver>(m_species, m_engine, sun_ctx);
return new_solver;
}
int MultiscalePartitioningEngineView::QSESolver::sys_func(
const N_Vector y,
@@ -2086,7 +2112,7 @@ namespace gridfire::engine {
data->comp.setMolarAbundance(species, y_data[index]);
}
const auto result = data->engine.calculateRHSAndEnergy(data->comp, data->T9, data->rho, false);
const auto result = data->engine.calculateRHSAndEnergy(data->ctx, data->comp, data->T9, data->rho, false);
if (!result) {
return 1; // Potentially recoverable error
@@ -2102,7 +2128,7 @@ namespace gridfire::engine {
for (const auto &s: map | std::views::keys) {
const double v = dydt.at(s);
if (!std::isfinite(v)) {
invalid_species.push_back(std::make_pair(s, v));
invalid_species.emplace_back(s, v);
}
}
std::string msg = std::format("Non-finite dydt values encountered for species: {}",
@@ -2150,6 +2176,7 @@ namespace gridfire::engine {
}
const NetworkJacobian jac = data->engine.generateJacobianMatrix(
data->ctx,
data->comp,
data->T9,
data->rho,
@@ -2159,9 +2186,6 @@ namespace gridfire::engine {
sunrealtype* J_data = SUNDenseMatrix_Data(J);
const sunindextype N = SUNDenseMatrix_Columns(J);
if (data->row_scaling_factors.size() != static_cast<size_t>(N)) {
data->row_scaling_factors.resize(N, 0.0);
}
for (const auto& [row_species, row_idx]: map) {
double max_value = std::numeric_limits<double>::lowest();