SERiF 0.0.1a
3+1D Stellar Structure and Evolution
Loading...
Searching...
No Matches
netgraph.cpp
Go to the documentation of this file.
1#include "netgraph.h"
2#include "atomicSpecies.h"
3#include "const.h"
4#include "network.h"
5#include "reaclib.h"
6#include "species.h"
7
8#include "quill/LogMacros.h"
9
10#include <cstdint>
11#include <iostream>
12#include <set>
13#include <stdexcept>
14#include <string>
15#include <string_view>
16#include <unordered_map>
17#include <vector>
18
19#include <boost/numeric/ublas/vector.hpp>
20#include <boost/numeric/odeint.hpp>
21
22
23namespace serif::network {
31
34 const double cullingThreshold,
35 const double T9
36 ):
40 }
41
49
50 // --- Network Graph Construction Methods ---
52 m_networkSpecies.clear();
53 m_networkSpeciesMap.clear();
54
55 std::set<std::string_view> uniqueSpeciesNames;
56
57 for (const auto& reaction: m_reactions) {
58 for (const auto& reactant: reaction.reactants()) {
59 uniqueSpeciesNames.insert(reactant.name());
60 }
61 for (const auto& product: reaction.products()) {
62 uniqueSpeciesNames.insert(product.name());
63 }
64 }
65
66 for (const auto& name: uniqueSpeciesNames) {
67 auto it = serif::atomic::species.find(name);
68 if (it != serif::atomic::species.end()) {
69 m_networkSpecies.push_back(it->second);
70 m_networkSpeciesMap.insert({name, it->second});
71 } else {
72 LOG_ERROR(m_logger, "Species '{}' not found in global atomic species database.", name);
73 throw std::runtime_error("Species not found in global atomic species database: " + std::string(name));
74 }
75 }
76
77 }
78
80 LOG_INFO(m_logger, "Populating reaction ID map for REACLIB graph network (serif::network::GraphNetwork)...");
81 m_reactionIDMap.clear();
82 for (const auto& reaction: m_reactions) {
83 m_reactionIDMap.insert({reaction.id(), reaction});
84 }
85 LOG_INFO(m_logger, "Populated {} reactions in the reaction ID map.", m_reactionIDMap.size());
86 }
87
89 m_speciesToIndexMap.clear();
90 for (size_t i = 0; i < m_networkSpecies.size(); ++i) {
92 }
93 }
94
96 // The implementation of this function (and others) constrains this nuclear network to a constant temperature and density during
97 // each evaluation.
98 size_t numSpecies = m_networkSpecies.size();
99 m_jacobianMatrix.clear();
100 m_jacobianMatrix.resize(numSpecies, numSpecies, false); // Sparse matrix, no initial values
101 LOG_INFO(m_logger, "Jacobian matrix resized to {} rows and {} columns.",
102 m_jacobianMatrix.size1(), m_jacobianMatrix.size2());
103 }
104
105 // --- Basic Accessors and Queries ---
106 const std::vector<serif::atomic::Species>& GraphNetwork::getNetworkSpecies() const {
107 // Returns a constant reference to the vector of unique species in the network.
108 LOG_DEBUG(m_logger, "Providing access to network species vector. Size: {}.", m_networkSpecies.size());
109 return m_networkSpecies;
110 }
111
112 const reaclib::REACLIBReactionSet& GraphNetwork::getNetworkReactions() const {
113 // Returns a constant reference to the set of reactions in the network.
114 LOG_DEBUG(m_logger, "Providing access to network reactions set. Size: {}.", m_reactions.size());
115 return m_reactions;
116 }
117
118 bool GraphNetwork::involvesSpecies(const serif::atomic::Species& species) const {
119 // Checks if a given species is present in the network's species map for efficient lookup.
120 const bool found = m_networkSpeciesMap.contains(species.name());
121 LOG_DEBUG(m_logger, "Checking if species '{}' is involved in the network: {}.", species.name(), found ? "Yes" : "No");
122 return found;
123 }
124
125 std::unordered_map<serif::atomic::Species, int> GraphNetwork::getNetReactionStoichiometry(const reaclib::REACLIBReaction& reaction) const {
126 // Calculates the net stoichiometric coefficients for species in a given reaction.
127 std::unordered_map<serif::atomic::Species, int> stoichiometry;
128
129 // Iterate through reactants, decrementing their counts
130 for (const auto& reactant : reaction.reactants()) {
131 auto it = m_networkSpeciesMap.find(reactant.name());
132 if (it != m_networkSpeciesMap.end()) {
133 stoichiometry[it->second]--; // Copy Species by value (PERF: Future performance improvements by using pointers or references (std::reference_wrapper<const ...>) or something like that)
134 } else {
135 LOG_WARNING(m_logger, "Reactant species '{}' in reaction '{}' not found in network species map during stoichiometry calculation.",
136 reactant.name(), reaction.id());
137 }
138 }
139
140 // Iterate through products, incrementing their counts
141 for (const auto& product : reaction.products()) {
142 auto it = m_networkSpeciesMap.find(product.name());
143 if (it != m_networkSpeciesMap.end()) {
144 stoichiometry[it->second]++; // Copy Species by value (PERF: Future performance improvements by using pointers or references (std::reference_wrapper<const ...>) or something like that)
145 } else {
146 LOG_WARNING(m_logger, "Product species '{}' in reaction '{}' not found in network species map during stoichiometry calculation.",
147 product.name(), reaction.id());
148 }
149 }
150 LOG_DEBUG(m_logger, "Calculated net stoichiometry for reaction '{}'. Total unique species in stoichiometry: {}.", reaction.id(), stoichiometry.size());
151 return stoichiometry;
152 }
153
154 // --- Validation Methods ---
156 LOG_INFO(m_logger, "Validating mass (A) and charge (Z) conservation across all reactions in the network.");
157
158 for (const auto& reaction : m_reactions) {
159 uint64_t totalReactantA = 0;
160 uint64_t totalReactantZ = 0;
161 uint64_t totalProductA = 0;
162 uint64_t totalProductZ = 0;
163
164 // Calculate total A and Z for reactants
165 for (const auto& reactant : reaction.reactants()) {
166 auto it = m_networkSpeciesMap.find(reactant.name());
167 if (it != m_networkSpeciesMap.end()) {
168 totalReactantA += it->second.a();
169 totalReactantZ += it->second.z();
170 } else {
171 // This scenario indicates a severe data integrity issue:
172 // a reactant is part of a reaction but not in the network's species map.
173 LOG_ERROR(m_logger, "CRITICAL ERROR: Reactant species '{}' in reaction '{}' not found in network species map during conservation validation.",
174 reactant.name(), reaction.id());
175 return false;
176 }
177 }
178
179 // Calculate total A and Z for products
180 for (const auto& product : reaction.products()) {
181 auto it = m_networkSpeciesMap.find(product.name());
182 if (it != m_networkSpeciesMap.end()) {
183 totalProductA += it->second.a();
184 totalProductZ += it->second.z();
185 } else {
186 // Similar critical error for product species
187 LOG_ERROR(m_logger, "CRITICAL ERROR: Product species '{}' in reaction '{}' not found in network species map during conservation validation.",
188 product.name(), reaction.id());
189 return false;
190 }
191 }
192
193 // Compare totals for conservation
194 if (totalReactantA != totalProductA) {
195 LOG_ERROR(m_logger, "Mass number (A) not conserved for reaction '{}': Reactants A={} vs Products A={}.",
196 reaction.id(), totalReactantA, totalProductA);
197 return false;
198 }
199 if (totalReactantZ != totalProductZ) {
200 LOG_ERROR(m_logger, "Atomic number (Z) not conserved for reaction '{}': Reactants Z={} vs Products Z={}.",
201 reaction.id(), totalReactantZ, totalProductZ);
202 return false;
203 }
204 }
205
206 LOG_INFO(m_logger, "Mass (A) and charge (Z) conservation validated successfully for all reactions.");
207 return true; // All reactions passed the conservation check
208 }
209
211
212 // Check if the requested network has already been cached.
213 // PERF: Rebuilding this should be pretty fast but it may be a good point of optimization in the future.
214 const reaclib::REACLIBReactionSet validationReactionSet = build_reaclib_nuclear_network(composition, culling, T9);
215 // TODO: need some more robust method here to
216 // A. Build the basic network from the composition's species with non zero mass fractions.
217 // B. rebuild a new composition from the reaction set's reactants + products (with the mass fractions from the things that are only products set to 0)
218 // C. Rebuild the reaction set from the new composition
219 // D. Cull reactions where all reactants have mass fractions below the culling threshold.
220 // E. Be careful about maintaining caching through all of this
221
222
223 // This allows for dynamic network modification while retaining caching for networks which are very similar.
224 if (validationReactionSet != m_reactions) {
225 LOG_INFO(m_logger, "Reaction set not cached. Rebuilding the reaction set for T9={} and culling={}.", T9, culling);
226 m_reactions = validationReactionSet;
227 syncInternalMaps(); // Re-sync internal maps after updating reactions. Note this will also retrace the AD tape.
228 }
229 }
230
231 // --- Generate Stoichiometry Matrix ---
233 LOG_INFO(m_logger, "Generating stoichiometry matrix...");
234
235 // Task 1: Set dimensions and initialize the matrix
236 size_t numSpecies = m_networkSpecies.size();
237 size_t numReactions = m_reactions.size();
238 m_stoichiometryMatrix.resize(numSpecies, numReactions, false);
239
240 LOG_INFO(m_logger, "Stoichiometry matrix initialized with dimensions: {} rows (species) x {} columns (reactions).",
241 numSpecies, numReactions);
242
243 // Task 2: Populate the stoichiometry matrix
244 // Iterate through all reactions, assign them a column index, and fill in their stoichiometric coefficients.
245 size_t reactionColumnIndex = 0;
246 for (const auto& reaction : m_reactions) {
247 // Get the net stoichiometry for the current reaction
248 std::unordered_map<serif::atomic::Species, int> netStoichiometry = getNetReactionStoichiometry(reaction);
249
250 // Iterate through the species and their coefficients in the stoichiometry map
251 for (const auto& pair : netStoichiometry) {
252 const serif::atomic::Species& species = pair.first; // The Species object
253 const int coefficient = pair.second; // The stoichiometric coefficient
254
255 // Find the row index for this species
256 auto it = m_speciesToIndexMap.find(species);
257 if (it != m_speciesToIndexMap.end()) {
258 const size_t speciesRowIndex = it->second;
259 // Set the matrix element. Boost.uBLAS handles sparse insertion.
260 m_stoichiometryMatrix(speciesRowIndex, reactionColumnIndex) = coefficient;
261 } else {
262 // This scenario should ideally not happen if m_networkSpeciesMap and m_speciesToIndexMap are correctly synced
263 LOG_ERROR(m_logger, "CRITICAL ERROR: Species '{}' from reaction '{}' stoichiometry not found in species to index map.",
264 species.name(), reaction.id());
265 throw std::runtime_error("Species not found in species to index map: " + std::string(species.name()));
266 }
267 }
268 reactionColumnIndex++; // Move to the next column for the next reaction
269 }
270
271 LOG_INFO(m_logger, "Stoichiometry matrix population complete. Number of non-zero elements: {}.",
272 m_stoichiometryMatrix.nnz()); // Assuming nnz() exists for compressed_matrix
273 }
274
275 void GraphNetwork::generateJacobianMatrix(const std::vector<double> &Y, const double T9,
276 const double rho) {
277
278 LOG_INFO(m_logger, "Generating jacobian matrix for T9={}, rho={}..", T9, rho);
279 const size_t numSpecies = m_networkSpecies.size();
280
281 // 1. Pack the input variables into a vector for CppAD
282 std::vector<double> adInput(numSpecies + 2, 0.0); // +2 for T9 and rho
283 for (size_t i = 0; i < numSpecies; ++i) {
284 adInput[i] = Y[i];
285 }
286 adInput[numSpecies] = T9; // T9
287 adInput[numSpecies + 1] = rho; // rho
288
289 // 2. Calculate the full jacobian
290 const std::vector<double> dotY = m_rhsADFun.Jacobian(adInput);
291
292 // 3. Pack jacobian vector into sparse matrix
293 m_jacobianMatrix.clear();
294 for (size_t i = 0; i < numSpecies; ++i) {
295 for (size_t j = 0; j < numSpecies; ++j) {
296 const double value = dotY[i * (numSpecies + 2) + j];
297 if (std::abs(value) > MIN_JACOBIAN_THRESHOLD) {
298 m_jacobianMatrix(i, j) = value;
299 }
300 }
301 }
302 LOG_INFO(m_logger, "Jacobian matrix generated with dimensions: {} rows x {} columns.", m_jacobianMatrix.size1(), m_jacobianMatrix.size2());
303 }
304
305 void GraphNetwork::detectStiff(const NetIn &netIn, const double T9, const double numSpecies, const boost::numeric::ublas::vector<double>& Y) {
306 // --- Heuristic for automatic stiffness detection ---
307 const std::vector<double> initial_y_stl(Y.begin(), Y.begin() + numSpecies); // Copy only the species abundances, not the specific energy rate
308 const auto derivatives = calculateAllDerivatives<double>(initial_y_stl, T9, netIn.density);
309 const std::vector<double>& initial_dotY = derivatives.dydt;
310
311 double min_timescale = std::numeric_limits<double>::max();
312 double max_timescale = 0.0;
313
314 for (size_t i = 0; i < numSpecies; ++i) {
315 if (std::abs(initial_dotY[i]) > 0) {
316 const double timescale = std::abs(Y(i) / initial_dotY[i]);
317 if (timescale > max_timescale) {max_timescale = timescale;}
318 if (timescale < min_timescale) {min_timescale = timescale;}
319 }
320 }
321
322 const double stiffnessRatio = max_timescale / min_timescale;
323
324 // TODO: Pull this out into a configuration option or a more sophisticated heuristic.
325 constexpr double stiffnessThreshold = 1.0e6; // This is a heuristic threshold, can be tuned based on network characteristics.
326
327 LOG_INFO(m_logger, "Stiffness ratio is {} (max timescale: {}, min timescale: {}).", stiffnessRatio, max_timescale, min_timescale);
328 if (stiffnessRatio > stiffnessThreshold) {
329 LOG_INFO(m_logger, "Network is detected to be stiff. Using stiff ODE solver.");
330 m_stiff = true;
331 } else {
332 LOG_INFO(m_logger, "Network is detected to be non-stiff. Using non-stiff ODE solver.");
333 m_stiff = false;
334 }
335 }
336
338 namespace ublas = boost::numeric::ublas;
339 namespace odeint = boost::numeric::odeint;
340
341 const double T9 = netIn.temperature / 1e9; // Convert temperature from Kelvin to T9 (T9 = T / 1e9)
342 validateComposition(netIn.composition, netIn.culling, T9);
343
344 const double numSpecies = m_networkSpecies.size();
345 constexpr double abs_tol = 1.0e-8;
346 constexpr double rel_tol = 1.0e-8;
347
348 int stepCount = 0;
349
350 // TODO: Pull these out into configuration options
351
352 ODETerm rhs_functor(*this, T9, netIn.density);
353
354
355 ublas::vector<double> Y(numSpecies + 1);
356 for (size_t i = 0; i < numSpecies; ++i) {
357 const auto& species = m_networkSpecies[i];
358 // Get the mass fraction for this specific species from the input object
359 Y(i) = netIn.composition.getMassFraction(std::string(species.name()));
360 }
361 Y(numSpecies) = 0; // initial specific energy rate, will be updated later
362
363 detectStiff(netIn, T9, numSpecies, Y);
364
365 if (m_stiff) {
366 JacobianTerm jacobian_functor(*this, T9, netIn.density);
367 LOG_INFO(m_logger, "Making use of stiff ODE solver for network evaluation.");
368 auto stepper = odeint::make_controlled<odeint::rosenbrock4<double>>(abs_tol, rel_tol);
369 stepCount = odeint::integrate_adaptive(
370 stepper,
371 std::make_pair(rhs_functor, jacobian_functor),
372 Y,
373 0.0, // Start time
374 netIn.tMax,
375 netIn.dt0
376 );
377
378 } else {
379 LOG_INFO(m_logger, "Making use of ODE solver (non-stiff) for network evaluation.");
380 using state_type = ublas::vector<double>;
381 auto stepper = odeint::make_controlled<odeint::runge_kutta_dopri5<state_type>>(abs_tol, rel_tol);
382 stepCount = odeint::integrate_adaptive(
383 stepper,
384 rhs_functor,
385 Y,
386 0.0, // Start time
387 netIn.tMax,
388 netIn.dt0
389 );
390
391
392 }
393
394 double sumY = 0.0;
395 for (int i = 0; i < numSpecies; ++i) { sumY += Y(i); }
396 for (int i = 0; i < numSpecies; ++i) { Y(i) /= sumY; }
397
398 // --- Marshall output variables ---
399 // PERF: Im sure this step could be tuned to avoid so many copies, that is a job for another day
400 std::vector<std::string> speciesNames;
401 speciesNames.reserve(numSpecies);
402 for (const auto& species : m_networkSpecies) {
403 speciesNames.push_back(std::string(species.name()));
404 }
405
406 std::vector<double> finalAbundances(Y.begin(), Y.begin() + numSpecies);
407 serif::composition::Composition outputComposition(speciesNames, finalAbundances);
408 outputComposition.finalize(true);
409
410 NetOut netOut;
411 netOut.composition = outputComposition;
412 netOut.num_steps = stepCount;
413 netOut.energy = Y(numSpecies); // The last element in Y is the specific energy rate
414
415 return netOut;
416
417 }
418
420 LOG_INFO(m_logger, "Recording AD tape for the RHS calculation...");
421
422 // Task 1: Set dimensions and initialize the matrix
423 const size_t numSpecies = m_networkSpecies.size();
424 if (numSpecies == 0) {
425 LOG_ERROR(m_logger, "Cannot record AD tape: No species in the network.");
426 throw std::runtime_error("Cannot record AD tape: No species in the network.");
427 }
428 const size_t numADInputs = numSpecies + 2; // Note here that by not letting T9 and rho be independent variables, we are constraining the network to a constant temperature and density during each evaluation.
429
430 // --- CppAD Tape Recording ---
431 // 1. Declare independent variable (adY)
432 // We also initialize the dummy variable for tape recording (these tell CppAD what the derivative chain looks like).
433 // Their numeric values are irrelevant except for in so far as they avoid numerical instabilities.
434
435 // Distribute total mass fraction uniformly between species in the dummy variable space
436 const auto uniformMassFraction = static_cast<CppAD::AD<double>>(1.0 / numSpecies);
437 std::vector<CppAD::AD<double>> adInput(numADInputs, uniformMassFraction);
438 adInput[numSpecies] = 1.0; // Dummy T9
439 adInput[numSpecies + 1] = 1.0; // Dummy rho
440
441 // 3. Declare independent variables (what CppAD will differentiate wrt.)
442 // This also beings the tape recording process.
443 CppAD::Independent(adInput);
444
445 std::vector<CppAD::AD<double>> adY(numSpecies);
446 for(size_t i = 0; i < numSpecies; ++i) {
447 adY[i] = adInput[i];
448 }
449 const CppAD::AD<double> adT9 = adInput[numSpecies];
450 const CppAD::AD<double> adRho = adInput[numSpecies + 1];
451
452
453 // 5. Call the actual templated function
454 // We let T9 and rho be constant, so we pass them as fixed values.
455 auto derivatives = calculateAllDerivatives<CppAD::AD<double>>(adY, adT9, adRho);
456
457 m_rhsADFun.Dependent(adInput, derivatives.dydt);
458
459 LOG_INFO(m_logger, "AD tape recorded successfully for the RHS calculation. Number of independent variables: {}.",
460 adInput.size());
461 }
462}
Manages the composition of elements.
bool finalize(bool norm=false)
Finalizes the composition.
boost::numeric::ublas::compressed_matrix< double > m_jacobianMatrix
Jacobian matrix (species x species).
Definition netgraph.h:192
const std::vector< serif::atomic::Species > & getNetworkSpecies() const
Get the vector of unique species in the network.
Definition netgraph.cpp:106
std::unordered_map< std::string_view, serif::atomic::Species > m_networkSpeciesMap
Map from species name to Species object.
Definition netgraph.h:188
std::unordered_map< std::string_view, const reaclib::REACLIBReaction > m_reactionIDMap
Map from reaction ID to REACLIBReaction. //PERF: This makes copies of REACLIBReaction and could be a ...
Definition netgraph.h:185
CppAD::ADFun< double > m_rhsADFun
CppAD function for the right-hand side of the ODE.
Definition netgraph.h:194
reaclib::REACLIBReactionSet m_reactions
Set of REACLIB reactions in the network.
Definition netgraph.h:184
std::unordered_map< serif::atomic::Species, int > getNetReactionStoichiometry(const reaclib::REACLIBReaction &reaction) const
Get the net stoichiometric coefficients for all species in a reaction.
Definition netgraph.cpp:125
void populateReactionIDMap()
Populate the reaction ID map from the reaction set.
Definition netgraph.cpp:79
void detectStiff(const NetIn &netIn, double T9, double numSpecies, const boost::numeric::ublas::vector< double > &Y)
Detect if the network is stiff and select the appropriate ODE solver.
Definition netgraph.cpp:305
GraphNetwork(const serif::composition::Composition &composition)
Construct a GraphNetwork from a composition.
Definition netgraph.cpp:24
bool validateConservation() const
Validate mass and charge conservation for all reactions.
Definition netgraph.cpp:155
void populateSpeciesToIndexMap()
Populate the species-to-index map for matrix construction.
Definition netgraph.cpp:88
void reserveJacobianMatrix()
Reserve and resize the Jacobian matrix.
Definition netgraph.cpp:95
void validateComposition(const serif::composition::Composition &composition, double culling, double T9)
Validate and update the network composition if needed.
Definition netgraph.cpp:210
void collectNetworkSpecies()
Collect all unique species from the reaction set.
Definition netgraph.cpp:51
std::unordered_map< serif::atomic::Species, size_t > m_speciesToIndexMap
Map from species to their index in the stoichiometry matrix.
Definition netgraph.h:189
void generateStoichiometryMatrix()
Generate the stoichiometry matrix for the network.
Definition netgraph.cpp:232
void syncInternalMaps()
Synchronize all internal maps and matrices after network changes.
Definition netgraph.cpp:42
bool involvesSpecies(const serif::atomic::Species &species) const
Check if a species is present in the network.
Definition netgraph.cpp:118
void generateJacobianMatrix(const std::vector< double > &Y, const double T9, const double rho)
Generate the Jacobian matrix for the network.
Definition netgraph.cpp:275
const reaclib::REACLIBReactionSet & getNetworkReactions() const
Get the set of REACLIB reactions in the network.
Definition netgraph.cpp:112
boost::numeric::ublas::compressed_matrix< int > m_stoichiometryMatrix
Stoichiometry matrix (species x reactions).
Definition netgraph.h:191
NetOut evaluate(const NetIn &netIn) override
Evolve the network for the given input conditions.
Definition netgraph.cpp:337
std::vector< serif::atomic::Species > m_networkSpecies
Vector of unique species in the network.
Definition netgraph.h:187
void recordADTape()
Record the CppAD tape for automatic differentiation.
Definition netgraph.cpp:419
StepDerivatives< T > calculateAllDerivatives(const std::vector< T > &Y, T T9, T rho) const
Calculate all derivatives (dY/dt and energy rate) for the current state.
Definition netgraph.h:445
bool m_stiff
Flag indicating if the network is stiff.
Definition network.h:141
Network(const NetworkFormat format=NetworkFormat::APPROX8)
Definition network.cpp:32
quill::Logger * m_logger
Logger instance.
Definition network.h:136
static constexpr double MIN_JACOBIAN_THRESHOLD
Minimum Jacobian entry threshold for sparsity.
Definition netgraph.h:52
@ REACLIB
General REACLIB nuclear reaction network format.
Definition network.h:38
serif::network::reaclib::REACLIBReactionSet build_reaclib_nuclear_network(const serif::composition::Composition &composition)
Definition network.cpp:74
Defines the GraphNetwork class for representing and evolving nuclear reaction networks as a heavily c...
Functor for Jacobian evaluation for stiff ODE solvers. (used in the NSE case)
Definition netgraph.h:237
Functor for ODE right-hand side evaluation.
Definition netgraph.h:201
Input structure for the network evaluation.
Definition network.h:65
Output structure for the network evaluation.
Definition network.h:89