2025-07-10 09:36:05 -04:00
# include "gridfire/engine/views/engine_multiscale.h"
2025-07-18 15:23:43 -04:00
# include "gridfire/exceptions/error_engine.h"
2025-09-19 15:14:46 -04:00
# include "gridfire/engine/procedures/priming.h"
2025-07-10 09:36:05 -04:00
2025-07-24 08:37:52 -04:00
# include <stdexcept>
2025-07-10 09:36:05 -04:00
# include <vector>
2025-10-07 15:16:03 -04:00
# include <ranges>
2025-07-10 09:36:05 -04:00
# include <unordered_map>
# include <unordered_set>
2025-07-16 12:14:02 -04:00
2025-07-24 10:20:44 -04:00
# include <queue>
2025-07-22 12:48:24 -04:00
# include <algorithm>
2025-07-18 15:23:43 -04:00
2025-07-16 12:14:02 -04:00
# include "quill/LogMacros.h"
# include "quill/Logger.h"
2025-07-10 09:36:05 -04:00
2025-07-18 15:23:43 -04:00
namespace {
2025-07-22 12:48:24 -04:00
using namespace fourdst : : atomic ;
2025-10-10 09:12:40 -04:00
//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 ,
2025-10-14 13:37:48 -04:00
const gridfire : : DynamicEngine & engine
2025-10-10 09:12:40 -04:00
) {
2025-07-18 15:23:43 -04:00
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 ) ;
}
2025-07-22 12:48:24 -04:00
2025-10-07 15:16:03 -04:00
std : : vector < std : : vector < Species > > findConnectedComponentsBFS (
const std : : unordered_map < Species , std : : vector < Species > > & graph ,
const std : : vector < Species > & nodes
2025-07-22 12:48:24 -04:00
) {
2025-10-07 15:16:03 -04:00
std : : vector < std : : vector < Species > > components ;
std : : unordered_set < Species > visited ;
2025-07-22 12:48:24 -04:00
2025-10-07 15:16:03 -04:00
for ( const Species & start_node : nodes ) {
2025-08-14 13:33:46 -04:00
if ( ! visited . contains ( start_node ) ) {
2025-10-07 15:16:03 -04:00
std : : vector < Species > current_component ;
std : : queue < Species > q ;
2025-07-22 12:48:24 -04:00
q . push ( start_node ) ;
visited . insert ( start_node ) ;
while ( ! q . empty ( ) ) {
2025-10-07 15:16:03 -04:00
Species u = q . front ( ) ;
2025-07-22 12:48:24 -04:00
q . pop ( ) ;
current_component . push_back ( u ) ;
2025-08-14 13:33:46 -04:00
if ( graph . contains ( u ) ) {
2025-07-22 12:48:24 -04:00
for ( const auto & v : graph . at ( u ) ) {
2025-08-14 13:33:46 -04:00
if ( ! visited . contains ( v ) ) {
2025-07-22 12:48:24 -04:00
visited . insert ( v ) ;
q . push ( v ) ;
}
}
}
}
components . push_back ( current_component ) ;
}
}
return components ;
}
struct SpeciesSetIntersection {
const Species species ;
std : : size_t count ;
} ;
std : : expected < SpeciesSetIntersection , std : : string > get_intersection_info (
const std : : unordered_set < Species > & setA ,
const std : : unordered_set < Species > & setB
) {
// Iterate over the smaller of the two
auto * outerSet = & setA ;
auto * innerSet = & setB ;
if ( setA . size ( ) > setB . size ( ) ) {
outerSet = & setB ;
innerSet = & setA ;
}
std : : size_t matchCount = 0 ;
const Species * firstMatch = nullptr ;
for ( const Species & sp : * outerSet ) {
if ( innerSet - > contains ( sp ) ) {
if ( matchCount = = 0 ) {
firstMatch = & sp ;
}
+ + matchCount ;
if ( matchCount > 1 ) {
break ;
}
}
}
if ( ! firstMatch ) {
// No matches found
return std : : unexpected { " Intersection is empty " } ;
}
if ( matchCount = = 0 ) {
// No matches found
return std : : unexpected { " No intersection found " } ;
}
// Return the first match and the count of matches
return SpeciesSetIntersection { * firstMatch , matchCount } ;
}
bool has_distinct_reactant_and_product_species (
const std : : unordered_set < Species > & poolSpecies ,
const std : : unordered_set < Species > & reactants ,
const std : : unordered_set < Species > & products
) {
const auto reactant_result = get_intersection_info ( poolSpecies , reactants ) ;
if ( ! reactant_result ) {
return false ; // No reactants found
}
const auto [ reactantSample , reactantCount ] = reactant_result . value ( ) ;
const auto product_result = get_intersection_info ( poolSpecies , products ) ;
if ( ! product_result ) {
return false ; // No products found
}
const auto [ productSample , productCount ] = product_result . value ( ) ;
// If either side has ≥2 distinct matches, we can always pick
// one from each that differ.
if ( reactantCount > 1 | | productCount > 1 ) {
return true ;
}
// Exactly one match on each side → they must differ
return reactantSample ! = productSample ;
}
2025-10-12 07:52:30 -04:00
const std : : unordered_map < Eigen : : LevenbergMarquardtSpace : : Status , std : : string > lm_status_map = {
{ Eigen : : LevenbergMarquardtSpace : : Status : : NotStarted , " NotStarted " } ,
{ Eigen : : LevenbergMarquardtSpace : : Status : : Running , " Running " } ,
{ Eigen : : LevenbergMarquardtSpace : : Status : : ImproperInputParameters , " ImproperInputParameters " } ,
{ Eigen : : LevenbergMarquardtSpace : : Status : : RelativeReductionTooSmall , " RelativeReductionTooSmall " } ,
{ Eigen : : LevenbergMarquardtSpace : : Status : : RelativeErrorTooSmall , " RelativeErrorTooSmall " } ,
{ Eigen : : LevenbergMarquardtSpace : : Status : : RelativeErrorAndReductionTooSmall , " RelativeErrorAndReductionTooSmall " } ,
{ Eigen : : LevenbergMarquardtSpace : : Status : : CosinusTooSmall , " CosinusTooSmall " } ,
{ Eigen : : LevenbergMarquardtSpace : : Status : : TooManyFunctionEvaluation , " TooManyFunctionEvaluation " } ,
{ Eigen : : LevenbergMarquardtSpace : : Status : : FtolTooSmall , " FtolTooSmall " } ,
{ Eigen : : LevenbergMarquardtSpace : : Status : : XtolTooSmall , " XtolTooSmall " } ,
{ Eigen : : LevenbergMarquardtSpace : : Status : : GtolTooSmall , " GtolTooSmall " } ,
{ Eigen : : LevenbergMarquardtSpace : : Status : : UserAsked , " UserAsked " }
} ;
2025-07-18 15:23:43 -04:00
}
2025-07-10 09:36:05 -04:00
namespace gridfire {
using fourdst : : atomic : : Species ;
MultiscalePartitioningEngineView : : MultiscalePartitioningEngineView (
2025-10-14 13:37:48 -04:00
DynamicEngine & baseEngine
2025-07-10 09:36:05 -04:00
) : m_baseEngine ( baseEngine ) { }
const std : : vector < Species > & MultiscalePartitioningEngineView : : getNetworkSpecies ( ) const {
2025-07-18 15:23:43 -04:00
return m_baseEngine . getNetworkSpecies ( ) ;
2025-07-10 09:36:05 -04:00
}
2025-07-22 12:48:24 -04:00
std : : expected < StepDerivatives < double > , expectations : : StaleEngineError > MultiscalePartitioningEngineView : : calculateRHSAndEnergy (
2025-10-07 15:16:03 -04:00
const fourdst : : composition : : Composition & comp ,
2025-07-10 09:36:05 -04:00
const double T9 ,
const double rho
) const {
2025-07-18 15:23:43 -04:00
// 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.
2025-10-07 15:16:03 -04:00
const auto result = m_baseEngine . calculateRHSAndEnergy ( comp , T9 , rho ) ;
2025-07-22 12:48:24 -04:00
if ( ! result ) {
return std : : unexpected { result . error ( ) } ;
}
auto deriv = result . value ( ) ;
2025-07-18 15:23:43 -04:00
2025-10-07 15:16:03 -04:00
for ( const auto & species : m_algebraic_species ) {
deriv . dydt [ species ] = 0.0 ; // Fix the algebraic species to the equilibrium abundances we calculate.
2025-07-18 15:23:43 -04:00
}
return deriv ;
2025-07-10 09:36:05 -04:00
}
2025-09-19 15:14:46 -04:00
EnergyDerivatives MultiscalePartitioningEngineView : : calculateEpsDerivatives (
2025-10-07 15:16:03 -04:00
const fourdst : : composition : : Composition & comp ,
2025-09-19 15:14:46 -04:00
const double T9 ,
const double rho
) const {
2025-10-07 15:16:03 -04:00
return m_baseEngine . calculateEpsDerivatives ( comp , T9 , rho ) ;
2025-09-19 15:14:46 -04:00
}
2025-07-10 09:36:05 -04:00
void MultiscalePartitioningEngineView : : generateJacobianMatrix (
2025-10-07 15:16:03 -04:00
const fourdst : : composition : : Composition & comp ,
2025-07-10 09:36:05 -04:00
const double T9 ,
const double rho
2025-07-18 15:23:43 -04:00
) const {
// TODO: Add sparsity pattern to this to prevent base engine from doing unnecessary work.
2025-10-07 15:16:03 -04:00
m_baseEngine . generateJacobianMatrix ( comp , T9 , rho ) ;
2025-07-10 09:36:05 -04:00
}
double MultiscalePartitioningEngineView : : getJacobianMatrixEntry (
2025-10-07 15:16:03 -04:00
const Species & rowSpecies ,
const Species & colSpecies
2025-07-10 09:36:05 -04:00
) const {
2025-09-19 15:14:46 -04:00
// Check if the species we are differentiating with respect to is algebraic or dynamic. If it is algebraic we can reduce the work significantly...
2025-10-07 15:16:03 -04:00
if ( std : : ranges : : contains ( m_algebraic_species , colSpecies ) ) {
2025-09-19 15:14:46 -04:00
return 0.0 ;
}
2025-10-07 15:16:03 -04:00
if ( std : : ranges : : contains ( m_algebraic_species , rowSpecies ) ) {
2025-09-22 11:15:14 -04:00
return 0.0 ;
}
2025-10-07 15:16:03 -04:00
return m_baseEngine . getJacobianMatrixEntry ( rowSpecies , colSpecies ) ;
2025-07-10 09:36:05 -04:00
}
void MultiscalePartitioningEngineView : : generateStoichiometryMatrix ( ) {
m_baseEngine . generateStoichiometryMatrix ( ) ;
}
int MultiscalePartitioningEngineView : : getStoichiometryMatrixEntry (
2025-10-07 15:16:03 -04:00
const Species & species ,
const reaction : : Reaction & reaction
2025-07-10 09:36:05 -04:00
) const {
2025-10-07 15:16:03 -04:00
return m_baseEngine . getStoichiometryMatrixEntry ( species , reaction ) ;
2025-07-10 09:36:05 -04:00
}
double MultiscalePartitioningEngineView : : calculateMolarReactionFlow (
const reaction : : Reaction & reaction ,
2025-10-07 15:16:03 -04:00
const fourdst : : composition : : Composition & comp ,
2025-07-14 14:50:49 -04:00
const double T9 ,
const double rho
2025-07-10 09:36:05 -04:00
) const {
2025-07-18 15:23:43 -04:00
// Fix the algebraic species to the equilibrium abundances we calculate.
2025-10-07 15:16:03 -04:00
fourdst : : composition : : Composition comp_mutable = comp ;
for ( const auto & species : m_algebraic_species ) {
// TODO: Check this conversion to mass fraction (also consider adding the ability to set molar abundance directly)
const double Yi = m_algebraic_abundances . at ( species ) ;
comp_mutable . setMassFraction ( species , Yi * species . a ( ) / ( rho * 1e-3 ) ) ; // Convert Yi (mol/g) to Xi (mass fraction)
}
if ( ! comp_mutable . finalize ( ) ) {
LOG_ERROR ( m_logger , " Failed to finalize composition after setting algebraic species abundances. " ) ;
m_logger - > flush_log ( ) ;
throw std : : runtime_error ( " Failed to finalize composition after setting algebraic species abundances. " ) ;
2025-07-18 15:23:43 -04:00
}
2025-10-07 15:16:03 -04:00
return m_baseEngine . calculateMolarReactionFlow ( reaction , comp_mutable , T9 , rho ) ;
2025-07-10 09:36:05 -04:00
}
2025-08-14 13:33:46 -04:00
const reaction : : ReactionSet & MultiscalePartitioningEngineView : : getNetworkReactions ( ) const {
2025-07-10 09:36:05 -04:00
return m_baseEngine . getNetworkReactions ( ) ;
}
2025-08-14 13:33:46 -04:00
void MultiscalePartitioningEngineView : : setNetworkReactions ( const reaction : : ReactionSet & reactions ) {
2025-07-22 12:48:24 -04:00
LOG_CRITICAL ( m_logger , " setNetworkReactions is not supported in MultiscalePartitioningEngineView. Did you mean to call this on the base engine? " ) ;
throw exceptions : : UnableToSetNetworkReactionsError ( " setNetworkReactions is not supported in MultiscalePartitioningEngineView. Did you mean to call this on the base engine? " ) ;
}
std : : expected < std : : unordered_map < Species , double > , expectations : : StaleEngineError > MultiscalePartitioningEngineView : : getSpeciesTimescales (
2025-10-07 15:16:03 -04:00
const fourdst : : composition : : Composition & comp ,
2025-07-10 09:36:05 -04:00
const double T9 ,
const double rho
) const {
2025-10-07 15:16:03 -04:00
const auto result = m_baseEngine . getSpeciesTimescales ( comp , T9 , rho ) ;
2025-07-22 12:48:24 -04:00
if ( ! result ) {
return std : : unexpected { result . error ( ) } ;
}
std : : unordered_map < Species , double > speciesTimescales = result . value ( ) ;
2025-07-18 15:23:43 -04:00
for ( const auto & algebraicSpecies : m_algebraic_species ) {
speciesTimescales [ algebraicSpecies ] = std : : numeric_limits < double > : : infinity ( ) ; // Algebraic species have infinite timescales.
}
return speciesTimescales ;
}
2025-07-22 12:48:24 -04:00
std : : expected < std : : unordered_map < fourdst : : atomic : : Species , double > , expectations : : StaleEngineError >
MultiscalePartitioningEngineView : : getSpeciesDestructionTimescales (
2025-10-07 15:16:03 -04:00
const fourdst : : composition : : Composition & comp ,
2025-09-29 13:35:48 -04:00
const double T9 ,
const double rho
2025-07-22 12:48:24 -04:00
) const {
2025-10-07 15:16:03 -04:00
const auto result = m_baseEngine . getSpeciesDestructionTimescales ( comp , T9 , rho ) ;
2025-07-22 12:48:24 -04:00
if ( ! result ) {
return std : : unexpected { result . error ( ) } ;
}
std : : unordered_map < Species , double > speciesDestructionTimescales = result . value ( ) ;
for ( const auto & algebraicSpecies : m_algebraic_species ) {
speciesDestructionTimescales [ algebraicSpecies ] = std : : numeric_limits < double > : : infinity ( ) ; // Algebraic species have infinite destruction timescales.
}
return speciesDestructionTimescales ;
}
2025-07-18 15:23:43 -04:00
fourdst : : composition : : Composition MultiscalePartitioningEngineView : : update ( const NetIn & netIn ) {
const fourdst : : composition : : Composition baseUpdatedComposition = m_baseEngine . update ( netIn ) ;
NetIn baseUpdatedNetIn = netIn ;
baseUpdatedNetIn . composition = baseUpdatedComposition ;
const fourdst : : composition : : Composition equilibratedComposition = equilibrateNetwork ( baseUpdatedNetIn ) ;
2025-10-07 15:16:03 -04:00
std : : unordered_map < Species , double > algebraicAbundances ;
for ( const auto & species : m_algebraic_species ) {
algebraicAbundances [ species ] = equilibratedComposition . getMolarAbundance ( species ) ;
2025-07-18 15:23:43 -04:00
}
2025-07-22 12:48:24 -04:00
2025-10-07 15:16:03 -04:00
m_algebraic_abundances = std : : move ( algebraicAbundances ) ;
2025-07-22 12:48:24 -04:00
2025-07-18 15:23:43 -04:00
return equilibratedComposition ;
2025-07-10 09:36:05 -04:00
}
2025-07-18 15:23:43 -04:00
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 ) ) {
2025-07-22 12:48:24 -04:00
return m_baseEngine . isStale ( netIn ) ; // The cache hit indicates the engine is not stale for the given conditions.
2025-07-18 15:23:43 -04:00
}
return true ;
2025-07-10 09:36:05 -04:00
}
void MultiscalePartitioningEngineView : : setScreeningModel (
const screening : : ScreeningType model
) {
m_baseEngine . setScreeningModel ( model ) ;
}
screening : : ScreeningType MultiscalePartitioningEngineView : : getScreeningModel ( ) const {
return m_baseEngine . getScreeningModel ( ) ;
}
const DynamicEngine & MultiscalePartitioningEngineView : : getBaseEngine ( ) const {
return m_baseEngine ;
}
2025-10-07 15:16:03 -04:00
std : : vector < std : : vector < Species > > MultiscalePartitioningEngineView : : analyzeTimescalePoolConnectivity (
const std : : vector < std : : vector < Species > > & timescale_pools ,
const fourdst : : composition : : Composition & comp ,
2025-07-22 12:48:24 -04:00
double T9 ,
double rho
) const {
2025-10-07 15:16:03 -04:00
std : : vector < std : : vector < Species > > final_connected_pools ;
2025-07-22 12:48:24 -04:00
for ( const auto & pool : timescale_pools ) {
if ( pool . empty ( ) ) {
continue ; // Skip empty pools
}
// For each timescale pool, we need to analyze connectivity.
auto connectivity_graph = buildConnectivityGraph ( pool ) ;
auto components = findConnectedComponentsBFS ( connectivity_graph , pool ) ;
final_connected_pools . insert ( final_connected_pools . end ( ) , components . begin ( ) , components . end ( ) ) ;
}
return final_connected_pools ;
}
2025-07-10 09:36:05 -04:00
void MultiscalePartitioningEngineView : : partitionNetwork (
2025-10-07 15:16:03 -04:00
const fourdst : : composition : : Composition & comp ,
2025-07-10 09:36:05 -04:00
const double T9 ,
2025-07-16 12:14:02 -04:00
const double rho
2025-07-10 09:36:05 -04:00
) {
// --- Step 0. Clear previous state ---
2025-07-16 12:14:02 -04:00
LOG_TRACE_L1 ( m_logger , " Partitioning network... " ) ;
LOG_TRACE_L1 ( m_logger , " Clearing previous state... " ) ;
2025-07-10 09:36:05 -04:00
m_qse_groups . clear ( ) ;
m_dynamic_species . clear ( ) ;
2025-07-16 12:14:02 -04:00
m_algebraic_species . clear ( ) ;
// --- Step 1. Identify distinct timescale regions ---
LOG_TRACE_L1 ( m_logger , " Identifying fast reactions... " ) ;
2025-10-07 15:16:03 -04:00
const std : : vector < std : : vector < Species > > timescale_pools = partitionByTimescale ( comp , T9 , rho ) ;
2025-07-16 12:14:02 -04:00
LOG_TRACE_L1 ( m_logger , " Found {} timescale pools. " , timescale_pools . size ( ) ) ;
// --- Step 2. Select the mean slowest pool as the base dynamical group ---
LOG_TRACE_L1 ( m_logger , " Identifying mean slowest pool... " ) ;
2025-10-07 15:16:03 -04:00
const size_t mean_slowest_pool_index = identifyMeanSlowestPool ( timescale_pools , comp , T9 , rho ) ;
2025-07-16 12:14:02 -04:00
LOG_TRACE_L1 ( m_logger , " Mean slowest pool index: {} " , mean_slowest_pool_index ) ;
// --- Step 3. Push the slowest pool into the dynamic species list ---
2025-10-07 15:16:03 -04:00
for ( const auto & slowSpecies : timescale_pools [ mean_slowest_pool_index ] ) {
m_dynamic_species . push_back ( slowSpecies ) ;
2025-07-16 12:14:02 -04:00
}
2025-07-10 09:36:05 -04:00
2025-07-16 12:14:02 -04:00
// --- Step 4. Pack Candidate QSE Groups ---
2025-10-07 15:16:03 -04:00
std : : vector < std : : vector < Species > > candidate_pools ;
2025-07-16 12:14:02 -04:00
for ( size_t i = 0 ; i < timescale_pools . size ( ) ; + + i ) {
if ( i = = mean_slowest_pool_index ) continue ; // Skip the slowest pool
LOG_TRACE_L1 ( m_logger , " Group {} with {} species identified for potential QSE. " , i , timescale_pools [ i ] . size ( ) ) ;
candidate_pools . push_back ( timescale_pools [ i ] ) ;
}
2025-07-10 09:36:05 -04:00
2025-07-22 12:48:24 -04:00
LOG_TRACE_L1 ( m_logger , " Preforming connectivity analysis on timescale pools... " ) ;
2025-10-07 15:16:03 -04:00
const std : : vector < std : : vector < Species > > connected_pools = analyzeTimescalePoolConnectivity ( candidate_pools , comp , T9 , rho ) ;
2025-07-22 12:48:24 -04:00
LOG_TRACE_L1 ( m_logger , " Found {} connected pools (compared to {} timescale pools) for QSE analysis. " , connected_pools . size ( ) , timescale_pools . size ( ) ) ;
2025-07-16 12:14:02 -04:00
// --- Step 5. Identify potential seed species for each candidate pool ---
LOG_TRACE_L1 ( m_logger , " Identifying potential seed species for candidate pools... " ) ;
2025-10-07 15:16:03 -04:00
const std : : vector < QSEGroup > candidate_groups = constructCandidateGroups ( connected_pools , comp , T9 , rho ) ;
2025-07-24 08:37:52 -04:00
LOG_TRACE_L1 ( m_logger , " Found {} candidate QSE groups for further analysis " , candidate_groups . size ( ) ) ;
2025-09-19 15:14:46 -04:00
LOG_TRACE_L2 (
m_logger ,
" {} " ,
[ & ] ( ) - > std : : string {
std : : stringstream ss ;
int j = 0 ;
for ( const auto & group : candidate_groups ) {
ss < < " CandidateQSEGroup(Algebraic: { " ;
int i = 0 ;
2025-10-07 15:16:03 -04:00
for ( const auto & species : group . algebraic_species ) {
ss < < species . name ( ) ;
if ( i < group . algebraic_species . size ( ) - 1 ) {
2025-09-19 15:14:46 -04:00
ss < < " , " ;
}
}
ss < < " }, Seed: { " ;
i = 0 ;
2025-10-07 15:16:03 -04:00
for ( const auto & species : group . seed_species ) {
ss < < species . name ( ) ;
if ( i < group . seed_species . size ( ) - 1 ) {
2025-09-19 15:14:46 -04:00
ss < < " , " ;
}
i + + ;
}
ss < < " }) " ;
if ( j < candidate_groups . size ( ) - 1 ) {
ss < < " , " ;
}
j + + ;
}
return ss . str ( ) ;
} ( )
) ;
2025-07-16 12:14:02 -04:00
LOG_TRACE_L1 ( m_logger , " Validating candidate groups with flux analysis... " ) ;
2025-10-07 15:16:03 -04:00
const auto [ validated_groups , invalidate_groups ] = validateGroupsWithFluxAnalysis ( candidate_groups , comp , T9 , rho ) ;
2025-07-16 12:14:02 -04:00
LOG_TRACE_L1 (
m_logger ,
" Validated {} group(s) QSE groups. {} " ,
validated_groups . size ( ) ,
[ & ] ( ) - > std : : string {
std : : stringstream ss ;
int count = 0 ;
for ( const auto & group : validated_groups ) {
ss < < " Group " < < count + 1 ;
if ( group . is_in_equilibrium ) {
ss < < " is in equilibrium " ;
} else {
ss < < " is not in equilibrium " ;
}
if ( count < validated_groups . size ( ) - 1 ) {
ss < < " , " ;
}
count + + ;
}
return ss . str ( ) ;
} ( )
) ;
2025-07-10 09:36:05 -04:00
2025-09-19 15:14:46 -04:00
// Push the invalidated groups' species into the dynamic set
for ( const auto & group : invalidate_groups ) {
2025-10-07 15:16:03 -04:00
for ( const auto & species : group . algebraic_species ) {
m_dynamic_species . push_back ( species ) ;
2025-09-19 15:14:46 -04:00
}
}
2025-08-14 13:33:46 -04:00
m_qse_groups = validated_groups ;
2025-07-16 12:14:02 -04:00
LOG_TRACE_L1 ( m_logger , " Identified {} QSE groups. " , m_qse_groups . size ( ) ) ;
2025-07-10 09:36:05 -04:00
2025-07-16 12:14:02 -04:00
for ( const auto & group : m_qse_groups ) {
// Add algebraic species to the algebraic set
2025-10-07 15:16:03 -04:00
for ( const auto & species : group . algebraic_species ) {
if ( std : : ranges : : find ( m_algebraic_species , species ) = = m_algebraic_species . end ( ) ) {
m_algebraic_species . push_back ( species ) ;
2025-07-16 12:14:02 -04:00
}
}
}
2025-07-10 09:36:05 -04:00
2025-07-22 12:48:24 -04:00
LOG_INFO (
m_logger ,
" Partitioning complete. Found {} dynamic species, {} algebraic (QSE) species ({}) spread over {} QSE group{}. " ,
m_dynamic_species . size ( ) ,
m_algebraic_species . size ( ) ,
[ & ] ( ) - > std : : string {
std : : stringstream ss ;
2025-07-24 10:20:44 -04:00
size_t count = 0 ;
2025-07-22 12:48:24 -04:00
for ( const auto & species : m_algebraic_species ) {
ss < < species . name ( ) ;
if ( m_algebraic_species . size ( ) > 1 & & count < m_algebraic_species . size ( ) - 1 ) {
ss < < " , " ;
}
count + + ;
}
return ss . str ( ) ;
} ( ) ,
m_qse_groups . size ( ) ,
m_qse_groups . size ( ) = = 1 ? " " : " s "
) ;
2025-07-10 09:36:05 -04:00
}
void MultiscalePartitioningEngineView : : partitionNetwork (
2025-07-16 12:14:02 -04:00
const NetIn & netIn
2025-07-10 09:36:05 -04:00
) {
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
2025-10-07 15:16:03 -04:00
partitionNetwork ( netIn . composition , T9 , rho ) ;
2025-07-10 09:36:05 -04:00
}
2025-07-16 12:14:02 -04:00
void MultiscalePartitioningEngineView : : exportToDot (
const std : : string & filename ,
2025-10-07 15:16:03 -04:00
const fourdst : : composition : : Composition & comp ,
2025-07-16 12:14:02 -04:00
const double T9 ,
const double rho
) const {
2025-07-10 09:36:05 -04:00
std : : ofstream dotFile ( filename ) ;
if ( ! dotFile . is_open ( ) ) {
LOG_ERROR ( m_logger , " Failed to open file for writing: {} " , filename ) ;
throw std : : runtime_error ( " Failed to open file for writing: " + filename ) ;
}
2025-07-16 12:14:02 -04:00
const auto & all_species = m_baseEngine . getNetworkSpecies ( ) ;
const auto & all_reactions = m_baseEngine . getNetworkReactions ( ) ;
// --- 1. Pre-computation and Categorization ---
// Categorize species into algebraic, seed, and core dynamic
2025-10-07 15:16:03 -04:00
std : : unordered_set < Species > algebraic_species ;
std : : unordered_set < Species > seed_species ;
2025-07-16 12:14:02 -04:00
for ( const auto & group : m_qse_groups ) {
if ( group . is_in_equilibrium ) {
2025-10-07 15:16:03 -04:00
algebraic_species . insert ( group . algebraic_species . begin ( ) , group . algebraic_species . end ( ) ) ;
seed_species . insert ( group . seed_species . begin ( ) , group . seed_species . end ( ) ) ;
2025-07-16 12:14:02 -04:00
}
}
// Calculate reaction flows and find min/max for logarithmic scaling of transparency
std : : vector < double > reaction_flows ;
reaction_flows . reserve ( all_reactions . size ( ) ) ;
double min_log_flow = std : : numeric_limits < double > : : max ( ) ;
double max_log_flow = std : : numeric_limits < double > : : lowest ( ) ;
for ( const auto & reaction : all_reactions ) {
2025-10-07 15:16:03 -04:00
double flow = std : : abs ( m_baseEngine . calculateMolarReactionFlow ( * reaction , comp , T9 , rho ) ) ;
2025-07-16 12:14:02 -04:00
reaction_flows . push_back ( flow ) ;
if ( flow > 1e-99 ) { // Avoid log(0)
double log_flow = std : : log10 ( flow ) ;
min_log_flow = std : : min ( min_log_flow , log_flow ) ;
max_log_flow = std : : max ( max_log_flow , log_flow ) ;
}
}
const double log_flow_range = ( max_log_flow > min_log_flow ) ? ( max_log_flow - min_log_flow ) : 1.0 ;
// --- 2. Write DOT file content ---
2025-07-10 09:36:05 -04:00
dotFile < < " digraph PartitionedNetwork { \n " ;
2025-07-16 12:14:02 -04:00
dotFile < < " graph [rankdir=TB, splines=true, overlap=false, bgcolor= \" #f8fafc \" , label= \" Multiscale Partitioned Network View \" , fontname= \" Helvetica \" , fontsize=16, labeljust=l]; \n " ;
dotFile < < " node [shape=circle, style=filled, fontname= \" Helvetica \" , width=0.8, fixedsize=true]; \n " ;
dotFile < < " edge [fontname= \" Helvetica \" , fontsize=10]; \n \n " ;
// --- Node Definitions ---
// Define all species nodes first, so they can be referenced by clusters and ranks later.
dotFile < < " // --- Species Nodes Definitions --- \n " ;
std : : map < int , std : : vector < std : : string > > species_by_mass ;
2025-10-07 15:16:03 -04:00
for ( const auto & species : all_species ) {
2025-07-16 12:14:02 -04:00
std : : string fillcolor = " #f1f5f9 " ; // Default: Other/Uninvolved
// Determine color based on category. A species can be a seed and also in the core dynamic group.
// The more specific category (algebraic, then seed) takes precedence.
2025-10-07 15:16:03 -04:00
if ( algebraic_species . contains ( species ) ) {
2025-07-16 12:14:02 -04:00
fillcolor = " #e0f2fe " ; // Light Blue: Algebraic (in QSE)
2025-10-07 15:16:03 -04:00
} else if ( seed_species . contains ( species ) ) {
2025-07-16 12:14:02 -04:00
fillcolor = " #a7f3d0 " ; // Light Green: Seed (Dynamic, feeds a QSE group)
2025-10-07 15:16:03 -04:00
} else if ( std : : ranges : : contains ( m_dynamic_species , species ) ) {
2025-07-16 12:14:02 -04:00
fillcolor = " #dcfce7 " ; // Pale Green: Core Dynamic
}
dotFile < < " \" " < < species . name ( ) < < " \" [label= \" " < < species . name ( ) < < " \" , fillcolor= \" " < < fillcolor < < " \" ]; \n " ;
2025-07-10 09:36:05 -04:00
2025-07-16 12:14:02 -04:00
// Group species by mass number for ranked layout.
// If species.a() returns incorrect values (e.g., 0 for many species), they will be grouped together here.
2025-08-14 13:33:46 -04:00
species_by_mass [ species . a ( ) ] . emplace_back ( species . name ( ) ) ;
2025-07-10 09:36:05 -04:00
}
dotFile < < " \n " ;
2025-07-16 12:14:02 -04:00
// --- Layout and Ranking ---
// Enforce a top-down layout based on mass number.
dotFile < < " // --- Layout using Ranks --- \n " ;
2025-07-18 15:23:43 -04:00
for ( const auto & species_list : species_by_mass | std : : views : : values ) {
2025-07-16 12:14:02 -04:00
dotFile < < " { rank=same; " ;
for ( const auto & name : species_list ) {
dotFile < < " \" " < < name < < " \" ; " ;
2025-07-10 09:36:05 -04:00
}
2025-07-16 12:14:02 -04:00
dotFile < < " } \n " ;
}
dotFile < < " \n " ;
// Chain by mass to get top down ordering
dotFile < < " // --- Chain by Mass --- \n " ;
for ( const auto & [ mass , species_list ] : species_by_mass ) {
// Find the next largest mass in the species list
int minLargestMass = std : : numeric_limits < int > : : max ( ) ;
2025-07-18 15:23:43 -04:00
for ( const auto & next_mass : species_by_mass | std : : views : : keys ) {
2025-07-16 12:14:02 -04:00
if ( next_mass > mass & & next_mass < minLargestMass ) {
minLargestMass = next_mass ;
}
}
if ( minLargestMass ! = std : : numeric_limits < int > : : max ( ) ) {
// Connect the current mass to the next largest mass
dotFile < < " \" " < < species_list [ 0 ] < < " \" -> \" " < < species_by_mass [ minLargestMass ] [ 0 ] < < " \" [style=invis]; \n " ;
2025-07-10 09:36:05 -04:00
}
}
2025-07-16 12:14:02 -04:00
// --- QSE Group Clusters ---
// Draw a prominent box around the algebraic species of each valid QSE group.
2025-07-10 09:36:05 -04:00
dotFile < < " // --- QSE Group Clusters --- \n " ;
int group_counter = 0 ;
for ( const auto & group : m_qse_groups ) {
2025-10-07 15:16:03 -04:00
if ( ! group . is_in_equilibrium | | group . algebraic_species . empty ( ) ) {
2025-07-16 12:14:02 -04:00
continue ;
}
dotFile < < " subgraph cluster_qse_ " < < group_counter + + < < " { \n " ;
dotFile < < " label = \" QSE Group " < < group_counter < < " \" ; \n " ;
2025-07-10 09:36:05 -04:00
dotFile < < " style = \" filled,rounded \" ; \n " ;
2025-07-16 12:14:02 -04:00
dotFile < < " color = \" #38bdf8 \" ; \n " ; // A bright, visible blue for the border
dotFile < < " penwidth = 2.0; \n " ; // Thicker border
dotFile < < " bgcolor = \" #f0f9ff80 \" ; \n " ; // Light blue fill with transparency
dotFile < < " subgraph cluster_seed_ " < < group_counter < < " { \n " ;
dotFile < < " label = \" Seed Species \" ; \n " ;
dotFile < < " style = \" filled,rounded \" ; \n " ;
dotFile < < " color = \" #a7f3d0 \" ; \n " ; // Light green for seed species
dotFile < < " penwidth = 1.5; \n " ; // Thinner border for seed cluster
std : : vector < std : : string > seed_node_ids ;
2025-10-07 15:16:03 -04:00
seed_node_ids . reserve ( group . seed_species . size ( ) ) ;
for ( const auto & species : group . seed_species ) {
2025-07-16 12:14:02 -04:00
std : : stringstream ss ;
2025-10-07 15:16:03 -04:00
ss < < " node_ " < < group_counter < < " _seed_ " < < species . name ( ) ;
dotFile < < " " < < ss . str ( ) < < " [label= \" " < < species . name ( ) < < " \" ]; \n " ;
2025-07-16 12:14:02 -04:00
seed_node_ids . push_back ( ss . str ( ) ) ;
2025-07-10 09:36:05 -04:00
}
2025-07-16 12:14:02 -04:00
for ( size_t i = 0 ; i < seed_node_ids . size ( ) - 1 ; + + i ) {
dotFile < < " " < < seed_node_ids [ i ] < < " -> " < < seed_node_ids [ i + 1 ] < < " [style=invis]; \n " ;
2025-07-10 09:36:05 -04:00
}
2025-07-16 12:14:02 -04:00
dotFile < < " } \n " ;
dotFile < < " subgraph cluster_algebraic_ " < < group_counter < < " { \n " ;
dotFile < < " label = \" Algebraic Species \" ; \n " ;
dotFile < < " style = \" filled,rounded \" ; \n " ;
dotFile < < " color = \" #e0f2fe \" ; \n " ; // Light blue for algebraic species
dotFile < < " penwidth = 1.5; \n " ; // Thinner border for algebraic cluster
std : : vector < std : : string > algebraic_node_ids ;
2025-10-07 15:16:03 -04:00
algebraic_node_ids . reserve ( group . algebraic_species . size ( ) ) ;
for ( const Species & species : group . algebraic_species ) {
2025-07-16 12:14:02 -04:00
std : : stringstream ss ;
2025-10-07 15:16:03 -04:00
ss < < " node_ " < < group_counter < < " _algebraic_ " < < species . name ( ) ;
dotFile < < " " < < ss . str ( ) < < " [label= \" " < < species . name ( ) < < " \" ]; \n " ;
2025-07-16 12:14:02 -04:00
algebraic_node_ids . push_back ( ss . str ( ) ) ;
}
2025-07-18 15:23:43 -04:00
// Make invisible edges between algebraic indices to keep them in top-down order
2025-07-16 12:14:02 -04:00
for ( size_t i = 0 ; i < algebraic_node_ids . size ( ) - 1 ; + + i ) {
dotFile < < " " < < algebraic_node_ids [ i ] < < " -> " < < algebraic_node_ids [ i + 1 ] < < " [style=invis]; \n " ;
}
dotFile < < " } \n " ;
2025-07-10 09:36:05 -04:00
dotFile < < " } \n " ;
2025-07-16 12:14:02 -04:00
}
dotFile < < " \n " ;
// --- Legend ---
// Add a legend to explain colors and conventions.
dotFile < < " // --- Legend --- \n " ;
dotFile < < " subgraph cluster_legend { \n " ;
dotFile < < " rank = sink " ; // Try to push the legend to the bottom
dotFile < < " label = \" Legend \" ; \n " ;
dotFile < < " bgcolor = \" #ffffff \" ; \n " ;
dotFile < < " color = \" #e2e8f0 \" ; \n " ;
dotFile < < " node [shape=box, style=filled, fontname= \" Helvetica \" ]; \n " ;
dotFile < < " key_core [label= \" Core Dynamic \" , fillcolor= \" #dcfce7 \" ]; \n " ;
dotFile < < " key_seed [label= \" Seed (Dynamic) \" , fillcolor= \" #a7f3d0 \" ]; \n " ;
dotFile < < " key_qse [label= \" Algebraic (QSE) \" , fillcolor= \" #e0f2fe \" ]; \n " ;
dotFile < < " key_other [label= \" Other \" , fillcolor= \" #f1f5f9 \" ]; \n " ;
dotFile < < " key_info [label= \" Edge Opacity ~ log(Reaction Flow) \" , shape=plaintext]; \n " ;
dotFile < < " " ; // Use invisible edges to stack legend items vertically
dotFile < < " key_core -> key_seed -> key_qse -> key_other -> key_info [style=invis]; \n " ;
dotFile < < " } \n \n " ;
// --- Reaction Edges ---
// Draw edges with transparency scaled by the log of the molar reaction flow.
dotFile < < " // --- Reaction Edges --- \n " ;
for ( size_t i = 0 ; i < all_reactions . size ( ) ; + + i ) {
const auto & reaction = all_reactions [ i ] ;
const double flow = reaction_flows [ i ] ;
if ( flow < 1e-99 ) continue ; // Don't draw edges for negligible flows
double log_flow_val = std : : log10 ( flow ) ;
double norm_alpha = ( log_flow_val - min_log_flow ) / log_flow_range ;
int alpha_val = 0x30 + static_cast < int > ( norm_alpha * ( 0xFF - 0x30 ) ) ; // Scale from ~20% to 100% opacity
alpha_val = std : : clamp ( alpha_val , 0x00 , 0xFF ) ;
std : : stringstream alpha_hex ;
alpha_hex < < std : : setw ( 2 ) < < std : : setfill ( ' 0 ' ) < < std : : hex < < alpha_val ;
std : : string edge_color = " #475569 " + alpha_hex . str ( ) ;
std : : string reactionNodeId = " reaction_ " + std : : string ( reaction . id ( ) ) ;
dotFile < < " \" " < < reactionNodeId < < " \" [shape=point, fillcolor=black, width=0.05, height=0.05]; \n " ;
for ( const auto & reactant : reaction . reactants ( ) ) {
dotFile < < " \" " < < reactant . name ( ) < < " \" -> \" " < < reactionNodeId < < " \" [color= \" " < < edge_color < < " \" , arrowhead=none]; \n " ;
}
for ( const auto & product : reaction . products ( ) ) {
dotFile < < " \" " < < reactionNodeId < < " \" -> \" " < < product . name ( ) < < " \" [color= \" " < < edge_color < < " \" ]; \n " ;
}
dotFile < < " \n " ;
2025-07-10 09:36:05 -04:00
}
dotFile < < " } \n " ;
dotFile . close ( ) ;
}
2025-07-16 12:14:02 -04:00
2025-07-10 09:36:05 -04:00
std : : vector < double > MultiscalePartitioningEngineView : : mapNetInToMolarAbundanceVector ( const NetIn & netIn ) const {
std : : vector < double > Y ( m_dynamic_species . size ( ) , 0.0 ) ; // Initialize with zeros
for ( const auto & [ symbol , entry ] : netIn . composition ) {
Y [ getSpeciesIndex ( entry . isotope ( ) ) ] = netIn . composition . getMolarAbundance ( symbol ) ; // Map species to their molar abundance
}
return Y ; // Return the vector of molar abundances
}
std : : vector < Species > MultiscalePartitioningEngineView : : getFastSpecies ( ) const {
const auto & all_species = m_baseEngine . getNetworkSpecies ( ) ;
std : : vector < Species > fast_species ;
fast_species . reserve ( all_species . size ( ) - m_dynamic_species . size ( ) ) ;
for ( const auto & species : all_species ) {
auto it = std : : ranges : : find ( m_dynamic_species , species ) ;
if ( it = = m_dynamic_species . end ( ) ) {
fast_species . push_back ( species ) ;
}
}
return fast_species ;
}
const std : : vector < Species > & MultiscalePartitioningEngineView : : getDynamicSpecies ( ) const {
return m_dynamic_species ;
}
2025-07-14 14:50:49 -04:00
PrimingReport MultiscalePartitioningEngineView : : primeEngine ( const NetIn & netIn ) {
return m_baseEngine . primeEngine ( netIn ) ;
}
fourdst : : composition : : Composition MultiscalePartitioningEngineView : : equilibrateNetwork (
2025-10-07 15:16:03 -04:00
const fourdst : : composition : : Composition & comp ,
2025-07-10 09:36:05 -04:00
const double T9 ,
2025-07-16 12:14:02 -04:00
const double rho
2025-07-10 09:36:05 -04:00
) {
2025-10-07 15:16:03 -04:00
partitionNetwork ( comp , T9 , rho ) ;
fourdst : : composition : : Composition qseComposition = solveQSEAbundances ( comp , T9 , rho ) ;
2025-07-14 14:50:49 -04:00
2025-10-07 15:16:03 -04:00
for ( const auto & symbol : qseComposition | std : : views : : keys ) {
const double speciesMassFraction = qseComposition . getMassFraction ( symbol ) ;
if ( speciesMassFraction < 0.0 & & std : : abs ( speciesMassFraction ) < 1e-20 ) {
qseComposition . setMassFraction ( symbol , 0.0 ) ; // Avoid negative mass fractions
2025-07-16 12:14:02 -04:00
}
2025-07-14 14:50:49 -04:00
}
2025-10-14 13:37:48 -04:00
bool didFinalize = qseComposition . finalize ( true ) ;
if ( ! didFinalize ) {
LOG_ERROR ( m_logger , " Failed to finalize composition after solving QSE abundances. " ) ;
m_logger - > flush_log ( ) ;
throw std : : runtime_error ( " Failed to finalize composition after solving QSE abundances. " ) ;
}
2025-07-18 15:23:43 -04:00
2025-10-07 15:16:03 -04:00
return qseComposition ;
2025-07-10 09:36:05 -04:00
}
2025-07-14 14:50:49 -04:00
fourdst : : composition : : Composition MultiscalePartitioningEngineView : : equilibrateNetwork (
2025-07-16 12:14:02 -04:00
const NetIn & netIn
2025-07-10 09:36:05 -04:00
) {
2025-07-18 15:23:43 -04:00
const PrimingReport primingReport = m_baseEngine . primeEngine ( netIn ) ;
2025-07-10 09:36:05 -04:00
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
2025-10-07 15:16:03 -04:00
return equilibrateNetwork ( primingReport . primedComposition , T9 , rho ) ;
2025-07-10 09:36:05 -04:00
}
2025-10-07 15:16:03 -04:00
size_t MultiscalePartitioningEngineView : : getSpeciesIndex ( const Species & species ) const {
2025-07-10 09:36:05 -04:00
return m_baseEngine . getSpeciesIndex ( species ) ;
}
2025-09-19 15:14:46 -04:00
2025-10-07 15:16:03 -04:00
std : : vector < std : : vector < Species > > MultiscalePartitioningEngineView : : partitionByTimescale (
const fourdst : : composition : : Composition & comp ,
2025-07-10 09:36:05 -04:00
const double T9 ,
2025-07-16 12:14:02 -04:00
const double rho
2025-07-10 09:36:05 -04:00
) const {
2025-07-16 12:14:02 -04:00
LOG_TRACE_L1 ( m_logger , " Partitioning by timescale... " ) ;
2025-10-14 13:37:48 -04:00
const auto destructionTimescale = m_baseEngine . getSpeciesDestructionTimescales ( comp , T9 , rho ) ;
2025-10-07 15:16:03 -04:00
const auto netTimescale = m_baseEngine . getSpeciesTimescales ( comp , T9 , rho ) ;
2025-10-10 09:12:40 -04:00
2025-10-14 13:37:48 -04:00
if ( ! destructionTimescale ) {
2025-09-19 15:14:46 -04:00
LOG_ERROR ( m_logger , " Failed to get species destruction timescales due to stale engine state " ) ;
2025-07-22 12:48:24 -04:00
m_logger - > flush_log ( ) ;
2025-09-19 15:14:46 -04:00
throw exceptions : : StaleEngineError ( " Failed to get species destruction timescales due to stale engine state " ) ;
}
if ( ! netTimescale ) {
LOG_ERROR ( m_logger , " Failed to get net species timescales due to stale engine state " ) ;
m_logger - > flush_log ( ) ;
throw exceptions : : StaleEngineError ( " Failed to get net species timescales due to stale engine state " ) ;
2025-07-22 12:48:24 -04:00
}
2025-10-14 13:37:48 -04:00
const std : : unordered_map < Species , double > & destruction_timescales = destructionTimescale . value ( ) ;
2025-09-19 15:14:46 -04:00
const std : : unordered_map < Species , double > & net_timescales = netTimescale . value ( ) ;
2025-10-10 09:12:40 -04:00
2025-10-14 13:37:48 -04:00
for ( const auto & [ species , destruction_timescale ] : destruction_timescales ) {
LOG_TRACE_L3 ( m_logger , " For {} destruction timescale is {} s " , species . name ( ) , destruction_timescale ) ;
}
2025-07-16 12:14:02 -04:00
const auto & all_species = m_baseEngine . getNetworkSpecies ( ) ;
2025-07-10 09:36:05 -04:00
2025-10-14 13:37:48 -04:00
std : : vector < std : : pair < double , Species > > sorted_destruction_timescales ;
for ( const auto & species : all_species ) {
double destruction_timescale = destruction_timescales . at ( species ) ;
double net_timescale = net_timescales . at ( species ) ;
if ( std : : isfinite ( destruction_timescale ) & & destruction_timescale > 0 ) {
LOG_TRACE_L3 ( m_logger , " Species {} has finite destruction timescale: destruction: {} s, net: {} s " , species . name ( ) , destruction_timescale , net_timescale ) ;
sorted_destruction_timescales . emplace_back ( destruction_timescale , species ) ;
2025-09-19 15:14:46 -04:00
} else {
2025-10-14 13:37:48 -04:00
LOG_TRACE_L3 ( m_logger , " Species {} has infinite or negative destruction timescale: destruction: {} s, net: {} s " , species . name ( ) , destruction_timescale , net_timescale ) ;
2025-07-16 12:14:02 -04:00
}
}
2025-07-10 09:36:05 -04:00
2025-07-16 12:14:02 -04:00
std : : ranges : : sort (
2025-10-14 13:37:48 -04:00
sorted_destruction_timescales ,
2025-07-16 12:14:02 -04:00
[ ] ( const auto & a , const auto & b )
{
return a . first > b . first ;
}
) ;
2025-07-10 09:36:05 -04:00
2025-10-07 15:16:03 -04:00
std : : vector < std : : vector < Species > > final_pools ;
2025-10-14 13:37:48 -04:00
if ( sorted_destruction_timescales . empty ( ) ) {
2025-07-16 12:14:02 -04:00
return final_pools ;
}
2025-07-10 09:36:05 -04:00
2025-07-16 12:14:02 -04:00
constexpr double ABSOLUTE_QSE_TIMESCALE_THRESHOLD = 3.156e7 ; // Absolute threshold for QSE timescale (1 yr)
2025-07-22 12:48:24 -04:00
constexpr double MIN_GAP_THRESHOLD = 2.0 ; // Require a 2 order of magnitude gap
2025-10-14 13:37:48 -04:00
constexpr double MIN_MOLAR_ABUNDANCE_THRESHOLD = 1e-10 ; // Minimum abundance threshold to consider a species for QSE. Any species above this will always be considered dynamic.
2025-07-10 09:36:05 -04:00
2025-10-14 13:37:48 -04:00
LOG_TRACE_L1 ( m_logger , " Found {} species with finite timescales. " , sorted_destruction_timescales . size ( ) ) ;
2025-07-16 12:14:02 -04:00
LOG_TRACE_L1 ( m_logger , " Absolute QSE timescale threshold: {} seconds ({} years). " ,
ABSOLUTE_QSE_TIMESCALE_THRESHOLD , ABSOLUTE_QSE_TIMESCALE_THRESHOLD / 3.156e7 ) ;
LOG_TRACE_L1 ( m_logger , " Minimum gap threshold: {} orders of magnitude. " , MIN_GAP_THRESHOLD ) ;
2025-10-14 13:37:48 -04:00
LOG_TRACE_L1 ( m_logger , " Minimum molar abundance threshold: {}. " , MIN_MOLAR_ABUNDANCE_THRESHOLD ) ;
2025-07-10 09:36:05 -04:00
2025-10-07 15:16:03 -04:00
std : : vector < Species > dynamic_pool_species ;
std : : vector < std : : pair < double , Species > > fast_candidates ;
2025-07-10 09:36:05 -04:00
2025-07-16 12:14:02 -04:00
// 1. First Pass: Absolute Timescale Cutoff
2025-10-14 13:37:48 -04:00
for ( const auto & [ destruction_timescale , species ] : sorted_destruction_timescales ) {
if ( species = = n_1 ) {
LOG_TRACE_L3 ( m_logger , " Skipping neutron (n) from timescale analysis. Neutrons are always considered dynamic due to their extremely high connectivity. " ) ;
dynamic_pool_species . push_back ( species ) ;
continue ;
}
if ( destruction_timescale > ABSOLUTE_QSE_TIMESCALE_THRESHOLD ) {
2025-07-22 12:48:24 -04:00
LOG_TRACE_L3 ( m_logger , " Species {} with timescale {} is considered dynamic (slower than qse timescale threshold). " ,
2025-10-14 13:37:48 -04:00
species . name ( ) , destruction_timescale ) ;
2025-10-07 15:16:03 -04:00
dynamic_pool_species . push_back ( species ) ;
2025-07-16 12:14:02 -04:00
} else {
2025-10-14 13:37:48 -04:00
const double Yi = comp . getMolarAbundance ( species ) ;
if ( Yi > MIN_MOLAR_ABUNDANCE_THRESHOLD ) {
LOG_TRACE_L3 ( m_logger , " Species {} with abundance {} is considered dynamic (above minimum abundance threshold of {}). " ,
species . name ( ) , Yi , MIN_MOLAR_ABUNDANCE_THRESHOLD ) ;
dynamic_pool_species . push_back ( species ) ;
continue ;
}
LOG_TRACE_L3 ( m_logger , " Species {} with timescale {} and molar abundance {} is a candidate fast species (faster than qse timescale threshold and less than the molar abundance threshold). " ,
species . name ( ) , destruction_timescale , Yi ) ;
fast_candidates . emplace_back ( destruction_timescale , species ) ;
2025-07-10 09:36:05 -04:00
}
2025-07-16 12:14:02 -04:00
}
2025-07-10 09:36:05 -04:00
2025-10-07 15:16:03 -04:00
if ( ! dynamic_pool_species . empty ( ) ) {
LOG_TRACE_L1 ( m_logger , " Found {} dynamic species (slower than QSE timescale threshold). " , dynamic_pool_species . size ( ) ) ;
final_pools . push_back ( dynamic_pool_species ) ;
2025-07-16 12:14:02 -04:00
}
if ( fast_candidates . empty ( ) ) {
LOG_TRACE_L1 ( m_logger , " No fast candidates found. " ) ;
return final_pools ;
}
// 2. Second Pass: Gap Detection on the remaining "fast" candidates
std : : vector < size_t > split_points ;
for ( size_t i = 0 ; i < fast_candidates . size ( ) - 1 ; + + i ) {
const double t1 = fast_candidates [ i ] . first ;
const double t2 = fast_candidates [ i + 1 ] . first ;
if ( std : : log10 ( t1 ) - std : : log10 ( t2 ) > MIN_GAP_THRESHOLD ) {
2025-07-22 12:48:24 -04:00
LOG_TRACE_L3 ( m_logger , " Detected gap between species {} (timescale {:0.2E}) and {} (timescale {:0.2E}). " ,
2025-10-07 15:16:03 -04:00
fast_candidates [ i ] . second . name ( ) , t1 ,
fast_candidates [ i + 1 ] . second . name ( ) , t2 ) ;
2025-07-16 12:14:02 -04:00
split_points . push_back ( i + 1 ) ;
2025-07-10 09:36:05 -04:00
}
2025-07-16 12:14:02 -04:00
}
2025-07-10 09:36:05 -04:00
2025-07-16 12:14:02 -04:00
size_t last_split = 0 ;
for ( const size_t split : split_points ) {
2025-10-07 15:16:03 -04:00
std : : vector < Species > pool ;
2025-07-16 12:14:02 -04:00
for ( size_t i = last_split ; i < split ; + + i ) {
pool . push_back ( fast_candidates [ i ] . second ) ;
2025-07-10 09:36:05 -04:00
}
2025-07-16 12:14:02 -04:00
final_pools . push_back ( pool ) ;
last_split = split ;
2025-07-10 09:36:05 -04:00
}
2025-07-16 12:14:02 -04:00
2025-10-07 15:16:03 -04:00
std : : vector < Species > final_fast_pool ;
2025-07-16 12:14:02 -04:00
for ( size_t i = last_split ; i < fast_candidates . size ( ) ; + + i ) {
final_fast_pool . push_back ( fast_candidates [ i ] . second ) ;
}
final_pools . push_back ( final_fast_pool ) ;
LOG_TRACE_L1 ( m_logger , " Final partitioned pools: {} " ,
[ & ] ( ) - > std : : string {
std : : stringstream ss ;
int oc = 0 ;
for ( const auto & pool : final_pools ) {
ss < < " [ " ;
int ic = 0 ;
2025-10-07 15:16:03 -04:00
for ( const auto & species : pool ) {
ss < < species . name ( ) ;
2025-07-16 12:14:02 -04:00
if ( ic < pool . size ( ) - 1 ) {
ss < < " , " ;
}
ic + + ;
}
ss < < " ] " ;
if ( oc < final_pools . size ( ) - 1 ) {
ss < < " , " ;
}
oc + + ;
}
return ss . str ( ) ;
} ( ) ) ;
return final_pools ;
2025-07-10 09:36:05 -04:00
}
2025-09-19 15:14:46 -04:00
std : : pair < std : : vector < MultiscalePartitioningEngineView : : QSEGroup > , std : : vector < MultiscalePartitioningEngineView : :
QSEGroup > >
2025-07-16 12:14:02 -04:00
MultiscalePartitioningEngineView : : validateGroupsWithFluxAnalysis (
const std : : vector < QSEGroup > & candidate_groups ,
2025-10-07 15:16:03 -04:00
const fourdst : : composition : : Composition & comp ,
2025-07-16 12:14:02 -04:00
const double T9 , const double rho
) const {
2025-09-19 15:14:46 -04:00
std : : vector < QSEGroup > validated_groups ;
std : : vector < QSEGroup > invalidated_groups ;
validated_groups . reserve ( candidate_groups . size ( ) ) ;
for ( auto & group : candidate_groups ) {
2025-10-14 13:37:48 -04:00
constexpr double FLUX_RATIO_THRESHOLD = 5 ;
constexpr double LOG_FLOW_RATIO_THRESHOLD = 2 ;
2025-10-07 15:16:03 -04:00
const std : : unordered_set < Species > algebraic_group_members (
group . algebraic_species . begin ( ) ,
group . algebraic_species . end ( )
2025-09-19 15:14:46 -04:00
) ;
2025-10-07 15:16:03 -04:00
const std : : unordered_set < Species > seed_group_members (
group . seed_species . begin ( ) ,
group . seed_species . end ( )
2025-07-10 09:36:05 -04:00
) ;
2025-10-14 13:37:48 -04:00
// Values for measuring the flux coupling vs leakage
2025-09-19 15:14:46 -04:00
double coupling_flux = 0.0 ;
double leakage_flux = 0.0 ;
2025-10-14 13:37:48 -04:00
// Values for validating if the group could physically be in equilibrium
double creationFlux = 0.0 ;
double destructionFlux = 0.0 ;
2025-07-10 09:36:05 -04:00
for ( const auto & reaction : m_baseEngine . getNetworkReactions ( ) ) {
2025-10-07 15:16:03 -04:00
const double flow = std : : abs ( m_baseEngine . calculateMolarReactionFlow ( * reaction , comp , T9 , rho ) ) ;
2025-07-10 09:36:05 -04:00
if ( flow = = 0.0 ) {
continue ; // Skip reactions with zero flow
}
2025-09-19 15:14:46 -04:00
bool has_internal_algebraic_reactant = false ;
2025-07-10 09:36:05 -04:00
2025-08-14 13:33:46 -04:00
for ( const auto & reactant : reaction - > reactants ( ) ) {
2025-10-07 15:16:03 -04:00
if ( algebraic_group_members . contains ( reactant ) ) {
2025-09-19 15:14:46 -04:00
has_internal_algebraic_reactant = true ;
2025-10-14 13:37:48 -04:00
LOG_TRACE_L3 ( m_logger , " Adjusting destruction flux (+= {} mol g^-1 s^-1) for QSEGroup due to reactant {} from reaction {} " ,
flow , reactant . name ( ) , reaction - > id ( ) ) ;
destructionFlux + = flow ;
2025-07-10 09:36:05 -04:00
}
2025-10-14 13:37:48 -04:00
2025-07-10 09:36:05 -04:00
}
2025-09-19 15:14:46 -04:00
bool has_internal_algebraic_product = false ;
2025-07-10 09:36:05 -04:00
2025-08-14 13:33:46 -04:00
for ( const auto & product : reaction - > products ( ) ) {
2025-10-07 15:16:03 -04:00
if ( algebraic_group_members . contains ( product ) ) {
2025-09-19 15:14:46 -04:00
has_internal_algebraic_product = true ;
2025-10-14 13:37:48 -04:00
LOG_TRACE_L3 ( m_logger , " Adjusting creation flux (+= {} mol g^-1 s^-1) for QSEGroup due to product {} from reaction {} " ,
flow , product . name ( ) , reaction - > id ( ) ) ;
creationFlux + = flow ;
2025-09-19 15:14:46 -04:00
}
}
if ( ! has_internal_algebraic_product & & ! has_internal_algebraic_reactant ) {
2025-10-07 15:16:03 -04:00
LOG_TRACE_L3 ( m_logger , " {}: Skipping reaction {} as it has no internal algebraic species in reactants or products. " , group . toString ( ) , reaction - > id ( ) ) ;
2025-09-19 15:14:46 -04:00
continue ;
}
double algebraic_participants = 0 ;
double seed_participants = 0 ;
double external_participants = 0 ;
std : : unordered_set < Species > participants ;
for ( const auto & p : reaction - > reactants ( ) ) participants . insert ( p ) ;
for ( const auto & p : reaction - > products ( ) ) participants . insert ( p ) ;
for ( const auto & species : participants ) {
2025-10-07 15:16:03 -04:00
if ( algebraic_group_members . contains ( species ) ) {
LOG_TRACE_L3 ( m_logger , " {}: Species {} is an algebraic participant in reaction {}. " , group . toString ( ) , species . name ( ) , reaction - > id ( ) ) ;
2025-09-19 15:14:46 -04:00
algebraic_participants + + ;
2025-10-07 15:16:03 -04:00
} else if ( seed_group_members . contains ( species ) ) {
LOG_TRACE_L3 ( m_logger , " {}: Species {} is a seed participant in reaction {}. " , group . toString ( ) , species . name ( ) , reaction - > id ( ) ) ;
2025-09-19 15:14:46 -04:00
seed_participants + + ;
2025-07-10 09:36:05 -04:00
} else {
2025-10-07 15:16:03 -04:00
LOG_TRACE_L3 ( m_logger , " {}: Species {} is an external participant in reaction {}. " , group . toString ( ) , species . name ( ) , reaction - > id ( ) ) ;
2025-09-19 15:14:46 -04:00
external_participants + + ;
2025-07-10 09:36:05 -04:00
}
}
2025-09-19 15:14:46 -04:00
const double total_participants = algebraic_participants + seed_participants + external_participants ;
if ( total_participants = = 0 ) {
LOG_CRITICAL ( m_logger , " Some catastrophic error has occurred. Reaction {} has no participants. " , reaction - > id ( ) ) ;
throw std : : runtime_error ( " Some catastrophic error has occurred. Reaction " + std : : string ( reaction - > id ( ) ) + " has no participants. " ) ;
2025-07-10 09:36:05 -04:00
}
2025-09-19 15:14:46 -04:00
const double leakage_fraction = external_participants / total_participants ;
const double coupling_fraction = ( algebraic_participants + seed_participants ) / total_participants ;
leakage_flux + = flow * leakage_fraction ;
coupling_flux + = flow * coupling_fraction ;
2025-10-14 13:37:48 -04:00
2025-07-10 09:36:05 -04:00
}
2025-09-19 15:14:46 -04:00
2025-10-14 13:37:48 -04:00
bool group_is_coupled = ( coupling_flux / leakage_flux ) > FLUX_RATIO_THRESHOLD ;
bool group_is_balanced = std : : abs ( std : : log ( creationFlux ) - std : : log ( destructionFlux ) ) < LOG_FLOW_RATIO_THRESHOLD ;
if ( group_is_coupled ) {
2025-07-22 12:48:24 -04:00
LOG_TRACE_L1 (
m_logger ,
2025-10-14 13:37:48 -04:00
" Group containing {} is in equilibrium due to high coupling flux and balanced creation and destruction rate: <coupling: leakage flux = {}, coupling flux = {}, ratio = {} (Threshold: {})>, <creation: creation flux = {}, destruction flux = {}, ratio = {} order of mag (Threshold: {} order of mag)> " ,
2025-07-22 12:48:24 -04:00
[ & ] ( ) - > std : : string {
std : : stringstream ss ;
int count = 0 ;
2025-10-07 15:16:03 -04:00
for ( const auto & species : group . algebraic_species ) {
ss < < species . name ( ) ;
if ( count < group . algebraic_species . size ( ) - 1 ) {
2025-07-22 12:48:24 -04:00
ss < < " , " ;
}
count + + ;
}
return ss . str ( ) ;
} ( ) ,
2025-09-19 15:14:46 -04:00
leakage_flux ,
coupling_flux ,
coupling_flux / leakage_flux ,
2025-10-14 13:37:48 -04:00
FLUX_RATIO_THRESHOLD ,
std : : log10 ( creationFlux ) ,
std : : log10 ( destructionFlux ) ,
std : : abs ( std : : log10 ( creationFlux ) - std : : log10 ( destructionFlux ) ) ,
LOG_FLOW_RATIO_THRESHOLD
2025-07-22 12:48:24 -04:00
) ;
2025-09-19 15:14:46 -04:00
validated_groups . emplace_back ( group ) ;
validated_groups . back ( ) . is_in_equilibrium = true ;
2025-07-10 09:36:05 -04:00
} else {
2025-07-22 12:48:24 -04:00
LOG_TRACE_L1 (
m_logger ,
2025-10-14 13:37:48 -04:00
" Group containing {} is NOT in equilibrium: <coupling: leakage flux = {}, coupling flux = {}, ratio = {} (Threshold: {})>, <creation: creation flux = {}, destruction flux = {}, ratio = {} order of mag (Threshold: {} order of mag)> " ,
2025-07-22 12:48:24 -04:00
[ & ] ( ) - > std : : string {
std : : stringstream ss ;
int count = 0 ;
2025-10-07 15:16:03 -04:00
for ( const auto & species : group . algebraic_species ) {
ss < < species . name ( ) ;
if ( count < group . algebraic_species . size ( ) - 1 ) {
2025-07-22 12:48:24 -04:00
ss < < " , " ;
}
count + + ;
}
return ss . str ( ) ;
} ( ) ,
2025-09-19 15:14:46 -04:00
leakage_flux ,
coupling_flux ,
coupling_flux / leakage_flux ,
2025-10-14 13:37:48 -04:00
FLUX_RATIO_THRESHOLD ,
std : : log10 ( creationFlux ) ,
std : : log10 ( destructionFlux ) ,
std : : abs ( std : : log10 ( creationFlux ) - std : : log10 ( destructionFlux ) ) ,
LOG_FLOW_RATIO_THRESHOLD
2025-07-22 12:48:24 -04:00
) ;
2025-09-19 15:14:46 -04:00
invalidated_groups . emplace_back ( group ) ;
invalidated_groups . back ( ) . is_in_equilibrium = false ;
2025-07-10 09:36:05 -04:00
}
}
2025-09-19 15:14:46 -04:00
return { validated_groups , invalidated_groups } ;
2025-07-10 09:36:05 -04:00
}
2025-10-07 15:16:03 -04:00
fourdst : : composition : : Composition MultiscalePartitioningEngineView : : solveQSEAbundances (
const fourdst : : composition : : Composition & comp ,
2025-07-10 09:36:05 -04:00
const double T9 ,
const double rho
) {
2025-07-16 12:14:02 -04:00
LOG_TRACE_L1 ( m_logger , " Solving for QSE abundances... " ) ;
2025-07-22 12:48:24 -04:00
// 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 ;
} ) ;
2025-10-07 15:16:03 -04:00
fourdst : : composition : : Composition outputComposition = comp ;
2025-07-22 12:48:24 -04:00
2025-10-07 15:16:03 -04:00
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 ( ) ) ) {
2025-07-16 12:14:02 -04:00
continue ;
}
2025-10-07 15:16:03 -04:00
fourdst : : composition : : Composition normalized_composition = comp ;
for ( const auto & species : algebraic_species ) {
if ( ! normalized_composition . contains ( species ) ) {
normalized_composition . registerSpecies ( species ) ;
normalized_composition . setMassFraction ( species , 0.0 ) ;
}
2025-07-16 12:14:02 -04:00
}
2025-10-07 15:16:03 -04:00
for ( const auto & species : seed_species ) {
if ( ! normalized_composition . contains ( species ) ) {
normalized_composition . registerSpecies ( species ) ;
normalized_composition . setMassFraction ( species , 0.0 ) ;
}
2025-07-10 09:36:05 -04:00
}
2025-10-10 09:12:40 -04:00
bool normCompFinalizedOkay = normalized_composition . finalize ( true ) ;
if ( ! normCompFinalizedOkay ) {
LOG_ERROR ( m_logger , " Failed to finalize composition before QSE solve. " ) ;
throw std : : runtime_error ( " Failed to finalize composition before QSE solve. " ) ;
}
2025-07-10 09:36:05 -04:00
2025-10-07 15:16:03 -04:00
Eigen : : VectorXd Y_scale ( algebraic_species . size ( ) ) ;
Eigen : : VectorXd v_initial ( algebraic_species . size ( ) ) ;
long i = 0 ;
std : : unordered_map < Species , size_t > species_to_index_map ;
for ( const auto & species : algebraic_species ) {
2025-07-10 09:36:05 -04:00
constexpr double abundance_floor = 1.0e-15 ;
2025-10-07 15:16:03 -04:00
const double initial_abundance = normalized_composition . getMolarAbundance ( species ) ;
2025-07-10 09:36:05 -04:00
Y_scale ( i ) = std : : max ( initial_abundance , abundance_floor ) ;
v_initial ( i ) = std : : asinh ( initial_abundance / Y_scale ( i ) ) ; // Scale the initial abundances using asinh
2025-10-07 15:16:03 -04:00
species_to_index_map . emplace ( species , i ) ;
i + + ;
2025-07-10 09:36:05 -04:00
}
2025-10-07 15:16:03 -04:00
EigenFunctor functor ( * this , algebraic_species , normalized_composition , T9 , rho , Y_scale , species_to_index_map ) ;
2025-07-10 09:36:05 -04:00
Eigen : : LevenbergMarquardt lm ( functor ) ;
lm . parameters . ftol = 1.0e-10 ;
lm . parameters . xtol = 1.0e-10 ;
2025-07-16 12:14:02 -04:00
LOG_TRACE_L1 ( m_logger , " Minimizing functor... " ) ;
2025-07-10 09:36:05 -04:00
Eigen : : LevenbergMarquardtSpace : : Status status = lm . minimize ( v_initial ) ;
if ( status < = 0 | | status > = 4 ) {
std : : stringstream msg ;
2025-10-12 07:52:30 -04:00
msg < < " While working on QSE group with algebraic species: " ;
int count = 0 ;
for ( const auto & species : algebraic_species ) {
msg < < species ;
if ( count < algebraic_species . size ( ) - 1 ) {
msg < < " , " ;
}
count + + ;
}
msg < < " the QSE solver failed to converge with status: " < < lm_status_map . at ( status ) ;
msg < < " . This likely indicates that the QSE groups were not properly partitioned " ;
msg < < " (for example if the algebraic set here contains species which should be dynamic like He-4 or H-1). " ;
LOG_ERROR ( m_logger , " {} " , msg . str ( ) ) ;
2025-07-10 09:36:05 -04:00
throw std : : runtime_error ( msg . str ( ) ) ;
}
2025-10-12 07:52:30 -04:00
LOG_TRACE_L1 ( m_logger , " QSE Group minimization succeeded with status: {} " , lm_status_map . at ( status ) ) ;
2025-07-10 09:36:05 -04:00
Eigen : : VectorXd Y_final_qse = Y_scale . array ( ) * v_initial . array ( ) . sinh ( ) ; // Convert back to physical abundances using asinh scaling
2025-10-07 15:16:03 -04:00
i = 0 ;
for ( const auto & species : algebraic_species ) {
2025-07-16 12:14:02 -04:00
LOG_TRACE_L1 (
m_logger ,
2025-10-07 15:16:03 -04:00
" During QSE solving species {} started with a molar abundance of {} and ended with an abundance of {}. " ,
species . name ( ) ,
normalized_composition . getMolarAbundance ( species ) ,
2025-07-16 12:14:02 -04:00
Y_final_qse ( i )
) ;
2025-10-08 11:17:35 -04:00
//TODO: Check this conversion
2025-10-07 15:16:03 -04:00
double Xi = Y_final_qse ( i ) * species . mass ( ) ; // Convert from molar abundance to mass fraction
2025-10-14 13:37:48 -04:00
if ( ! outputComposition . hasSpecies ( species ) ) {
2025-10-07 15:16:03 -04:00
outputComposition . registerSpecies ( species ) ;
}
outputComposition . setMassFraction ( species , Xi ) ;
i + + ;
2025-07-10 09:36:05 -04:00
}
}
2025-10-14 13:37:48 -04:00
bool didFinalize = outputComposition . finalize ( false ) ;
if ( ! didFinalize ) {
LOG_ERROR ( m_logger , " Failed to finalize composition after solving QSE abundances. " ) ;
m_logger - > flush_log ( ) ;
throw std : : runtime_error ( " Failed to finalize composition after solving QSE abundances. " ) ;
}
2025-10-07 15:16:03 -04:00
return outputComposition ;
2025-07-10 09:36:05 -04:00
}
2025-07-16 12:14:02 -04:00
size_t MultiscalePartitioningEngineView : : identifyMeanSlowestPool (
2025-10-07 15:16:03 -04:00
const std : : vector < std : : vector < Species > > & pools ,
const fourdst : : composition : : Composition & comp ,
2025-07-16 12:14:02 -04:00
const double T9 ,
const double rho
) const {
2025-10-07 15:16:03 -04:00
const auto & result = m_baseEngine . getSpeciesDestructionTimescales ( comp , T9 , rho ) ;
2025-07-22 12:48:24 -04:00
if ( ! result ) {
LOG_ERROR ( m_logger , " Failed to get species timescales due to stale engine state " ) ;
m_logger - > flush_log ( ) ;
throw exceptions : : StaleEngineError ( " Failed to get species timescales due to stale engine state " ) ;
}
const std : : unordered_map < Species , double > all_timescales = result . value ( ) ;
2025-07-16 12:14:02 -04:00
size_t slowest_pool_index = 0 ; // Default to the first pool if no valid pool is found
double slowest_mean_timescale = std : : numeric_limits < double > : : min ( ) ;
2025-07-24 10:20:44 -04:00
size_t count = 0 ;
2025-07-16 12:14:02 -04:00
for ( const auto & pool : pools ) {
double mean_timescale = 0.0 ;
2025-10-07 15:16:03 -04:00
for ( const auto & species : pool ) {
const double timescale = all_timescales . at ( species ) ;
2025-07-16 12:14:02 -04:00
mean_timescale + = timescale ;
}
2025-08-14 13:33:46 -04:00
mean_timescale = mean_timescale / static_cast < double > ( pool . size ( ) ) ;
2025-07-22 12:48:24 -04:00
if ( std : : isinf ( mean_timescale ) ) {
LOG_CRITICAL ( m_logger , " Encountered infinite mean timescale for pool {} with species: {} " ,
count , [ & ] ( ) - > std : : string {
std : : stringstream ss ;
2025-07-24 10:20:44 -04:00
size_t iCount = 0 ;
2025-10-07 15:16:03 -04:00
for ( const auto & species : pool ) {
ss < < species . name ( ) < < " : " < < all_timescales . at ( species ) ;
2025-07-22 12:48:24 -04:00
if ( iCount < pool . size ( ) - 1 ) {
ss < < " , " ;
}
iCount + + ;
}
return ss . str ( ) ;
} ( )
) ;
m_logger - > flush_log ( ) ;
throw std : : logic_error ( " Encountered infinite mean destruction timescale for a pool while identifying the mean slowest pool set, indicating a potential issue with species timescales. Check log file for more details on specific pool composition... " ) ;
}
2025-07-16 12:14:02 -04:00
if ( mean_timescale > slowest_mean_timescale ) {
slowest_mean_timescale = mean_timescale ;
slowest_pool_index = & pool - & pools [ 0 ] ; // Get the index of the pool
}
}
return slowest_pool_index ;
}
2025-10-07 15:16:03 -04:00
std : : unordered_map < Species , std : : vector < Species > > MultiscalePartitioningEngineView : : buildConnectivityGraph (
const std : : vector < Species > & species_pool
2025-07-22 12:48:24 -04:00
) const {
2025-10-07 15:16:03 -04:00
std : : unordered_map < Species , std : : vector < Species > > connectivity_graph ;
const std : : set < Species > pool_set ( species_pool . begin ( ) , species_pool . end ( ) ) ;
2025-07-22 12:48:24 -04:00
const std : : unordered_set < Species > pool_species = [ & ] ( ) - > std : : unordered_set < Species > {
std : : unordered_set < Species > result ;
2025-10-07 15:16:03 -04:00
for ( const auto & species : species_pool ) {
2025-07-22 12:48:24 -04:00
result . insert ( species ) ;
}
return result ;
} ( ) ;
2025-08-14 13:33:46 -04:00
std : : map < size_t , std : : vector < reaction : : LogicalReaclibReaction * > > speciesReactionMap ;
std : : vector < const reaction : : LogicalReaclibReaction * > candidate_reactions ;
2025-07-22 12:48:24 -04:00
for ( const auto & reaction : m_baseEngine . getNetworkReactions ( ) ) {
2025-08-14 13:33:46 -04:00
const std : : vector < Species > & reactants = reaction - > reactants ( ) ;
const std : : vector < Species > & products = reaction - > products ( ) ;
2025-07-22 12:48:24 -04:00
std : : unordered_set < Species > reactant_set ( reactants . begin ( ) , reactants . end ( ) ) ;
std : : unordered_set < Species > product_set ( products . begin ( ) , products . end ( ) ) ;
// Only consider reactions where at least one distinct reactant and product are in the pool
if ( has_distinct_reactant_and_product_species ( pool_species , reactant_set , product_set ) ) {
2025-10-07 15:16:03 -04:00
std : : set < Species > involvedSet ;
involvedSet . insert ( reactants . begin ( ) , reactants . end ( ) ) ;
involvedSet . insert ( products . begin ( ) , products . end ( ) ) ;
2025-07-22 12:48:24 -04:00
2025-10-07 15:16:03 -04:00
std : : vector < Species > intersection ;
2025-07-22 12:48:24 -04:00
intersection . reserve ( involvedSet . size ( ) ) ;
2025-10-07 15:16:03 -04:00
for ( const auto & s : pool_species ) { // Find intersection with pool species
if ( involvedSet . contains ( s ) ) {
intersection . push_back ( s ) ;
}
}
2025-07-22 12:48:24 -04:00
// Add clique
2025-10-07 15:16:03 -04:00
for ( const auto & u : intersection ) {
for ( const auto & v : intersection ) {
2025-07-22 12:48:24 -04:00
if ( u ! = v ) { // Avoid self-loops
connectivity_graph [ u ] . push_back ( v ) ;
}
}
}
}
}
return connectivity_graph ;
}
2025-07-16 12:14:02 -04:00
std : : vector < MultiscalePartitioningEngineView : : QSEGroup > MultiscalePartitioningEngineView : : constructCandidateGroups (
2025-10-07 15:16:03 -04:00
const std : : vector < std : : vector < Species > > & candidate_pools ,
const fourdst : : composition : : Composition & comp ,
const double T9 ,
const double rho
2025-07-16 12:14:02 -04:00
) const {
const auto & all_reactions = m_baseEngine . getNetworkReactions ( ) ;
2025-10-07 15:16:03 -04:00
const auto & result = m_baseEngine . getSpeciesDestructionTimescales ( comp , T9 , rho ) ;
2025-07-22 12:48:24 -04:00
if ( ! result ) {
LOG_ERROR ( m_logger , " Failed to get species timescales due to stale engine state " ) ;
m_logger - > flush_log ( ) ;
throw exceptions : : StaleEngineError ( " Failed to get species timescales due to stale engine state " ) ;
}
const std : : unordered_map < Species , double > destruction_timescales = result . value ( ) ;
2025-07-16 12:14:02 -04:00
std : : vector < QSEGroup > candidate_groups ;
2025-07-22 12:48:24 -04:00
for ( const auto & pool : candidate_pools ) {
2025-07-16 12:14:02 -04:00
if ( pool . empty ( ) ) continue ; // Skip empty pools
2025-07-22 12:48:24 -04:00
2025-07-16 12:14:02 -04:00
// For each pool first identify all topological bridge connections
2025-08-14 13:33:46 -04:00
std : : vector < std : : pair < const reaction : : Reaction * , double > > bridge_reactions ;
2025-10-07 15:16:03 -04:00
for ( const auto & ash : pool ) {
2025-07-16 12:14:02 -04:00
for ( const auto & reaction : all_reactions ) {
2025-08-14 13:33:46 -04:00
if ( reaction - > contains ( ash ) ) {
2025-07-16 12:14:02 -04:00
// Check to make sure there is at least one reactant that is not in the pool
// This lets seed nuclei bring mass into the QSE group.
bool has_external_reactant = false ;
2025-08-14 13:33:46 -04:00
for ( const auto & reactant : reaction - > reactants ( ) ) {
2025-10-07 15:16:03 -04:00
if ( std : : ranges : : find ( pool , reactant ) = = pool . end ( ) ) {
2025-07-16 12:14:02 -04:00
has_external_reactant = true ;
2025-08-15 12:11:32 -04:00
LOG_TRACE_L3 ( m_logger , " Found external reactant {} in reaction {} for species {}. " , reactant . name ( ) , reaction - > id ( ) , ash . name ( ) ) ;
2025-07-16 12:14:02 -04:00
break ; // Found an external reactant, no need to check further
}
}
if ( has_external_reactant ) {
2025-10-07 15:16:03 -04:00
double flow = std : : abs ( m_baseEngine . calculateMolarReactionFlow ( * reaction , comp , T9 , rho ) ) ;
2025-08-15 12:11:32 -04:00
LOG_TRACE_L3 ( m_logger , " Found bridge reaction {} with flow {} for species {}. " , reaction - > id ( ) , flow , ash . name ( ) ) ;
2025-08-14 13:33:46 -04:00
bridge_reactions . emplace_back ( reaction . get ( ) , flow ) ;
2025-07-16 12:14:02 -04:00
}
}
}
}
std : : ranges : : sort (
bridge_reactions ,
[ ] ( const auto & a , const auto & b ) {
return a . second > b . second ; // Sort by flow in descending order
} ) ;
constexpr double MIN_GAP_THRESHOLD = 1 ; // Minimum logarithmic molar flow gap threshold for bridge reactions
std : : vector < size_t > split_points ;
for ( size_t i = 0 ; i < bridge_reactions . size ( ) - 1 ; + + i ) {
const double f1 = bridge_reactions [ i ] . second ;
const double f2 = bridge_reactions [ i + 1 ] . second ;
if ( std : : log10 ( f1 ) - std : : log10 ( f2 ) > MIN_GAP_THRESHOLD ) {
2025-07-22 12:48:24 -04:00
LOG_TRACE_L3 ( m_logger , " Detected gap between bridge reactions with flows {} and {}. " , f1 , f2 ) ;
2025-07-16 12:14:02 -04:00
split_points . push_back ( i + 1 ) ;
}
}
if ( split_points . empty ( ) ) { // If no split points were found, we consider the whole set of bridge reactions as one group.
split_points . push_back ( bridge_reactions . size ( ) - 1 ) ;
}
2025-10-07 15:16:03 -04:00
std : : vector < Species > seed_species ;
2025-08-14 13:33:46 -04:00
for ( auto & reaction : bridge_reactions | std : : views : : keys ) {
for ( const auto & fuel : reaction - > reactants ( ) ) {
2025-07-16 12:14:02 -04:00
// Only add the fuel if it is not already in the pool
2025-10-07 15:16:03 -04:00
if ( std : : ranges : : find ( pool , fuel ) = = pool . end ( ) ) {
seed_species . push_back ( fuel ) ;
2025-07-16 12:14:02 -04:00
}
}
}
2025-10-07 15:16:03 -04:00
std : : set < Species > pool_species ( pool . begin ( ) , pool . end ( ) ) ;
for ( const auto & species : seed_species ) {
pool_species . insert ( species ) ;
2025-07-16 12:14:02 -04:00
}
2025-10-07 15:16:03 -04:00
const std : : set < Species > poolSet ( pool . begin ( ) , pool . end ( ) ) ;
const std : : set < Species > seedSet ( seed_species . begin ( ) , seed_species . end ( ) ) ;
2025-07-22 12:48:24 -04:00
double mean_timescale = 0.0 ;
2025-10-07 15:16:03 -04:00
for ( const auto & species : poolSet ) {
2025-07-22 12:48:24 -04:00
if ( destruction_timescales . contains ( species ) ) {
mean_timescale + = std : : min ( destruction_timescales . at ( species ) , species . halfLife ( ) ) ; // Use the minimum of destruction timescale and half-life
} else {
mean_timescale + = species . halfLife ( ) ;
}
}
2025-08-14 13:33:46 -04:00
mean_timescale / = static_cast < double > ( poolSet . size ( ) ) ;
2025-10-07 15:16:03 -04:00
QSEGroup qse_group ( false , poolSet , seedSet , mean_timescale ) ;
2025-07-16 12:14:02 -04:00
candidate_groups . push_back ( qse_group ) ;
}
return candidate_groups ;
}
2025-07-18 15:23:43 -04:00
2025-07-10 09:36:05 -04:00
int MultiscalePartitioningEngineView : : EigenFunctor : : operator ( ) ( const InputType & v_qse , OutputType & f_qse ) const {
2025-10-07 15:16:03 -04:00
fourdst : : composition : : Composition comp_trial = m_initial_comp ;
2025-07-10 09:36:05 -04:00
Eigen : : VectorXd y_qse = m_Y_scale . array ( ) * v_qse . array ( ) . sinh ( ) ; // Convert to physical abundances using asinh scaling
2025-10-07 15:16:03 -04:00
for ( const auto & species : m_qse_solve_species ) {
2025-10-10 09:12:40 -04:00
if ( ! comp_trial . hasSymbol ( std : : string ( species . name ( ) ) ) ) {
2025-10-07 15:16:03 -04:00
comp_trial . registerSpecies ( species ) ;
}
2025-10-10 09:12:40 -04:00
const double molarAbundance = y_qse [ static_cast < long > ( m_qse_solve_species_index_map . at ( species ) ) ] ;
2025-10-14 13:37:48 -04:00
double massFraction = molarAbundance * species . mass ( ) ;
if ( massFraction < 0 & & std : : abs ( massFraction ) < 1e-20 ) { // if there is a larger negative mass fraction, let the composition module throw an error
massFraction = 0.0 ; // Avoid setting minuscule negative mass fractions due to numerical noise
}
2025-10-10 09:12:40 -04:00
comp_trial . setMassFraction ( species , massFraction ) ;
}
const bool didFinalize = comp_trial . finalize ( false ) ;
if ( ! didFinalize ) {
std : : string msg = std : : format ( " Failed to finalize composition (comp_trial) in {} at line {} " , __FILE__ , __LINE__ ) ;
throw std : : runtime_error ( msg ) ;
2025-07-10 09:36:05 -04:00
}
2025-10-07 15:16:03 -04:00
const auto result = m_view - > getBaseEngine ( ) . calculateRHSAndEnergy ( comp_trial , m_T9 , m_rho ) ;
2025-07-22 12:48:24 -04:00
if ( ! result ) {
throw exceptions : : StaleEngineError ( " Failed to calculate RHS and energy due to stale engine state " ) ;
}
const auto & [ dydt , nuclearEnergyGenerationRate ] = result . value ( ) ;
2025-10-07 15:16:03 -04:00
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
for ( const auto & species : m_qse_solve_species ) {
f_qse ( i ) = dydt . at ( species ) ;
i + + ;
2025-07-10 09:36:05 -04:00
}
2025-07-14 14:50:49 -04:00
2025-07-10 09:36:05 -04:00
return 0 ; // Success
}
int MultiscalePartitioningEngineView : : EigenFunctor : : df ( const InputType & v_qse , JacobianType & J_qse ) const {
2025-10-07 15:16:03 -04:00
fourdst : : composition : : Composition comp_trial = m_initial_comp ;
2025-07-10 09:36:05 -04:00
Eigen : : VectorXd y_qse = m_Y_scale . array ( ) * v_qse . array ( ) . sinh ( ) ; // Convert to physical abundances using asinh scaling
2025-10-07 15:16:03 -04:00
for ( const auto & species : m_qse_solve_species ) {
2025-10-10 09:12:40 -04:00
if ( ! comp_trial . hasSymbol ( std : : string ( species . name ( ) ) ) ) {
2025-10-07 15:16:03 -04:00
comp_trial . registerSpecies ( species ) ;
}
2025-10-10 09:12:40 -04:00
const double molarAbundance = y_qse [ static_cast < long > ( m_qse_solve_species_index_map . at ( species ) ) ] ;
const double massFraction = molarAbundance * species . mass ( ) ;
comp_trial . setMassFraction ( species , massFraction ) ;
}
const bool didFinalize = comp_trial . finalize ( false ) ;
if ( ! didFinalize ) {
2025-10-14 13:37:48 -04:00
const std : : string msg = std : : format ( " Failed to finalize composition (comp_trial) in {} at line {} " , __FILE__ , __LINE__ ) ;
2025-10-10 09:12:40 -04:00
throw std : : runtime_error ( msg ) ;
2025-07-10 09:36:05 -04:00
}
2025-10-07 15:16:03 -04:00
m_view - > getBaseEngine ( ) . generateJacobianMatrix ( comp_trial , m_T9 , m_rho ) ;
2025-07-10 09:36:05 -04:00
2025-10-07 15:16:03 -04:00
const long N = static_cast < long > ( m_qse_solve_species . size ( ) ) ;
J_qse . resize ( N , N ) ;
long rowID = 0 ;
for ( const auto & rowSpecies : m_qse_solve_species ) {
2025-10-10 09:12:40 -04:00
long colID = 0 ;
2025-10-07 15:16:03 -04:00
for ( const auto & colSpecies : m_qse_solve_species ) {
2025-10-10 09:12:40 -04:00
J_qse ( rowID , colID ) = m_view - > getBaseEngine ( ) . getJacobianMatrixEntry (
2025-10-07 15:16:03 -04:00
rowSpecies ,
colSpecies
2025-07-10 09:36:05 -04:00
) ;
2025-10-10 09:12:40 -04:00
colID + = 1 ;
2025-10-14 13:37:48 -04:00
LOG_TRACE_L3 ( m_view - > m_logger , " Jacobian[{}, {}] (d(dY({}))/dY({})) = {} " , rowID , colID - 1 , rowSpecies . name ( ) , colSpecies . name ( ) , J_qse ( rowID , colID - 1 ) ) ;
2025-07-10 09:36:05 -04:00
}
2025-10-10 09:12:40 -04:00
rowID + = 1 ;
2025-07-10 09:36:05 -04:00
}
// Chain rule for asinh scaling:
for ( long j = 0 ; j < J_qse . cols ( ) ; + + j ) {
const double dY_dv = m_Y_scale ( j ) * std : : cosh ( v_qse ( j ) ) ;
J_qse . col ( j ) * = dY_dv ; // Scale the column by the derivative of the asinh scaling
}
return 0 ; // Success
}
2025-07-18 15:23:43 -04:00
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 ;
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 ) ) ;
2025-09-19 15:14:46 -04:00
double negThresh = 1e-10 ; // Threshold for considering a value as negative.
2025-07-18 15:23:43 -04:00
for ( double Yi : m_Y ) {
2025-09-19 15:14:46 -04:00
if ( Yi < 0.0 & & std : : abs ( Yi ) < negThresh ) {
2025-07-18 15:23:43 -04:00
Yi = 0.0 ; // Avoid negative abundances
2025-09-19 15:14:46 -04:00
} else if ( Yi < 0.0 & & std : : abs ( Yi ) > = negThresh ) {
2025-07-18 15:23:43 -04:00
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 ;
}
2025-07-22 12:48:24 -04:00
bool MultiscalePartitioningEngineView : : QSEGroup : : operator = = ( const QSEGroup & other ) const {
return mean_timescale = = other . mean_timescale ;
}
bool MultiscalePartitioningEngineView : : QSEGroup : : operator < ( const QSEGroup & other ) const {
return mean_timescale < other . mean_timescale ;
}
bool MultiscalePartitioningEngineView : : QSEGroup : : operator > ( const QSEGroup & other ) const {
return mean_timescale > other . mean_timescale ;
}
bool MultiscalePartitioningEngineView : : QSEGroup : : operator ! = ( const QSEGroup & other ) const {
return ! ( * this = = other ) ;
}
2025-09-19 15:14:46 -04:00
std : : string MultiscalePartitioningEngineView : : QSEGroup : : toString ( ) const {
std : : stringstream ss ;
ss < < " QSEGroup(Algebraic: [ " ;
size_t count = 0 ;
2025-10-07 15:16:03 -04:00
for ( const auto & species : algebraic_species ) {
ss < < species . name ( ) ;
if ( count < algebraic_species . size ( ) - 1 ) {
2025-09-19 15:14:46 -04:00
ss < < " , " ;
}
count + + ;
}
ss < < " ], Seed: [ " ;
count = 0 ;
2025-10-07 15:16:03 -04:00
for ( const auto & species : seed_species ) {
ss < < species . name ( ) ;
if ( count < seed_species . size ( ) - 1 ) {
2025-09-19 15:14:46 -04:00
ss < < " , " ;
}
count + + ;
}
ss < < " ], Mean Timescale: " < < mean_timescale < < " , Is In Equilibrium: " < < ( is_in_equilibrium ? " True " : " False " ) < < " ) " ;
return ss . str ( ) ;
}
2025-07-24 08:37:52 -04:00
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 ) ;
}
2025-07-10 09:36:05 -04:00
}