fix(MultiscalePartitioningEngineView): began work to call the QSE solver every timestep

one major issue was that QSE solving was only running at each partition. This was creating effectivley infinite sources of partitioned species. Now we partition when engine updates are triggered, however, solveQSEAbundance is called every timestep. This has major performance implications and so has required a lot of optimization to make it even somewhat viable. For now construction is much slower. Time per iteration is still slower than it was before; however, it is tractable. There is also currently too much stiffness in the network. This is likeley a bug that was introduced in this refactoring which will be addressed soon.
This commit is contained in:
2025-11-12 16:54:12 -05:00
parent 741a11d256
commit 81cca35130
20 changed files with 446 additions and 697 deletions

View File

@@ -62,6 +62,7 @@ namespace gridfire {
struct StepDerivatives {
std::map<fourdst::atomic::Species, T> dydt{}; ///< Derivatives of abundances (dY/dt for each species).
T nuclearEnergyGenerationRate = T(0.0); ///< Specific energy generation rate (e.g., erg/g/s).
std::map<fourdst::atomic::Species, std::unordered_map<std::string, T>> reactionContributions{};
StepDerivatives() : dydt(), nuclearEnergyGenerationRate(T(0.0)) {}
};
@@ -406,11 +407,15 @@ namespace gridfire {
* @note It is up to each engine to decide how to handle filling in the return composition.
* @note These methods return an unfinalized composition which must then be finalized by the caller
* @param comp Input composition to "normalize".
* @param T9
* @param rho
* @return An updated composition which is a superset of comp. This may contain species which were culled, for
* example, by either QSE partitioning or reaction flow rate culling
*/
virtual fourdst::composition::Composition collectComposition(
fourdst::composition::CompositionAbstract &comp
const fourdst::composition::CompositionAbstract &comp,
double T9,
double rho
) const = 0;
};

View File

@@ -744,11 +744,15 @@ namespace gridfire {
* can be tracked by an instance of GraphEngine are registered in the composition object.
* @note If a species is in the input comp but not in the network
* @param comp Input Composition
* @param T9
* @param rho
* @param T9
* @param rho
* @return A new composition where all members of the active species set are registered. And any members not in comp
* have a molar abundance set to 0.
* @throws BadCollectionError If the input composition contains species not present in the network species set
*/
fourdst::composition::Composition collectComposition(fourdst::composition::CompositionAbstract &comp) const override;
fourdst::composition::Composition collectComposition(const fourdst::composition::CompositionAbstract &comp, double T9, double rho) const override;
private:
@@ -1159,9 +1163,11 @@ namespace gridfire {
const T c = static_cast<T>(m_constants.c); // Speed of light in cm/s
// --- SINGLE LOOP OVER ALL REACTIONS ---
StepDerivatives<T> result{};
for (size_t reactionIndex = 0; reactionIndex < m_reactions.size(); ++reactionIndex) {
bool skipReaction = false;
const auto& reaction = m_reactions[reactionIndex];
if (!reactionLookup(reaction)) {
continue; // Skip this reaction if not in the "active" reaction set
}
@@ -1174,9 +1180,6 @@ namespace gridfire {
if (skipReaction) {
continue; // Skip this reaction if any reactant is not present
}
if (reaction.type() == reaction::ReactionType::WEAK && !m_useReverseReactions) {
continue; // Skip weak reactions if reverse reactions are disabled
}
// 1. Calculate forward reaction rate
const T forwardMolarReactionFlow = screeningFactors[reactionIndex] *
@@ -1193,7 +1196,7 @@ namespace gridfire {
// 2. Calculate reverse reaction rate
T reverseMolarFlow = static_cast<T>(0.0);
// Do not calculate reverse flow for weak reactions since photodisintegration does not apply
if (reaction.type() == reaction::ReactionType::LOGICAL_REACLIB || reaction.type() == reaction::ReactionType::REACLIB) {
if ((reaction.type() == reaction::ReactionType::LOGICAL_REACLIB || reaction.type() == reaction::ReactionType::REACLIB) && m_useReverseReactions) {
reverseMolarFlow = calculateReverseMolarReactionFlow<T>(
T9,
rho,
@@ -1204,19 +1207,21 @@ namespace gridfire {
);
}
const T molarReactionFlow = forwardMolarReactionFlow - reverseMolarFlow; // Net molar reaction flow
// 3. Use the rate to update all relevant species derivatives (dY/dt)
for (size_t speciesIdx = 0; speciesIdx < m_networkSpecies.size(); ++speciesIdx) {
const auto& species = m_networkSpecies[speciesIdx];
const T nu_ij = static_cast<T>(reaction.stoichiometry(species));
dydt_vec[speciesIdx] += threshold_flag * molarReactionFlow * nu_ij;
const T dydt_increment = threshold_flag * molarReactionFlow * nu_ij;
dydt_vec[speciesIdx] += dydt_increment;
result.reactionContributions[species][std::string(reaction.id())] = dydt_increment;
}
}
T massProductionRate = static_cast<T>(0.0); // [mol][s^-1]
StepDerivatives<T> result{};
for (const auto& [species, deriv] : std::views::zip(m_networkSpecies, dydt_vec)) {
massProductionRate += deriv * species.mass() * u;
result.dydt[species] = deriv; // [mol][s^-1][g^-1]
@@ -1272,6 +1277,7 @@ namespace gridfire {
if (count > 1) {
molar_concentration_product /= static_cast<T>(std::tgamma(static_cast<double>(count + 1))); // Gamma function for factorial
}
}
// --- Final reaction flow calculation [mol][s^-1][g^-1] ---
// Note: If the threshold flag ever gets set to zero this will return zero.

View File

@@ -303,7 +303,7 @@ namespace gridfire {
[[nodiscard]] PrimingReport primeEngine(const NetIn &netIn) override;
fourdst::composition::Composition collectComposition(fourdst::composition::CompositionAbstract &comp) const override;
fourdst::composition::Composition collectComposition(const fourdst::composition::CompositionAbstract &comp, double T9, double rho) const override;
private:
using Config = fourdst::config::Config;
using LogManager = fourdst::logging::LogManager;

View File

@@ -221,7 +221,7 @@ namespace gridfire{
[[nodiscard]] PrimingReport primeEngine(const NetIn &netIn) override;
fourdst::composition::Composition collectComposition(fourdst::composition::CompositionAbstract &comp) const override;
fourdst::composition::Composition collectComposition(const fourdst::composition::CompositionAbstract &comp, double T9, double rho) const override;
protected:
bool m_isStale = true;
GraphEngine& m_baseEngine;

View File

@@ -6,123 +6,6 @@
#include "unsupported/Eigen/NonLinearOptimization"
namespace gridfire {
/**
* @brief Configuration struct for the QSE cache.
*
* @par Purpose
* This struct defines the tolerances used to determine if a QSE cache key
* is considered a hit. It allows for tuning the sensitivity of the cache.
*
* @par How
* It works by providing binning widths for temperature, density, and abundances.
* When a `QSECacheKey` is created, it uses these tolerances to discretize the
* continuous physical values into bins. If two sets of conditions fall into the
* same bins, they will produce the same hash and be considered a cache hit.
*
* @par Usage Example:
* Although not typically set by the user directly, the `QSECacheKey` uses this
* internally. A smaller tolerance (e.g., `T9_tol = 1e-4`) makes the cache more
* sensitive, leading to more frequent re-partitions, while a larger tolerance
* (`T9_tol = 1e-2`) makes it less sensitive.
*/
struct QSECacheConfig {
double T9_tol; ///< Absolute tolerance to produce the same hash for T9.
double rho_tol; ///< Absolute tolerance to produce the same hash for rho.
double Yi_tol; ///< Absolute tolerance to produce the same hash for species abundances.
};
/**
* @brief Key struct for the QSE abundance cache.
*
* @par Purpose
* This struct is used as the key for the QSE abundance cache (`m_qse_abundance_cache`)
* within the `MultiscalePartitioningEngineView`. Its primary goal is to avoid
* expensive re-partitioning and QSE solves for thermodynamic conditions that are
* "close enough" to previously computed ones.
*
* @par How
* It works by storing the temperature (`m_T9`), density (`m_rho`), and species
* abundances (`m_Y`). A pre-computed hash is generated in the constructor by
* calling the `hash()` method. This method discretizes the continuous physical
* values into bins using the tolerances defined in `QSECacheConfig`. The `operator==`
* simply compares the pre-computed hash values for fast lookups in the `std::unordered_map`.
*/
struct QSECacheKey {
double m_T9;
double m_rho;
std::vector<double> m_Y; ///< Note that the ordering of Y must match the dynamic species indices in the view.
std::size_t m_hash = 0; ///< Precomputed hash value for this key.
// TODO: We should probably sort out how to adjust these from absolute to relative tolerances.
QSECacheConfig m_cacheConfig = {
1e-10, // Default tolerance for T9
1e-10, // Default tolerance for rho
1e-10 // Default tolerance for species abundances
};
/**
* @brief Constructs a QSECacheKey.
*
* @param T9 Temperature in units of 10^9 K.
* @param rho Density in g/cm^3.
* @param Y Species molar abundances.
*
* @post The `m_hash` member is computed and stored.
*/
QSECacheKey(
const double T9,
const double rho,
const std::vector<double>& Y
);
/**
* @brief Computes the hash value for this key.
*
* @return The computed hash value.
*
* @par How
* This method combines the hashes of the binned temperature, density, and
* each species abundance. The `bin()` static method is used for discretization.
*/
[[nodiscard]] size_t hash() const;
/**
* @brief Converts a value to a discrete bin based on a tolerance.
* @param value The value to bin.
* @param tol The tolerance (bin width) to use for binning.
* @return The bin number as a long integer.
*
* @par How
* The algorithm is `floor(value / tol)`.
*/
static long bin(double value, double tol);
/**
* @brief Equality operator for QSECacheKey.
* @param other The other QSECacheKey to compare to.
* @return True if the pre-computed hashes are equal, false otherwise.
*/
bool operator==(const QSECacheKey& other) const;
};
}
// Needs to be in this order (splitting gridfire namespace up) to avoid some issues with forward declarations and the () operator.
template <>
struct std::hash<gridfire::QSECacheKey> {
/**
* @brief Computes the hash of a QSECacheKey for use in `std::unordered_map`.
* @param key The QSECacheKey to hash.
* @return The pre-computed hash value of the key.
*/
size_t operator()(const gridfire::QSECacheKey& key) const noexcept {
// The hash is pre-computed, so we just return it.
return key.m_hash;
}
}; // namespace std
namespace gridfire {
/**
* @class MultiscalePartitioningEngineView
@@ -569,66 +452,6 @@ namespace gridfire {
*/
const DynamicEngine & getBaseEngine() const override;
/**
* @brief Analyzes the connectivity of timescale pools.
*
* @param timescale_pools A vector of vectors of species indices, where each inner vector
* represents a timescale pool.
* @param comp Vector of current molar abundances for the full network.
* @param T9 Temperature in units of 10^9 K.
* @param rho Density in g/cm^3.
* @return A vector of vectors of species indices, where each inner vector represents a
* single connected component.
*
* @par Purpose
* To merge timescale pools that are strongly connected by reactions, forming
* cohesive groups for QSE analysis.
*
* @par How
* For each pool, it builds a reaction connectivity graph using `buildConnectivityGraph`.
* It then finds the connected components within that graph using a Breadth-First Search (BFS).
* The resulting components from all pools are collected and returned.
*/
std::vector<std::vector<fourdst::atomic::Species>> analyzeTimescalePoolConnectivity(
const std::vector<std::vector<fourdst::atomic::Species>> &timescale_pools,
const fourdst::composition::Composition &comp,
double T9,
double rho
) const;
/**
* @brief Partitions the network into dynamic and algebraic (QSE) groups based on timescales.
*
* @param comp Vector of current molar abundances for the full network.
* @param T9 Temperature in units of 10^9 K.
* @param rho Density in g/cm^3.
*
* @par Purpose
* To perform the core partitioning logic that identifies which species are "fast"
* (and can be treated algebraically) and which are "slow" (and must be integrated dynamically).
*
* @how
* 1. **`partitionByTimescale`**: Gets species destruction timescales from the base engine,
* sorts them, and looks for large gaps to create timescale "pools".
* 2. **`identifyMeanSlowestPool`**: The pool with the slowest average timescale is designated
* as the core set of dynamic species.
* 3. **`analyzeTimescalePoolConnectivity`**: The other (faster) pools are analyzed for
* reaction connectivity to form cohesive groups.
* 4. **`constructCandidateGroups`**: These connected groups are processed to identify "seed"
* species (dynamic species that feed the group) and "algebraic" species (the rest).
* 5. **`validateGroupsWithFluxAnalysis`**: The groups are validated by ensuring their internal
* reaction flux is much larger than the flux connecting them to the outside network.
*
* @pre The input state (Y, T9, rho) must be a valid physical state.
* @post The internal member variables `m_qse_groups`, `m_dynamic_species`, and
* `m_algebraic_species` (and their index maps) are populated with the results of the
* partitioning.
*/
void partitionNetwork(
const fourdst::composition::Composition &comp,
double T9,
double rho
);
/**
* @brief Partitions the network based on timescales from a `NetIn` struct.
@@ -642,8 +465,8 @@ namespace gridfire {
* It unpacks the `netIn` struct into `Y`, `T9`, and `rho` and then calls the
* primary `partitionNetwork` method.
*/
void partitionNetwork(
const NetIn& netIn
fourdst::composition::Composition partitionNetwork(
const NetIn &netIn
);
/**
@@ -734,49 +557,6 @@ namespace gridfire {
*/
[[nodiscard]] const std::vector<fourdst::atomic::Species>& getDynamicSpecies() const;
/**
* @brief Equilibrates the network by partitioning and solving for QSE abundances.
*
* @param comp Vector of current molar abundances for the full network.
* @param T9 Temperature in units of 10^9 K.
* @param rho Density in g/cm^3.
* @return A new composition object with the equilibrated abundances.
*
* @par Purpose
* A convenience method to run the full QSE analysis and get an equilibrated
* composition object as a result.
*
* @par How
* It first calls `partitionNetwork()` with the given state to define the QSE groups.
* Then, it calls `solveQSEAbundances()` to compute the new equilibrium abundances for the
* algebraic species. Finally, it packs the resulting full abundance vector into a new
* `fourdst::composition::Composition` object and returns it.
*
* @pre The input state (Y, T9, rho) must be a valid physical state.
* @post The engine's internal partition is updated. A new composition object is returned.
*/
fourdst::composition::Composition equilibrateNetwork(
const fourdst::composition::Composition &comp,
double T9,
double rho
);
/**
* @brief Equilibrates the network using QSE from a `NetIn` struct.
*
* @param netIn A struct containing the current network input.
* @return The equilibrated composition.
*
* @par Purpose
* A convenience overload for `equilibrateNetwork`.
*
* @par How
* It unpacks the `netIn` struct into `Y`, `T9`, and `rho` and then calls the
* primary `equilibrateNetwork` method.
*/
fourdst::composition::Composition equilibrateNetwork(
const NetIn &netIn
);
bool involvesSpecies(const fourdst::atomic::Species &species) const;
@@ -784,15 +564,19 @@ namespace gridfire {
bool involvesSpeciesInDynamic(const fourdst::atomic::Species &species) const;
fourdst::composition::Composition getNormalizedEquilibratedComposition(const fourdst::composition::CompositionAbstract& comp, double T9, double rho) const;
/**
* @brief Collect the composition from this and sub engines.
* @details This method operates by injecting the current equilibrium abundances for algebraic species into
* the composition object so that they can be bubbled up to the caller.
* @param comp Input Composition
* @param T9
* @param rho
* @return New composition which is comp + any edits from lower levels + the equilibrium abundances of all algebraic species.
* @throws BadCollectionError: if there is a species in the algebraic species set which does not show up in the reported composition from the base engine.:w
*/
fourdst::composition::Composition collectComposition(fourdst::composition::CompositionAbstract &comp) const override;
fourdst::composition::Composition collectComposition(const fourdst::composition::CompositionAbstract &comp, double T9, double rho) const override;
private:
@@ -878,7 +662,7 @@ namespace gridfire {
/**
* @brief Pointer to the MultiscalePartitioningEngineView instance.
*/
MultiscalePartitioningEngineView* m_view;
const MultiscalePartitioningEngineView& m_view;
/**
* @brief The set of species to solve for in the QSE group.
*/
@@ -886,7 +670,7 @@ namespace gridfire {
/**
* @brief Initial abundances of all species in the full network.
*/
const fourdst::composition::Composition& m_initial_comp;
const fourdst::composition::CompositionAbstract& m_initial_comp;
/**
* @brief Temperature in units of 10^9 K.
*/
@@ -917,15 +701,15 @@ namespace gridfire {
* @param qse_solve_species_index_map Mapping from species to their indices in the QSE solve vector.
*/
EigenFunctor(
MultiscalePartitioningEngineView& view,
const MultiscalePartitioningEngineView& view,
const std::set<fourdst::atomic::Species>& qse_solve_species,
const fourdst::composition::Composition& initial_comp,
const fourdst::composition::CompositionAbstract& initial_comp,
const double T9,
const double rho,
const Eigen::VectorXd& Y_scale,
const std::unordered_map<fourdst::atomic::Species, size_t>& qse_solve_species_index_map
) :
m_view(&view),
m_view(view),
m_qse_solve_species(qse_solve_species),
m_initial_comp(initial_comp),
m_T9(T9),
@@ -960,98 +744,6 @@ namespace gridfire {
int df(const InputType& v_qse, JacobianType& J_qse) const;
};
/**
* @brief Struct for tracking cache statistics.
*
* @par Purpose
* A simple utility to monitor the performance of the QSE cache by counting
* hits and misses for various engine operations.
*/
struct CacheStats {
enum class operators {
CalculateRHSAndEnergy,
GenerateJacobianMatrix,
CalculateMolarReactionFlow,
GetSpeciesTimescales,
GetSpeciesDestructionTimescales,
Other,
All
};
/**
* @brief Map from operators to their string names for logging.
*/
std::map<operators, std::string> operatorsNameMap = {
{operators::CalculateRHSAndEnergy, "calculateRHSAndEnergy"},
{operators::GenerateJacobianMatrix, "generateJacobianMatrix"},
{operators::CalculateMolarReactionFlow, "calculateMolarReactionFlow"},
{operators::GetSpeciesTimescales, "getSpeciesTimescales"},
{operators::GetSpeciesDestructionTimescales, "getSpeciesDestructionTimescales"},
{operators::Other, "other"}
};
/**
* @brief Total number of cache hits.
*/
size_t m_hit = 0;
/**
* @brief Total number of cache misses.
*/
size_t m_miss = 0;
/**
* @brief Map from operators to the number of cache hits for that operator.
*/
std::map<operators, size_t> m_operatorHits = {
{operators::CalculateRHSAndEnergy, 0},
{operators::GenerateJacobianMatrix, 0},
{operators::CalculateMolarReactionFlow, 0},
{operators::GetSpeciesTimescales, 0},
{operators::GetSpeciesDestructionTimescales, 0},
{operators::Other, 0}
};
/**
* @brief Map from operators to the number of cache misses for that operator.
*/
std::map<operators, size_t> m_operatorMisses = {
{operators::CalculateRHSAndEnergy, 0},
{operators::GenerateJacobianMatrix, 0},
{operators::CalculateMolarReactionFlow, 0},
{operators::GetSpeciesTimescales, 0},
{operators::GetSpeciesDestructionTimescales, 0},
{operators::Other, 0}
};
/**
* @brief Increments the hit counter for a given operator.
* @param op The operator that resulted in a cache hit.
* @throws std::invalid_argument if `op` is `All`.
*/
void hit(const operators op=operators::Other);
/**
* @brief Increments the miss counter for a given operator.
* @param op The operator that resulted in a cache miss.
* @throws std::invalid_argument if `op` is `All`.
*/
void miss(const operators op=operators::Other);
/**
* @brief Gets the number of hits for a specific operator or all operators.
* @param op The operator to get the number of hits for. Defaults to `All`.
* @return The number of hits.
*/
[[nodiscard]] size_t hits(const operators op=operators::All) const;
/**
* @brief Gets the number of misses for a specific operator or all operators.
* @param op The operator to get the number of misses for. Defaults to `All`.
* @return The number of misses.
*/
[[nodiscard]] size_t misses(const operators op=operators::All) const;
};
private:
/**
* @brief Logger instance for logging messages.
@@ -1088,22 +780,7 @@ namespace gridfire {
*/
std::vector<size_t> m_activeReactionIndices;
/**
* @brief Cache for QSE abundances based on T9, rho, and Y.
*
* @par Purpose
* This is the core of the caching mechanism. It stores the results of QSE solves
* to avoid re-computation. The key is a `QSECacheKey` which hashes the thermodynamic
* state, and the value is the vector of solved molar abundances for the algebraic species.
*/
mutable std::unordered_map<QSECacheKey, std::vector<double>> m_qse_abundance_cache;
/**
* @brief Statistics for the QSE abundance cache.
*/
mutable CacheStats m_cacheStats;
mutable std::unordered_map<uint64_t, fourdst::composition::Composition> m_composition_cache;
private:
/**
@@ -1150,9 +827,7 @@ namespace gridfire {
* flux exceeds a configurable threshold, the group is considered valid and is added
* to the returned vector.
*/
std::pair<std::vector<MultiscalePartitioningEngineView::QSEGroup>, std::vector<MultiscalePartitioningEngineView
::
QSEGroup>> validateGroupsWithFluxAnalysis(
std::pair<std::vector<QSEGroup>, std::vector<QSEGroup>> validateGroupsWithFluxAnalysis(
const std::vector<QSEGroup> &candidate_groups,
const fourdst::composition::Composition &comp,
double T9,
@@ -1180,10 +855,10 @@ namespace gridfire {
* @post The algebraic species in the QSE cache are updated with the new equilibrium abundances.
*/
fourdst::composition::Composition solveQSEAbundances(
const fourdst::composition::Composition &comp,
const fourdst::composition::CompositionAbstract &comp,
double T9,
double rho
);
) const;
/**
* @brief Identifies the pool with the slowest mean timescale.
@@ -1253,6 +928,27 @@ namespace gridfire {
double T9,
double rho
) const;
/**
* @brief Analyzes the connectivity of timescale pools.
*
* @param timescale_pools A vector of vectors of species indices, where each inner vector
* represents a timescale pool.
* @return A vector of vectors of species indices, where each inner vector represents a
* single connected component.
*
* @par Purpose
* To merge timescale pools that are strongly connected by reactions, forming
* cohesive groups for QSE analysis.
*
* @par How
* For each pool, it builds a reaction connectivity graph using `buildConnectivityGraph`.
* It then finds the connected components within that graph using a Breadth-First Search (BFS).
* The resulting components from all pools are collected and returned.
*/
std::vector<std::vector<fourdst::atomic::Species>> analyzeTimescalePoolConnectivity(
const std::vector<std::vector<fourdst::atomic::Species>> &timescale_pools
) const;
};
}

View File

@@ -0,0 +1,22 @@
#pragma once
#include <exception>
#include <string>
#include <format>
namespace gridfire::exceptions {
class ReactionError : public std::exception {
private:
std::string m_message;
std::string m_reactionID;
public:
ReactionError(const std::string& msg, const std::string& reactionId): m_message(msg), m_reactionID(reactionId) {}
const char* what() const noexcept override {
return std::format("Reaction {}: {}", m_reactionID, m_message).c_str();
}
};
class ReactionParsingError final : public ReactionError {
using ReactionError::ReactionError;
};
}

View File

@@ -0,0 +1,21 @@
#pragma once
#include <exception>
#include <string>
namespace gridfire::exceptions {
class SolverError : public std::exception {
public:
SolverError(std::string msg) : m_msg(std::move(msg)) {}
[[nodiscard]] const char* what() const noexcept override {
return m_msg.c_str();
}
private:
std::string m_msg;
};
class CVODESolverFailureError final : public SolverError {
using SolverError::SolverError;
};
}

View File

@@ -521,13 +521,31 @@ namespace gridfire::reaction {
* @brief Gets the vector of reactant species.
* @return A const reference to the vector of reactants.
*/
[[nodiscard]] const std::vector<fourdst::atomic::Species>& reactants() const override { return m_reactants; }
[[nodiscard]] const std::vector<fourdst::atomic::Species>& reactants() const override {
if (!m_reactantsVec) {
m_reactantsVec.emplace(std::vector<fourdst::atomic::Species>());
m_reactantsVec->reserve(m_reactants.size());
for (const auto& reactant : m_reactants) {
m_reactantsVec->push_back(reactant);
}
}
return m_reactantsVec.value();
}
/**
* @brief Gets the vector of product species.
* @return A const reference to the vector of products.
*/
[[nodiscard]] const std::vector<fourdst::atomic::Species>& products() const override { return m_products; }
[[nodiscard]] const std::vector<fourdst::atomic::Species>& products() const override {
if (!m_productsVec) {
m_productsVec.emplace(std::vector<fourdst::atomic::Species>());
m_productsVec->reserve(m_products.size());
for (const auto& product : m_products) {
m_productsVec->push_back(product);
}
}
return m_productsVec.value();
}
/**
* @brief Checks if this is a reverse reaction rate.
@@ -575,8 +593,12 @@ namespace gridfire::reaction {
std::string m_peName; ///< Name of the reaction in (projectile, ejectile) notation (e.g. "p(p,g)d").
int m_chapter; ///< Chapter number from the REACLIB database, defining the reaction structure.
double m_qValue = 0.0; ///< Q-value of the reaction in MeV.
std::vector<fourdst::atomic::Species> m_reactants; ///< Reactants of the reaction.
std::vector<fourdst::atomic::Species> m_products; ///< Products of the reaction.
std::set<fourdst::atomic::Species> m_reactants; ///< Reactants of the reaction.
std::set<fourdst::atomic::Species> m_products; ///< Products of the reaction.
mutable std::optional<std::vector<fourdst::atomic::Species>> m_reactantsVec;
mutable std::optional<std::vector<fourdst::atomic::Species>> m_productsVec;
std::string m_sourceLabel; ///< Source label for the rate data (e.g., "wc12w", "st08").
RateCoefficientSet m_rateCoefficients; ///< The seven rate coefficients.
bool m_reverse = false; ///< Flag indicating if this is a reverse reaction rate.
@@ -746,7 +768,6 @@ namespace gridfire::reaction {
rate.a5 * T953 +
rate.a6 * logT9;
sum += CppAD::exp(exponent);
// return sum; // TODO: REMOVE OR COMMENT THIS. ITS FOR TESTING ONLY
}
return sum;
}
@@ -912,6 +933,7 @@ namespace gridfire::reaction {
std::vector<std::unique_ptr<Reaction>> m_reactions;
std::string m_id;
std::unordered_map<std::string, size_t> m_reactionNameMap; ///< Maps reaction IDs to Reaction objects for quick lookup.
std::unordered_set<size_t> m_reactionHashes;
};

View File

@@ -177,6 +177,7 @@ namespace gridfire::solver {
const std::vector<fourdst::atomic::Species>& networkSpecies; ///< Species layout.
const size_t currentConvergenceFailures; ///< Total number of convergence failures
const size_t currentNonlinearIterations; ///< Total number of non-linear iterations
const std::map<fourdst::atomic::Species, std::unordered_map<std::string, double>>& reactionContributionMap; ///< Map of reaction contributions for the current step
/**
* @brief Construct a context snapshot.
@@ -192,7 +193,8 @@ namespace gridfire::solver {
const DynamicEngine& engine,
const std::vector<fourdst::atomic::Species>& networkSpecies,
size_t currentConvergenceFailure,
size_t currentNonlinearIterations
size_t currentNonlinearIterations,
const std::map<fourdst::atomic::Species, std::unordered_map<std::string, double>> &reactionContributionMap
);
/**
@@ -223,6 +225,11 @@ namespace gridfire::solver {
double energy{};
const std::vector<fourdst::atomic::Species>* networkSpecies{};
std::unique_ptr<exceptions::StaleEngineTrigger> captured_exception = nullptr;
std::optional<std::map<fourdst::atomic::Species, std::unordered_map<std::string, double>>> reaction_contribution_map;
};
struct CVODERHSOutputData {
std::map<fourdst::atomic::Species, std::unordered_map<std::string, double>> reaction_contribution_map;
};
private:
@@ -248,7 +255,7 @@ namespace gridfire::solver {
* engine.calculateRHSAndEnergy(T9, rho). Negative small abundances are clamped to zero
* before constructing Composition. On stale engine, throws exceptions::StaleEngineTrigger.
*/
void calculate_rhs(sunrealtype t, N_Vector y, N_Vector ydot, const CVODEUserData* data) const;
CVODERHSOutputData calculate_rhs(sunrealtype t, N_Vector y, N_Vector ydot, const CVODEUserData *data) const;
/**
* @brief Allocate and initialize CVODE vectors, linear algebra, tolerances, and constraints.

View File

@@ -98,7 +98,7 @@ namespace gridfire {
// --- The public facing interface can always use the precomputed version since taping is done internally ---
return calculateAllDerivativesUsingPrecomputation(comp, bare_rates, bare_reverse_rates, T9, rho, activeReactions);
} else {
return calculateAllDerivatives<double>(
StepDerivatives<double> result = calculateAllDerivatives<double>(
comp.getMolarAbundanceVector(),
T9,
rho,
@@ -115,6 +115,7 @@ namespace gridfire {
return false;
}
);
return result;
}
}
@@ -238,8 +239,6 @@ namespace gridfire {
throw std::runtime_error("Species not found in global atomic species database: " + std::string(name));
}
}
// TODO: Currently this works. We sort the vector based on mass so that for the same set of species we always get the same ordering and we get the same ordering as a composition with the same set of species
// However, we need some checks so that when we get a composition we confirm that it is the same ordering / contains the same species. This is important for the ODE integrator to work properly.
std::ranges::sort(m_networkSpecies, [](const fourdst::atomic::Species& a, const fourdst::atomic::Species& b) -> bool {
return a.mass() < b.mass(); // Otherwise, sort by mass
});
@@ -581,7 +580,9 @@ namespace gridfire {
}
fourdst::composition::Composition GraphEngine::collectComposition(
fourdst::composition::CompositionAbstract &comp
const fourdst::composition::CompositionAbstract &comp,
double T9,
double rho
) const {
for (const auto &species: comp.getRegisteredSpecies()) {
if (!m_networkSpeciesMap.contains(species.name())) {
@@ -615,6 +616,7 @@ namespace gridfire {
rho
);
// --- Optimized loop ---
std::vector<double> molarReactionFlows;
molarReactionFlows.reserve(m_precomputedReactions.size());
@@ -654,6 +656,7 @@ namespace gridfire {
forwardAbundanceProduct *
std::pow(rho, numReactants > 1 ? static_cast<double>(numReactants) - 1 : 0.0);
// --- Reverse reaction flow ---
// Only do this is the reaction has a non-zero reverse symmetry factor (i.e. is reversible)
double reverseMolarReactionFlow = 0.0;
@@ -697,11 +700,35 @@ namespace gridfire {
const int stoichiometricCoefficient = precomp.stoichiometric_coefficients[i];
// Update the derivative for this species
result.dydt.at(species) += static_cast<double>(stoichiometricCoefficient) * R_j;
double dydt_increment = static_cast<double>(stoichiometricCoefficient) * R_j;
result.dydt.at(species) += dydt_increment;
result.reactionContributions[species][std::string(reaction->id())] = dydt_increment;
}
reactionCounter++;
}
// std::vector<std::string> reactionIDs;
// for (const auto& reaction: activeReactions) {
// reactionIDs.push_back(std::string(reaction->id()));
// }
//
// std::vector<std::unique_ptr<utils::ColumnBase>> columns;
// columns.push_back(std::make_unique<utils::Column<std::string>>("Reaction", reactionIDs));
// for (const auto& [species, contributions] : reactionContributions) {
// std::vector<double> speciesData;
// for (const auto& reactionID : reactionIDs) {
// if (contributions.contains(reactionID)) {
// speciesData.push_back(contributions.at(reactionID));
// } else {
// speciesData.push_back(0.0);
// }
// }
// columns.push_back(std::make_unique<utils::Column<double>>(std::string(species.name()), speciesData));
// }
// utils::print_table("Contributions", columns);
// exit(0);
// --- Calculate the nuclear energy generation rate ---
double massProductionRate = 0.0; // [mol][s^-1]
for (const auto & species : m_networkSpecies) {
@@ -1103,7 +1130,7 @@ namespace gridfire {
) const {
const double Ye = comp.getElectronAbundance();
auto [dydt, _] = calculateAllDerivatives<double>(
auto [dydt, _, __] = calculateAllDerivatives<double>(
comp.getMolarAbundanceVector(),
T9,
rho,
@@ -1155,7 +1182,7 @@ namespace gridfire {
return std::nullopt; // Species not present
};
auto [dydt, _] = calculateAllDerivatives<double>(
auto [dydt, _, __] = calculateAllDerivatives<double>(
Y,
T9,
rho,
@@ -1246,7 +1273,7 @@ namespace gridfire {
// 5. Call the actual templated function
// We let T9 and rho be constant, so we pass them as fixed values.
auto [dydt, nuclearEnergyGenerationRate] = calculateAllDerivatives<CppAD::AD<double>>(
auto [dydt, nuclearEnergyGenerationRate, _] = calculateAllDerivatives<CppAD::AD<double>>(
adY,
adT9,
adRho,

View File

@@ -147,8 +147,8 @@ namespace gridfire {
const double rho
) const {
validateState();
// TODO: Think about if I need to reach in and adjust the composition to zero out inactive species.
auto result = m_baseEngine.calculateRHSAndEnergy(comp, T9, rho);
fourdst::composition::Composition collectedComp = collectComposition(comp, T9, rho);
auto result = m_baseEngine.calculateRHSAndEnergy(collectedComp, T9, rho);
if (!result) {
return std::unexpected{result.error()};
@@ -313,9 +313,11 @@ namespace gridfire {
}
fourdst::composition::Composition AdaptiveEngineView::collectComposition(
fourdst::composition::CompositionAbstract &comp
const fourdst::composition::CompositionAbstract &comp,
const double T9,
const double rho
) const {
fourdst::composition::Composition result = m_baseEngine.collectComposition(comp); // Step one is to bubble the results from lower levels of the engine chain up
fourdst::composition::Composition result = m_baseEngine.collectComposition(comp, T9, rho);
for (const auto& species : m_activeSpecies) {
if (!result.contains(species)) {

View File

@@ -284,9 +284,11 @@ namespace gridfire {
}
fourdst::composition::Composition DefinedEngineView::collectComposition(
fourdst::composition::CompositionAbstract &comp
const fourdst::composition::CompositionAbstract &comp,
const double T9,
const double rho
) const {
fourdst::composition::Composition result = m_baseEngine.collectComposition(comp);
fourdst::composition::Composition result = m_baseEngine.collectComposition(comp, T9, rho);
for (const auto& species : m_activeSpecies) {
if (!result.contains(species)) {

View File

@@ -17,34 +17,11 @@
#include "quill/LogMacros.h"
#include "quill/Logger.h"
#include "xxhash64.h"
#include "fourdst/composition/utils/composition_hash.h"
namespace {
using namespace fourdst::atomic;
//TODO: Replace all calls to this function with composition.getMolarAbundanceVector() so that
// we don't have to keep this function around. (Cant do this right now because there is not a
// guarantee that this function will return the same ordering as the canonical vector representation)
std::vector<double> packCompositionToVector(
const fourdst::composition::Composition& composition,
const gridfire::DynamicEngine& engine
) {
std::vector<double> Y(engine.getNetworkSpecies().size(), 0.0);
const auto& allSpecies = engine.getNetworkSpecies();
for (size_t i = 0; i < allSpecies.size(); ++i) {
const auto& species = allSpecies[i];
if (!composition.contains(species)) {
Y[i] = 0.0; // Species not in the composition, set to zero
} else {
Y[i] = composition.getMolarAbundance(species);
}
}
return Y;
}
template <class T>
void hash_combine(std::size_t& seed, const T& v) {
std::hash<T> hashed;
seed ^= hashed(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
}
std::vector<std::vector<Species>> findConnectedComponentsBFS(
const std::unordered_map<Species, std::vector<Species>>& graph,
const std::vector<Species>& nodes
@@ -186,11 +163,14 @@ namespace gridfire {
const double T9,
const double rho
) const {
// Check the cache to see if the network needs to be repartitioned. Note that the QSECacheKey manages binning of T9, rho, and Y_full to ensure that small changes (which would likely not result in a repartitioning) do not trigger a cache miss.
const auto result = m_baseEngine.calculateRHSAndEnergy(comp, T9, rho);
const fourdst::composition::Composition qseComposition = getNormalizedEquilibratedComposition(comp, T9, rho);
const auto result = m_baseEngine.calculateRHSAndEnergy(qseComposition, T9, rho);
if (!result) {
return std::unexpected{result.error()};
}
auto deriv = result.value();
for (const auto& species : m_algebraic_species) {
@@ -204,7 +184,8 @@ namespace gridfire {
const double T9,
const double rho
) const {
return m_baseEngine.calculateEpsDerivatives(comp, T9, rho);
const fourdst::composition::Composition qseComposition = getNormalizedEquilibratedComposition(comp, T9, rho);
return m_baseEngine.calculateEpsDerivatives(qseComposition, T9, rho);
}
void MultiscalePartitioningEngineView::generateJacobianMatrix(
@@ -212,8 +193,8 @@ namespace gridfire {
const double T9,
const double rho
) const {
// We do not need to generate the jacobian for QSE species since those entries are by definition 0
m_baseEngine.generateJacobianMatrix(comp, T9, rho, m_dynamic_species);
const fourdst::composition::Composition qseComposition = getNormalizedEquilibratedComposition(comp, T9, rho);
m_baseEngine.generateJacobianMatrix(qseComposition, T9, rho, m_dynamic_species);
}
void MultiscalePartitioningEngineView::generateJacobianMatrix(
@@ -249,7 +230,9 @@ namespace gridfire {
}
}
m_baseEngine.generateJacobianMatrix(comp, T9, rho, dynamicActiveSpeciesIntersection);
const fourdst::composition::Composition qseComposition = getNormalizedEquilibratedComposition(comp, T9, rho);
m_baseEngine.generateJacobianMatrix(qseComposition, T9, rho, dynamicActiveSpeciesIntersection);
}
void MultiscalePartitioningEngineView::generateJacobianMatrix(
@@ -258,7 +241,8 @@ namespace gridfire {
const double rho,
const SparsityPattern &sparsityPattern
) const {
return m_baseEngine.generateJacobianMatrix(comp, T9, rho, sparsityPattern);
const fourdst::composition::Composition qseComposition = getNormalizedEquilibratedComposition(comp, T9, rho);
return m_baseEngine.generateJacobianMatrix(qseComposition, T9, rho, sparsityPattern);
}
double MultiscalePartitioningEngineView::getJacobianMatrixEntry(
@@ -292,19 +276,9 @@ namespace gridfire {
const double T9,
const double rho
) const {
// Fix the algebraic species to the equilibrium abundances we calculate.
fourdst::composition::Composition comp_mutable;
for (const auto& species : comp.getRegisteredSpecies()) {
comp_mutable.registerSpecies(species);
comp_mutable.setMolarAbundance(species, comp.getMolarAbundance(species));
}
const fourdst::composition::Composition qseComposition = getNormalizedEquilibratedComposition(comp, T9, rho);
for (const auto& species : m_algebraic_species) {
const double Yi = m_algebraic_abundances.at(species);
comp_mutable.setMolarAbundance(species, Yi);
}
return m_baseEngine.calculateMolarReactionFlow(reaction, comp_mutable, T9, rho);
return m_baseEngine.calculateMolarReactionFlow(reaction, qseComposition, T9, rho);
}
const reaction::ReactionSet & MultiscalePartitioningEngineView::getNetworkReactions() const {
@@ -321,7 +295,8 @@ namespace gridfire {
const double T9,
const double rho
) const {
const auto result = m_baseEngine.getSpeciesTimescales(comp, T9, rho);
const fourdst::composition::Composition qseComposition = getNormalizedEquilibratedComposition(comp, T9, rho);
const auto result = m_baseEngine.getSpeciesTimescales(qseComposition, T9, rho);
if (!result) {
return std::unexpected{result.error()};
}
@@ -338,7 +313,8 @@ namespace gridfire {
const double T9,
const double rho
) const {
const auto result = m_baseEngine.getSpeciesDestructionTimescales(comp, T9, rho);
const fourdst::composition::Composition qseComposition = getNormalizedEquilibratedComposition(comp, T9, rho);
const auto result = m_baseEngine.getSpeciesDestructionTimescales(qseComposition, T9, rho);
if (!result) {
return std::unexpected{result.error()};
}
@@ -354,27 +330,13 @@ namespace gridfire {
NetIn baseUpdatedNetIn = netIn;
baseUpdatedNetIn.composition = baseUpdatedComposition;
const fourdst::composition::Composition equilibratedComposition = equilibrateNetwork(baseUpdatedNetIn);
std::unordered_map<Species, double> algebraicAbundances;
for (const auto& species : m_algebraic_species) {
algebraicAbundances.emplace(species, equilibratedComposition.getMolarAbundance(species));
}
m_algebraic_abundances = std::move(algebraicAbundances);
fourdst::composition::Composition equilibratedComposition = partitionNetwork(baseUpdatedNetIn);
return equilibratedComposition;
}
bool MultiscalePartitioningEngineView::isStale(const NetIn &netIn) {
const auto key = QSECacheKey(
netIn.temperature,
netIn.density,
packCompositionToVector(netIn.composition, m_baseEngine)
);
if (m_qse_abundance_cache.contains(key)) {
return m_baseEngine.isStale(netIn); // The cache hit indicates the engine is not stale for the given conditions.
}
return true;
return m_baseEngine.isStale(netIn);
}
void MultiscalePartitioningEngineView::setScreeningModel(
@@ -392,10 +354,7 @@ namespace gridfire {
}
std::vector<std::vector<Species>> MultiscalePartitioningEngineView::analyzeTimescalePoolConnectivity(
const std::vector<std::vector<Species>> &timescale_pools,
const fourdst::composition::Composition &comp,
double T9,
double rho
const std::vector<std::vector<Species>> &timescale_pools
) const {
std::vector<std::vector<Species>> final_connected_pools;
@@ -413,17 +372,22 @@ namespace gridfire {
}
void MultiscalePartitioningEngineView::partitionNetwork(
const fourdst::composition::Composition &comp,
const double T9,
const double rho
fourdst::composition::Composition MultiscalePartitioningEngineView::partitionNetwork(
const NetIn &netIn
) {
// --- Step 0. Clear previous state ---
// --- Step 0. Prime the network ---
const PrimingReport primingReport = m_baseEngine.primeEngine(netIn);
const fourdst::composition::Composition& comp = primingReport.primedComposition;
const double T9 = netIn.temperature / 1e9;
const double rho = netIn.density;
// --- 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_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
// --- Step 1. Identify distinct timescale regions ---
LOG_TRACE_L1(m_logger, "Identifying fast reactions...");
@@ -449,7 +413,7 @@ namespace gridfire {
}
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(candidate_pools);
LOG_TRACE_L1(m_logger, "Found {} connected pools (compared to {} timescale pools) for QSE analysis.", connected_pools.size(), timescale_pools.size());
@@ -555,15 +519,12 @@ namespace gridfire {
m_qse_groups.size(),
m_qse_groups.size() == 1 ? "" : "s"
);
}
void MultiscalePartitioningEngineView::partitionNetwork(
const NetIn &netIn
) {
const double T9 = netIn.temperature / 1e9; // Convert temperature from Kelvin to T9 (T9 = T / 1e9)
const double rho = netIn.density; // Density in g/cm^3
partitionNetwork(netIn.composition, T9, rho);
// Sort the QSE groups by mean timescale so that fastest groups get equilibrated first (as these may feed slower groups)
std::ranges::sort(m_qse_groups, [](const QSEGroup& a, const QSEGroup& b) {
return a.mean_timescale < b.mean_timescale;
});
return getNormalizedEquilibratedComposition(comp, T9, rho);
}
void MultiscalePartitioningEngineView::exportToDot(
@@ -593,6 +554,7 @@ namespace gridfire {
}
}
const fourdst::composition::Composition qseComposition = getNormalizedEquilibratedComposition(comp, T9, rho);
// Calculate reaction flows and find min/max for logarithmic scaling of transparency
std::vector<double> reaction_flows;
reaction_flows.reserve(all_reactions.size());
@@ -600,7 +562,7 @@ namespace gridfire {
double max_log_flow = std::numeric_limits<double>::lowest();
for (const auto& reaction : all_reactions) {
double flow = std::abs(m_baseEngine.calculateMolarReactionFlow(*reaction, comp, T9, rho));
double flow = std::abs(m_baseEngine.calculateMolarReactionFlow(*reaction, qseComposition, T9, rho));
reaction_flows.push_back(flow);
if (flow > 1e-99) { // Avoid log(0)
double log_flow = std::log10(flow);
@@ -776,7 +738,7 @@ namespace gridfire {
std::vector<double> MultiscalePartitioningEngineView::mapNetInToMolarAbundanceVector(const NetIn &netIn) const {
std::vector<double> Y(m_dynamic_species.size(), 0.0); // Initialize with zeros
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
}
@@ -804,33 +766,6 @@ namespace gridfire {
return m_baseEngine.primeEngine(netIn);
}
fourdst::composition::Composition MultiscalePartitioningEngineView::equilibrateNetwork(
const fourdst::composition::Composition &comp,
const double T9,
const double rho
) {
partitionNetwork(comp, T9, rho);
fourdst::composition::Composition qseComposition = solveQSEAbundances(comp, T9, rho);
for (const auto &[sp, y]: qseComposition) {
if (y < 0.0 && std::abs(y) < 1e-20) {
qseComposition.setMolarAbundance(sp, 0.0); // Avoid negative mass fractions
}
}
return qseComposition;
}
fourdst::composition::Composition MultiscalePartitioningEngineView::equilibrateNetwork(
const NetIn &netIn
) {
const PrimingReport primingReport = m_baseEngine.primeEngine(netIn);
const double T9 = netIn.temperature / 1e9; // Convert temperature from Kelvin to T9 (T9 = T / 1e9)
const double rho = netIn.density; // Density in g/cm^3
return equilibrateNetwork(primingReport.primedComposition, T9, rho);
}
bool MultiscalePartitioningEngineView::involvesSpecies(
const Species &species
@@ -852,30 +787,43 @@ namespace gridfire {
return std::ranges::find(m_dynamic_species, species) != m_dynamic_species.end();
}
fourdst::composition::Composition MultiscalePartitioningEngineView::collectComposition(
fourdst::composition::CompositionAbstract &comp
fourdst::composition::Composition MultiscalePartitioningEngineView::getNormalizedEquilibratedComposition(
const fourdst::composition::CompositionAbstract& comp,
const double T9,
const double rho
) const {
fourdst::composition::Composition result = m_baseEngine.collectComposition(comp);
std::map<Species, double> Ym; // Use an ordered map here so that this is ordered by atomic mass (which is the </> comparator for Species)
for (const auto& [sp, y] : result) {
Ym.emplace(sp, y);
}
for (const auto& [species, Yi] : m_algebraic_abundances) {
if (!Ym.contains(species)) {
throw exceptions::BadCollectionError("MuiltiscalePartitioningEngineView failed to collect composition for species " + std::string(species.name()) + " as the base engine did not report that species present in its composition!");
}
Ym.at(species) = Yi;
}
std::vector<double> Y;
std::vector<std::string> speciesNames;
Y.reserve(Ym.size());
const std::array<uint64_t, 3> hashes = {
fourdst::composition::utils::CompositionHash::hash_exact(comp),
std::hash<double>()(T9),
std::hash<double>()(rho)
};
for (const auto& [species, Yi] : Ym) {
Y.emplace_back(Yi);
speciesNames.emplace_back(species.name());
const uint64_t composite_hash = XXHash64::hash(hashes.begin(), sizeof(uint64_t) * 3, 0);
if (m_composition_cache.contains(composite_hash)) {
LOG_TRACE_L1(m_logger, "Cache Hit in Multiscale Partitioning Engine View for composition at T9 = {}, rho = {}.", T9, rho);
return m_composition_cache.at(composite_hash);
}
fourdst::composition::Composition qseComposition = solveQSEAbundances(comp, T9, rho);
for (const auto &[sp, y]: qseComposition) {
if (y < 0.0 && std::abs(y) < 1e-20) {
qseComposition.setMolarAbundance(sp, 0.0); // normalize small negative abundances to zero
}
}
m_composition_cache[composite_hash] = qseComposition;
return qseComposition;
}
return {speciesNames, Y};
fourdst::composition::Composition MultiscalePartitioningEngineView::collectComposition(
const fourdst::composition::CompositionAbstract &comp,
const double T9,
const double rho
) const {
const fourdst::composition::Composition result = m_baseEngine.collectComposition(comp, T9, rho);
fourdst::composition::Composition qseComposition = solveQSEAbundances(result, T9, rho);
return qseComposition;
}
size_t MultiscalePartitioningEngineView::getSpeciesIndex(const Species &species) const {
@@ -1201,33 +1149,21 @@ namespace gridfire {
}
fourdst::composition::Composition MultiscalePartitioningEngineView::solveQSEAbundances(
const fourdst::composition::Composition &comp,
const fourdst::composition::CompositionAbstract &comp,
const double T9,
const double rho
) {
) const {
LOG_TRACE_L1(m_logger, "Solving for QSE abundances...");
// Sort by timescale to solve fastest QSE groups first (these can seed slower groups)
std::ranges::sort(m_qse_groups, [](const QSEGroup& a, const QSEGroup& b) {
return a.mean_timescale < b.mean_timescale;
});
fourdst::composition::Composition outputComposition = comp;
fourdst::composition::Composition outputComposition(comp.getRegisteredSpecies());
for (const auto& [sp, y]: comp) {
outputComposition.setMolarAbundance(sp, y);
}
for (const auto&[is_in_equilibrium, algebraic_species, seed_species, mean_timescale] : m_qse_groups) {
if (!is_in_equilibrium || (algebraic_species.empty() && seed_species.empty())) {
continue;
}
fourdst::composition::Composition normalized_composition = comp;
for (const auto& species: algebraic_species) {
if (!normalized_composition.contains(species)) {
normalized_composition.registerSpecies(species);
}
}
for (const auto& species: seed_species) {
if (!normalized_composition.contains(species)) {
normalized_composition.registerSpecies(species);
}
}
Eigen::VectorXd Y_scale(algebraic_species.size());
Eigen::VectorXd v_initial(algebraic_species.size());
@@ -1235,14 +1171,14 @@ namespace gridfire {
std::unordered_map<Species, size_t> species_to_index_map;
for (const auto& species : algebraic_species) {
constexpr double abundance_floor = 1.0e-100;
const double initial_abundance = normalized_composition.getMolarAbundance(species);
const double initial_abundance = comp.getMolarAbundance(species);
const double Y = std::max(initial_abundance, abundance_floor);
v_initial(i) = std::log(Y);
species_to_index_map.emplace(species, i);
i++;
}
EigenFunctor functor(*this, algebraic_species, normalized_composition, T9, rho, Y_scale, species_to_index_map);
EigenFunctor functor(*this, algebraic_species, comp, T9, rho, Y_scale, species_to_index_map);
Eigen::LevenbergMarquardt lm(functor);
lm.parameters.ftol = 1.0e-10;
lm.parameters.xtol = 1.0e-10;
@@ -1275,7 +1211,7 @@ namespace gridfire {
m_logger,
"During QSE solving species {} started with a molar abundance of {} and ended with an abundance of {}.",
species.name(),
normalized_composition.getMolarAbundance(species),
comp.getMolarAbundance(species),
Y_final_qse(i)
);
// double Xi = Y_final_qse(i) * species.mass(); // Convert from molar abundance to mass fraction
@@ -1488,23 +1424,28 @@ namespace gridfire {
}
//////////////////////////////////////
/// Eigen Functor Member Functions ///
/////////////////////////////////////
int MultiscalePartitioningEngineView::EigenFunctor::operator()(const InputType &v_qse, OutputType &f_qse) const {
fourdst::composition::Composition comp_trial = m_initial_comp;
fourdst::composition::Composition comp_trial(m_initial_comp.getRegisteredSpecies());
for (const auto& [sp, y] : m_initial_comp) {
comp_trial.setMolarAbundance(sp, y);
}
Eigen::VectorXd y_qse = v_qse.array().exp(); // Convert to physical abundances using exponential scaling
for (const auto& species: m_qse_solve_species) {
if (!comp_trial.contains(std::string(species.name()))) {
comp_trial.registerSpecies(species);
}
auto index = static_cast<long>(m_qse_solve_species_index_map.at(species));
comp_trial.setMolarAbundance(species, y_qse[index]);
}
const auto result = m_view->getBaseEngine().calculateRHSAndEnergy(comp_trial, m_T9, m_rho);
const auto result = m_view.getBaseEngine().calculateRHSAndEnergy(comp_trial, m_T9, m_rho);
if (!result) {
throw exceptions::StaleEngineError("Failed to calculate RHS and energy due to stale engine state");
}
const auto&[dydt, nuclearEnergyGenerationRate] = result.value();
const auto&[dydt, nuclearEnergyGenerationRate, _] = result.value();
f_qse.resize(static_cast<long>(m_qse_solve_species.size()));
long i = 0;
// TODO: make sure that just counting up i is a valid approach, this is a possible place an indexing bug may have crept in
@@ -1514,7 +1455,7 @@ namespace gridfire {
i++;
}
LOG_TRACE_L2(
m_view->m_logger,
m_view.m_logger,
"Functor evaluation at T9 = {}, rho = {}, y_qse = <{}> complete. ||f|| = {}",
m_T9,
m_rho,
@@ -1531,7 +1472,7 @@ namespace gridfire {
f_qse.norm()
);
LOG_TRACE_L3(
m_view->m_logger,
m_view.m_logger,
"{}",
[&]() -> std::string {
std::stringstream ss;
@@ -1546,24 +1487,24 @@ namespace gridfire {
}
int MultiscalePartitioningEngineView::EigenFunctor::df(const InputType &v_qse, JacobianType &J_qse) const {
fourdst::composition::Composition comp_trial = m_initial_comp;
fourdst::composition::Composition comp_trial(m_initial_comp.getRegisteredSpecies());
for (const auto& [sp, y] : m_initial_comp) {
comp_trial.setMolarAbundance(sp, y);
}
Eigen::VectorXd y_qse = v_qse.array().exp(); // Convert to physical abundances using exponential scaling
for (const auto& species: m_qse_solve_species) {
if (!comp_trial.contains(std::string(species.name()))) {
comp_trial.registerSpecies(species);
}
const double molarAbundance = y_qse[static_cast<long>(m_qse_solve_species_index_map.at(species))];
comp_trial.setMolarAbundance(species, molarAbundance);
}
std::vector<Species> qse_species_vector(m_qse_solve_species.begin(), m_qse_solve_species.end());
m_view->getBaseEngine().generateJacobianMatrix(comp_trial, m_T9, m_rho, qse_species_vector);
const auto result = m_view->getBaseEngine().calculateRHSAndEnergy(comp_trial, m_T9, m_rho);
m_view.getBaseEngine().generateJacobianMatrix(comp_trial, m_T9, m_rho, qse_species_vector);
const auto result = m_view.getBaseEngine().calculateRHSAndEnergy(comp_trial, m_T9, m_rho);
if (!result) {
throw exceptions::StaleEngineError("Failed to calculate RHS and energy due to stale engine state");
}
const auto&[dydt, nuclearEnergyGenerationRate] = result.value();
const auto&[dydt, nuclearEnergyGenerationRate, _] = result.value();
const long N = static_cast<long>(m_qse_solve_species.size());
J_qse.resize(N, N);
@@ -1571,12 +1512,12 @@ namespace gridfire {
for (const auto& rowSpecies : m_qse_solve_species) {
long colID = 0;
for (const auto& colSpecies: m_qse_solve_species) {
J_qse(rowID, colID) = m_view->getBaseEngine().getJacobianMatrixEntry(
J_qse(rowID, colID) = m_view.getBaseEngine().getJacobianMatrixEntry(
rowSpecies,
colSpecies
);
colID += 1;
LOG_TRACE_L3(m_view->m_logger, "Jacobian[{}, {}] (d(dY({}))/dY({})) = {}", rowID, colID - 1, rowSpecies.name(), colSpecies.name(), J_qse(rowID, colID - 1));
LOG_TRACE_L3(m_view.m_logger, "Jacobian[{}, {}] (d(dY({}))/dY({})) = {}", rowID, colID - 1, rowSpecies.name(), colSpecies.name(), J_qse(rowID, colID - 1));
}
rowID += 1;
}
@@ -1597,46 +1538,12 @@ namespace gridfire {
}
QSECacheKey::QSECacheKey(
const double T9,
const double rho,
const std::vector<double> &Y
) :
m_T9(T9),
m_rho(rho),
m_Y(Y) {
m_hash = hash();
}
size_t QSECacheKey::hash() const {
std::size_t seed = 0;
/////////////////////////////////
/// QSEGroup Member Functions ///
////////////////////////////////
hash_combine(seed, m_Y.size());
hash_combine(seed, bin(m_T9, m_cacheConfig.T9_tol));
hash_combine(seed, bin(m_rho, m_cacheConfig.rho_tol));
double negThresh = 1e-10; // Threshold for considering a value as negative.
for (double Yi : m_Y) {
if (Yi < 0.0 && std::abs(Yi) < negThresh) {
Yi = 0.0; // Avoid negative abundances
} else if (Yi < 0.0 && std::abs(Yi) >= negThresh) {
throw std::invalid_argument("Yi should be positive for valid hashing (expected Yi > 0, received " + std::to_string(Yi) + ")");
}
hash_combine(seed, bin(Yi, m_cacheConfig.Yi_tol));
}
return seed;
}
long QSECacheKey::bin(const double value, const double tol) {
return static_cast<long>(std::floor(value / tol));
}
bool QSECacheKey::operator==(const QSECacheKey &other) const {
return m_hash == other.m_hash;
}
bool MultiscalePartitioningEngineView::QSEGroup::operator==(const QSEGroup &other) const {
return mean_timescale == other.mean_timescale;
@@ -1707,36 +1614,4 @@ namespace gridfire {
return ss.str();
}
void MultiscalePartitioningEngineView::CacheStats::hit(const operators op) {
if (op == operators::All) {
throw std::invalid_argument("Cannot use 'ALL' as an operator for a hit");
}
m_hit ++;
m_operatorHits[op]++;
}
void MultiscalePartitioningEngineView::CacheStats::miss(const operators op) {
if (op == operators::All) {
throw std::invalid_argument("Cannot use 'ALL' as an operator for a miss");
}
m_miss ++;
m_operatorMisses[op]++;
}
size_t MultiscalePartitioningEngineView::CacheStats::hits(const operators op) const {
if (op == operators::All) {
return m_hit;
}
return m_operatorHits.at(op);
}
size_t MultiscalePartitioningEngineView::CacheStats::misses(const operators op) const {
if (op == operators::All) {
return m_miss;
}
return m_operatorMisses.at(op);
}
}

View File

@@ -55,6 +55,7 @@ namespace gridfire::policy {
);
auto& graphRepr = dynamic_cast<GraphEngine&>(*m_network_stack.back().get());
// graphRepr.setPrecomputation(false);
graphRepr.setUseReverseReactions(false);

View File

@@ -4,13 +4,18 @@
#include "gridfire/reaction/reaclib.h"
#include "gridfire/reaction/reactions_data.h"
#include "gridfire/network.h"
#include "gridfire/exceptions/error_reaction.h"
#include <stdexcept>
#include <sstream>
#include <vector>
#include <string>
#include <format>
#include <print>
#include <expected>
std::string trim_whitespace(const std::string& str) {
namespace {
std::string trim_whitespace(const std::string& str) {
auto startIt = str.begin();
const auto endIt = str.end();
@@ -23,8 +28,75 @@ std::string trim_whitespace(const std::string& str) {
const auto ritr = std::find_if(str.rbegin(), std::string::const_reverse_iterator(startIt),
[](const unsigned char ch){ return !std::isspace(ch); });
return {startIt, ritr.base()};
}
}
enum class ReactionParseError {
MissingOpenParenthesis,
MissingCloseParenthesis,
ParenthesesOutOfOrder,
MissingComma,
BadFormat // Generic error (e.g., from an out_of_range exception)
};
std::string error_to_string(ReactionParseError err) {
switch (err) {
case ReactionParseError::MissingOpenParenthesis:
return "Missing '('";
case ReactionParseError::MissingCloseParenthesis:
return "Missing ')'";
case ReactionParseError::ParenthesesOutOfOrder:
return "')' found before '('";
case ReactionParseError::MissingComma:
return "Missing ',' within parentheses";
case ReactionParseError::BadFormat:
return "Bad format (substr error)";
}
return "Unknown error";
}
std::expected<std::string, ReactionParseError> reverse_pe_name(const std::string& forwardPEName) noexcept {
try {
size_t pos_open_paren = forwardPEName.find('(');
size_t pos_close_paren = forwardPEName.find(')');
if (pos_open_paren == std::string::npos) {
return std::unexpected(ReactionParseError::MissingOpenParenthesis);
}
if (pos_close_paren == std::string::npos) {
return std::unexpected(ReactionParseError::MissingCloseParenthesis);
}
if (pos_close_paren < pos_open_paren) {
return std::unexpected(ReactionParseError::ParenthesesOutOfOrder);
}
std::string target = forwardPEName.substr(0, pos_open_paren);
std::string result = forwardPEName.substr(pos_close_paren + 1);
std::string inner_content = forwardPEName.substr(
pos_open_paren + 1,
pos_close_paren - pos_open_paren - 1
);
size_t pos_comma = inner_content.find(',');
if (pos_comma == std::string::npos) {
return std::unexpected(ReactionParseError::MissingComma);
}
std::string projectiles = inner_content.substr(0, pos_comma);
std::string ejectiles = inner_content.substr(pos_comma + 1);
std::ostringstream oss;
oss << result << "(" << ejectiles << "," << projectiles << ")" << target;
return oss.str();
} catch (const std::out_of_range&) {
return std::unexpected(ReactionParseError::BadFormat);
}
}
}
namespace gridfire::reaclib {
static std::unique_ptr<reaction::ReactionSet> s_all_reaclib_reactions_ptr = nullptr;
@@ -106,10 +178,20 @@ namespace gridfire::reaclib {
coeffs[6]
};
auto rpName_revNormalized = std::string(rpName_sv);
if (reverse) {
auto result = reverse_pe_name(std::string(rpName_sv));
if (!result) {
std::string msg = std::format("Error reversing stored projectile-ejectile name for marked reverse reaction ({})", error_to_string(result.error()));
throw exceptions::ReactionParsingError(msg, rpName_revNormalized);
}
rpName_revNormalized = result.value();
}
// Construct the Reaction object. We use rpName for both the unique ID and the human-readable name.
reaction_list.emplace_back(std::make_unique<reaction::ReaclibReaction>(
rpName_sv,
rpName_sv,
rpName_revNormalized,
rpName_revNormalized,
chapter,
reactants,
products,

View File

@@ -40,8 +40,8 @@ namespace gridfire::reaction {
m_peName(peName),
m_chapter(chapter),
m_qValue(qValue),
m_reactants(reactants),
m_products(products),
m_reactants(reactants.begin(), reactants.end()),
m_products(products.begin(), products.end()),
m_sourceLabel(label),
m_rateCoefficients(sets),
m_reverse(reverse) {}
@@ -343,10 +343,12 @@ namespace gridfire::reaction {
return; // Case where the reactions will be added later.
}
m_reactionNameMap.reserve(m_reactions.size());
m_reactionHashes.reserve(m_reactions.size());
size_t i = 0;
for (const auto& reaction : m_reactions) {
m_id += reaction->id();
m_reactionNameMap.emplace(std::string(reaction->id()), i);
m_reactionHashes.insert(reaction->hash(0));
i++;
}
}
@@ -354,11 +356,13 @@ namespace gridfire::reaction {
ReactionSet::ReactionSet(const std::vector<Reaction *> &reactions) {
m_reactions.reserve(reactions.size());
m_reactionNameMap.reserve(reactions.size());
m_reactionHashes.reserve(reactions.size());
size_t i = 0;
for (const auto& reaction : reactions) {
m_reactions.push_back(reaction->clone());
m_id += reaction->id();
m_reactionNameMap.emplace(std::string(reaction->id()), i);
m_reactionHashes.insert(reaction->hash(0));
i++;
}
}
@@ -367,14 +371,13 @@ namespace gridfire::reaction {
ReactionSet::ReactionSet(const ReactionSet &other) {
m_reactions.reserve(other.m_reactions.size());
for (const auto& reaction: other.m_reactions) {
m_reactions.push_back(reaction->clone());
}
m_reactionHashes.reserve(other.m_reactions.size());
m_reactionNameMap.reserve(other.m_reactionNameMap.size());
size_t i = 0;
for (const auto& reaction : m_reactions) {
for (const auto& reaction: other.m_reactions) {
m_reactions.push_back(reaction->clone());
m_reactionNameMap.emplace(std::string(reaction->id()), i);
m_reactionHashes.insert(reaction->hash(0));
i++;
}
}
@@ -397,18 +400,23 @@ namespace gridfire::reaction {
m_id += reaction_id;
m_reactionNameMap.emplace(std::move(reaction_id), new_index);
m_reactionHashes.insert(reaction.hash(0));
}
void ReactionSet::add_reaction(std::unique_ptr<Reaction>&& reaction) {
const std::size_t new_index = m_reactionNameMap.size();
auto reaction_id = std::string(reaction->id());
size_t reaction_hash = reaction->hash(0);
m_reactions.emplace_back(std::move(reaction));
m_id += reaction_id;
m_reactionNameMap.emplace(std::move(reaction_id), new_index);
m_reactionHashes.insert(reaction_hash);
}
void ReactionSet::extend(const ReactionSet &other) {
@@ -425,43 +433,32 @@ namespace gridfire::reaction {
}
void ReactionSet::remove_reaction(const Reaction& reaction) {
const auto reaction_id = std::string(reaction.id());
if (!m_reactionNameMap.contains(reaction_id)) {
const size_t rh = reaction.hash(0);
if (!m_reactionHashes.contains(rh)) {
return;
}
std::erase_if(m_reactions, [&reaction_id](const auto& r_ptr) {
return r_ptr->id() == reaction_id;
std::erase_if(m_reactions, [&rh](const auto& r_ptr) {
return r_ptr->hash(0) == rh;
});
m_reactionNameMap.clear();
m_reactionNameMap.reserve(m_reactions.size());
for (size_t i = 0; i < m_reactions.size(); ++i) {
m_reactionNameMap.emplace(std::string(m_reactions[i]->id()), i);
}
m_reactionNameMap.erase(std::string(reaction.id()));
m_id.clear();
for (const auto& r_ptr : m_reactions) {
m_id += r_ptr->id();
}
m_reactionHashes.erase(rh);
}
bool ReactionSet::contains(const std::string_view& id) const {
for (const auto& reaction : m_reactions) {
if (reaction->id() == id) {
return true;
}
}
return false;
return m_reactionNameMap.contains(std::string(id));
}
bool ReactionSet::contains(const Reaction& reaction) const {
for (const auto& r : m_reactions) {
if (r->id() == reaction.id()) {
return true;
}
}
return false;
const size_t rh = reaction.hash(0);
return m_reactionHashes.contains(rh);
}
void ReactionSet::clear() {
@@ -505,12 +502,13 @@ namespace gridfire::reaction {
}
const Reaction& ReactionSet::operator[](const std::string_view& id) const {
if (const auto it = m_reactionNameMap.find(std::string(id)); it != m_reactionNameMap.end()) {
return *m_reactions[it->second];
}
if (!contains(id)) {
m_logger -> flush_log();
throw std::out_of_range("Reaction " + std::string(id) + " does not exist in ReactionSet.");
}
const size_t idx = m_reactionNameMap.at(std::string(id));
return *m_reactions[idx];
}
bool ReactionSet::operator==(const ReactionSet& other) const {
if (size() != other.size()) {
@@ -523,7 +521,7 @@ namespace gridfire::reaction {
return !(*this == other);
}
uint64_t ReactionSet::hash(uint64_t seed) const {
uint64_t ReactionSet::hash(const uint64_t seed) const {
if (m_reactions.empty()) {
return XXHash64::hash(nullptr, 0, seed);
}

View File

@@ -16,10 +16,12 @@
#include <stdexcept>
#include <algorithm>
#include "fourdst/atomic/species.h"
#include "fourdst/composition/exceptions/exceptions_composition.h"
#include "gridfire/engine/engine_graph.h"
#include "gridfire/solver/strategies/triggers/engine_partitioning_trigger.h"
#include "gridfire/trigger/procedures/trigger_pprint.h"
#include "gridfire/exceptions/error_solver.h"
namespace {
std::unordered_map<int, std::string> cvode_ret_code_map {
@@ -59,9 +61,9 @@ namespace {
void check_cvode_flag(const int flag, const std::string& func_name) {
if (flag < 0) {
if (!cvode_ret_code_map.contains(flag)) {
throw std::runtime_error("CVODE error in " + func_name + ": Unknown error code: " + std::to_string(flag));
throw gridfire::exceptions::CVODESolverFailureError("CVODE error in " + func_name + ": Unknown error code: " + std::to_string(flag));
}
throw std::runtime_error("CVODE error in " + func_name + ": " + cvode_ret_code_map.at(flag));
throw gridfire::exceptions::CVODESolverFailureError("CVODE error in " + func_name + ": " + cvode_ret_code_map.at(flag));
}
}
@@ -91,7 +93,8 @@ namespace gridfire::solver {
const DynamicEngine &engine,
const std::vector<fourdst::atomic::Species> &networkSpecies,
const size_t currentConvergenceFailure,
const size_t currentNonlinearIterations
const size_t currentNonlinearIterations,
const std::map<fourdst::atomic::Species, std::unordered_map<std::string, double>> &reactionContributionMap
) :
t(t),
state(state),
@@ -103,7 +106,8 @@ namespace gridfire::solver {
engine(engine),
networkSpecies(networkSpecies),
currentConvergenceFailures(currentConvergenceFailure),
currentNonlinearIterations(currentNonlinearIterations)
currentNonlinearIterations(currentNonlinearIterations),
reactionContributionMap(reactionContributionMap)
{}
std::vector<std::tuple<std::string, std::string>> CVODESolverStrategy::TimestepContext::describe() const {
@@ -241,6 +245,13 @@ namespace gridfire::solver {
<< "\n";
}
static const std::map<fourdst::atomic::Species,
std::unordered_map<std::string,double>> kEmptyMap{};
const auto& rcMap = user_data.reaction_contribution_map.has_value()
? user_data.reaction_contribution_map.value()
: kEmptyMap;
auto ctx = TimestepContext(
current_time,
m_Y,
@@ -252,7 +263,9 @@ namespace gridfire::solver {
m_engine,
m_engine.getNetworkSpecies(),
convFail_diff,
iter_diff);
iter_diff,
rcMap
);
prev_nonlinear_iterations = nliters + total_nonlinear_iterations;
prev_convergence_failures = nlcfails + total_convergence_failures;
@@ -459,7 +472,7 @@ namespace gridfire::solver {
LOG_TRACE_L2(m_logger, "Constructing final composition= with {} species", numSpecies);
fourdst::composition::Composition topLevelComposition(m_engine.getNetworkSpecies(), y_vec);
fourdst::composition::Composition outputComposition = m_engine.collectComposition(topLevelComposition);
fourdst::composition::Composition outputComposition = m_engine.collectComposition(topLevelComposition, netIn.temperature/1e9, netIn.density);
assert(outputComposition.getRegisteredSymbols().size() == equilibratedComposition.getRegisteredSymbols().size());
@@ -514,7 +527,8 @@ namespace gridfire::solver {
const auto* instance = data->solver_instance;
try {
instance->calculate_rhs(t, y, ydot, data);
const CVODERHSOutputData out = instance->calculate_rhs(t, y, ydot, data);
data->reaction_contribution_map = out.reaction_contribution_map;
return 0;
} catch (const exceptions::StaleEngineTrigger& e) {
data->captured_exception = std::make_unique<exceptions::StaleEngineTrigger>(e);
@@ -564,7 +578,7 @@ namespace gridfire::solver {
return 0;
}
void CVODESolverStrategy::calculate_rhs(
CVODESolverStrategy::CVODERHSOutputData CVODESolverStrategy::calculate_rhs(
const sunrealtype t,
N_Vector y,
N_Vector ydot,
@@ -581,14 +595,6 @@ namespace gridfire::solver {
y_data[i] = 0.0;
}
}
// PERF: The trade off of ensured index consistency is some performance here. If this becomes a bottleneck we can revisit.
// The specific trade off is that we have decided to enforce that all interfaces accept composition objects rather
// than raw vectors of molar abundances. This then lets any method lookup the species by name rather than relying on
// the index in the vector being consistent. The trade off is that sometimes we need to construct a composition object
// which, at the moment, requires a somewhat expensive set of operations. Perhaps in the future we could enforce
// some consistent memory layout for the composition object to make this cheeper. That optimization would need to be
// done in the libcomposition library though...
std::vector<double> y_vec(y_data, y_data + numSpecies);
fourdst::composition::Composition composition(m_engine.getNetworkSpecies(), y_vec);
@@ -598,13 +604,15 @@ namespace gridfire::solver {
}
sunrealtype* ydot_data = N_VGetArrayPointer(ydot);
const auto& [dydt, nuclearEnergyGenerationRate] = result.value();
const auto& [dydt, nuclearEnergyGenerationRate, reactionContributions] = result.value();
for (size_t i = 0; i < numSpecies; ++i) {
fourdst::atomic::Species species = m_engine.getNetworkSpecies()[i];
ydot_data[i] = dydt.at(species);
}
ydot_data[numSpecies] = nuclearEnergyGenerationRate; // Set the last element to the specific energy rate
return {reactionContributions};
}
void CVODESolverStrategy::initialize_cvode_integration_resources(

View File

@@ -3,7 +3,7 @@
#include "gridfire/engine/engine.h"
#include "gridfire/expectations/expected_engine.h"
#include "fourdst/composition/atomicSpecies.h"
#include "fourdst/atomic/atomicSpecies.h"
#include <vector>
#include <expected>

View File

@@ -3,7 +3,6 @@
#include "bindings.h"
#include "gridfire/utils/general_composition.h"
#include "gridfire/utils/hashing.h"
namespace py = pybind11;
@@ -20,30 +19,6 @@ void register_utils_bindings(py::module &m) {
"Format a string for logging nuclear timescales based on temperature, density, and energy generation rate."
);
m.def(
"massFractionFromMolarAbundanceAndComposition",
&gridfire::utils::massFractionFromMolarAbundanceAndComposition,
py::arg("composition"),
py::arg("species"),
py::arg("Yi"),
"Convert a specific species molar abundance into its mass fraction if it were present in a given composition."
);
m.def(
"massFractionFromMolarAbundanceAndMolarMass",
&gridfire::utils::massFractionFromMolarAbundanceAndMolarMass,
py::arg("molarAbundances"),
py::arg("molarMasses"),
"Convert a vector of molar abundances and a parallel vector of molar masses into a vector of mass fractions"
);
m.def(
"molarMassVectorFromComposition",
&gridfire::utils::molarMassVectorFromComposition,
py::arg("composition"),
"Extract vector of molar masses from a composition object, this will be sorted by species mass so that the lightest species are at the front of the list."
);
m.def(
"hash_atomic",
&gridfire::utils::hash_atomic,

View File

@@ -1,4 +1,4 @@
[wrap-git]
url = https://github.com/4D-STAR/fourdst
revision = v0.9.4
revision = v0.9.5
depth = 1