6#include <unordered_map>
7#include <unordered_set>
14#include "quill/LogMacros.h"
15#include "quill/Logger.h"
18 using namespace fourdst::atomic;
19 std::vector<double> packCompositionToVector(
const fourdst::composition::Composition& composition,
const gridfire::GraphEngine& engine) {
22 for (
size_t i = 0; i < allSpecies.size(); ++i) {
23 const auto& species = allSpecies[i];
24 if (!composition.contains(species)) {
27 Y[i] = composition.getMolarAbundance(species);
34 void hash_combine(std::size_t& seed,
const T& v) {
36 seed ^= hashed(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
39 std::vector<std::vector<size_t>> findConnectedComponentsBFS(
40 const std::unordered_map<
size_t, std::vector<size_t>>& graph,
41 const std::vector<size_t>& nodes
43 std::vector<std::vector<size_t>> components;
44 std::unordered_set<size_t> visited;
46 for (
const size_t& start_node : nodes) {
47 if (visited.find(start_node) == visited.end()) {
48 std::vector<size_t> current_component;
52 visited.insert(start_node);
57 current_component.push_back(u);
60 for (
const auto& v : graph.at(u)) {
61 if (visited.find(v) == visited.end()) {
68 components.push_back(current_component);
74 struct SpeciesSetIntersection {
75 const Species species;
79 std::expected<SpeciesSetIntersection, std::string> get_intersection_info (
80 const std::unordered_set<Species>& setA,
81 const std::unordered_set<Species>& setB
84 auto* outerSet = &setA;
85 auto* innerSet = &setB;
86 if (setA.size() > setB.size()) {
91 std::size_t matchCount = 0;
92 const Species* firstMatch =
nullptr;
94 for (
const Species& sp : *outerSet) {
95 if (innerSet->contains(sp)) {
96 if (matchCount == 0) {
100 if (matchCount > 1) {
107 return std::unexpected{
"Intersection is empty"};
109 if (matchCount == 0) {
111 return std::unexpected{
"No intersection found"};
115 return SpeciesSetIntersection{*firstMatch, matchCount};
119 bool has_distinct_reactant_and_product_species (
120 const std::unordered_set<Species>& poolSpecies,
121 const std::unordered_set<Species>& reactants,
122 const std::unordered_set<Species>& products
124 const auto reactant_result = get_intersection_info(poolSpecies, reactants);
125 if (!reactant_result) {
128 const auto [reactantSample, reactantCount] = reactant_result.value();
130 const auto product_result = get_intersection_info(poolSpecies, products);
131 if (!product_result) {
135 const auto [productSample, productCount] = product_result.value();
139 if (reactantCount > 1 || productCount > 1) {
144 return reactantSample != productSample;
151 using fourdst::atomic::Species;
162 const std::vector<double> &Y_full,
169 "Y_full size ({}) does not match the number of species in the network ({})",
173 throw std::runtime_error(
"Y_full size does not match the number of species in the network. See logs for more details...");
182 "QSE abundance cache miss for T9 = {}, rho = {} (misses: {}, hits: {}). calculateRHSAndEnergy does not receive sufficient context to partition and stabilize the network. Throwing an error which should be caught by the caller and trigger a re-partition stage.",
191 const auto result =
m_baseEngine.calculateRHSAndEnergy(Y_full, T9, rho);
193 return std::unexpected{result.error()};
195 auto deriv = result.value();
199 deriv.dydt[species_index] = 0.0;
205 const std::vector<double> &Y_full,
214 "QSE abundance cache miss for T9 = {}, rho = {} (misses: {}, hits: {}). generateJacobianMatrix does not receive sufficient context to partition and stabilize the network. Throwing an error which should be caught by the caller and trigger a re-partition stage.",
220 throw exceptions::StaleEngineError(
"QSE Cache Miss while lacking context for partitioning. This should be caught by the caller and trigger a re-partition stage.");
239 return m_baseEngine.getJacobianMatrixEntry(i_full, j_full);
247 const int speciesIndex,
248 const int reactionIndex
250 return m_baseEngine.getStoichiometryMatrixEntry(speciesIndex, reactionIndex);
255 const std::vector<double> &Y_full,
264 "QSE abundance cache miss for T9 = {}, rho = {} (misses: {}, hits: {}). calculateMolarReactionFlow does not receive sufficient context to partition and stabilize the network. Throwing an error which should be caught by the caller and trigger a re-partition stage.",
270 throw exceptions::StaleEngineError(
"QSE Cache Miss while lacking context for partitioning. This should be caught by the caller and trigger a re-partition stage.");
278 std::vector<double> Y_mutable = Y_full;
280 Y_mutable[index] = Yi;
291 LOG_CRITICAL(
m_logger,
"setNetworkReactions is not supported in MultiscalePartitioningEngineView. Did you mean to call this on the base engine?");
296 const std::vector<double> &Y,
305 "QSE abundance cache miss for T9 = {}, rho = {} (misses: {}, hits: {}). getSpeciesTimescales does not receive sufficient context to partition and stabilize the network. Throwing an error which should be caught by the caller and trigger a re-partition stage.",
311 throw exceptions::StaleEngineError(
"QSE Cache Miss while lacking context for partitioning. This should be caught by the caller and trigger a re-partition stage.");
314 const auto result =
m_baseEngine.getSpeciesTimescales(Y, T9, rho);
316 return std::unexpected{result.error()};
318 std::unordered_map<Species, double> speciesTimescales = result.value();
320 speciesTimescales[algebraicSpecies] = std::numeric_limits<double>::infinity();
322 return speciesTimescales;
327 const std::vector<double> &Y,
336 "QSE abundance cache miss for T9 = {}, rho = {} (misses: {}, hits: {}). getSpeciesDestructionTimescales does not receive sufficient context to partition and stabilize the network. Throwing an error which should be caught by the caller and trigger a re-partition stage.",
342 throw exceptions::StaleEngineError(
"QSE Cache Miss while lacking context for partitioning. This should be caught by the caller and trigger a re-partition stage.");
345 const auto result =
m_baseEngine.getSpeciesDestructionTimescales(Y, T9, rho);
347 return std::unexpected{result.error()};
349 std::unordered_map<Species, double> speciesDestructionTimescales = result.value();
351 speciesDestructionTimescales[algebraicSpecies] = std::numeric_limits<double>::infinity();
353 return speciesDestructionTimescales;
357 const fourdst::composition::Composition baseUpdatedComposition =
m_baseEngine.update(netIn);
363 packCompositionToVector(baseUpdatedComposition,
m_baseEngine)
366 return baseUpdatedComposition;
368 NetIn baseUpdatedNetIn = netIn;
369 baseUpdatedNetIn.
composition = baseUpdatedComposition;
370 const fourdst::composition::Composition equilibratedComposition =
equilibrateNetwork(baseUpdatedNetIn);
374 Y_algebraic[i] = equilibratedComposition.getMolarAbundance(
m_baseEngine.getNetworkSpecies()[species_index]);
383 packCompositionToVector(equilibratedComposition,
m_baseEngine)
387 return equilibratedComposition;
417 const std::vector<std::vector<size_t>> ×cale_pools,
418 const std::vector<double> &Y,
422 std::vector<std::vector<size_t>> final_connected_pools;
424 for (
const auto& pool : timescale_pools) {
431 auto components = findConnectedComponentsBFS(connectivity_graph, pool);
432 final_connected_pools.insert(final_connected_pools.end(), components.begin(), components.end());
434 return final_connected_pools;
439 const std::vector<double> &Y,
444 LOG_TRACE_L1(
m_logger,
"Partitioning network...");
445 LOG_TRACE_L1(
m_logger,
"Clearing previous state...");
453 LOG_TRACE_L1(
m_logger,
"Identifying fast reactions...");
455 LOG_TRACE_L1(
m_logger,
"Found {} timescale pools.", timescale_pools.size());
458 LOG_TRACE_L1(
m_logger,
"Identifying mean slowest pool...");
460 LOG_TRACE_L1(
m_logger,
"Mean slowest pool index: {}", mean_slowest_pool_index);
469 std::vector<std::vector<size_t>> candidate_pools;
470 for (
size_t i = 0; i < timescale_pools.size(); ++i) {
471 if (i == mean_slowest_pool_index)
continue;
472 LOG_TRACE_L1(
m_logger,
"Group {} with {} species identified for potential QSE.", i, timescale_pools[i].size());
473 candidate_pools.push_back(timescale_pools[i]);
476 LOG_TRACE_L1(
m_logger,
"Preforming connectivity analysis on timescale pools...");
478 LOG_TRACE_L1(
m_logger,
"Found {} connected pools (compared to {} timescale pools) for QSE analysis.", connected_pools.size(), timescale_pools.size());
482 LOG_TRACE_L1(
m_logger,
"Identifying potential seed species for candidate pools...");
484 LOG_TRACE_L1(
m_logger,
"Found {} candidate QSE groups for further analysis", candidate_groups.size());
486 LOG_TRACE_L1(
m_logger,
"Validating candidate groups with flux analysis...");
490 "Validated {} group(s) QSE groups. {}",
491 validated_groups.size(),
492 [&]() -> std::string {
493 std::stringstream ss;
495 for (const auto& group : validated_groups) {
496 ss <<
"Group " << count + 1;
497 if (group.is_in_equilibrium) {
498 ss <<
" is in equilibrium";
500 ss <<
" is not in equilibrium";
502 if (count < validated_groups.size() - 1) {
511 m_qse_groups = std::move(validated_groups);
512 LOG_TRACE_L1(m_logger,
"Identified {} QSE groups.", m_qse_groups.size());
514 for (
const auto& group : m_qse_groups) {
516 for (
const auto& index : group.algebraic_indices) {
517 if (std::ranges::find(m_algebraic_species_indices, index) == m_algebraic_species_indices.end()) {
518 m_algebraic_species.push_back(m_baseEngine.getNetworkSpecies()[index]);
519 m_algebraic_species_indices.push_back(index);
527 "Partitioning complete. Found {} dynamic species, {} algebraic (QSE) species ({}) spread over {} QSE group{}.",
528 m_dynamic_species.size(),
529 m_algebraic_species.size(),
530 [&]() -> std::string {
531 std::stringstream ss;
533 for (const auto& species : m_algebraic_species) {
534 ss << species.name();
535 if (m_algebraic_species.size() > 1 && count < m_algebraic_species.size() - 1) {
543 m_qse_groups.size() == 1 ?
"" :
"s"
553 const double rho = netIn.
density;
559 const std::string &filename,
560 const std::vector<double>& Y,
564 std::ofstream dotFile(filename);
565 if (!dotFile.is_open()) {
566 LOG_ERROR(
m_logger,
"Failed to open file for writing: {}", filename);
567 throw std::runtime_error(
"Failed to open file for writing: " + filename);
570 const auto& all_species =
m_baseEngine.getNetworkSpecies();
571 const auto& all_reactions =
m_baseEngine.getNetworkReactions();
576 std::unordered_set<size_t> algebraic_indices;
577 std::unordered_set<size_t> seed_indices;
579 if (group.is_in_equilibrium) {
580 algebraic_indices.insert(group.algebraic_indices.begin(), group.algebraic_indices.end());
581 seed_indices.insert(group.seed_indices.begin(), group.seed_indices.end());
586 std::vector<double> reaction_flows;
587 reaction_flows.reserve(all_reactions.size());
588 double min_log_flow = std::numeric_limits<double>::max();
589 double max_log_flow = std::numeric_limits<double>::lowest();
591 for (
const auto&
reaction : all_reactions) {
593 reaction_flows.push_back(flow);
595 double log_flow = std::log10(flow);
596 min_log_flow = std::min(min_log_flow, log_flow);
597 max_log_flow = std::max(max_log_flow, log_flow);
600 const double log_flow_range = (max_log_flow > min_log_flow) ? (max_log_flow - min_log_flow) : 1.0;
604 dotFile <<
"digraph PartitionedNetwork {\n";
605 dotFile <<
" graph [rankdir=TB, splines=true, overlap=false, bgcolor=\"#f8fafc\", label=\"Multiscale Partitioned Network View\", fontname=\"Helvetica\", fontsize=16, labeljust=l];\n";
606 dotFile <<
" node [shape=circle, style=filled, fontname=\"Helvetica\", width=0.8, fixedsize=true];\n";
607 dotFile <<
" edge [fontname=\"Helvetica\", fontsize=10];\n\n";
611 dotFile <<
" // --- Species Nodes Definitions ---\n";
612 std::map<int, std::vector<std::string>> species_by_mass;
613 for (
size_t i = 0; i < all_species.size(); ++i) {
614 const auto& species = all_species[i];
615 std::string fillcolor =
"#f1f5f9";
619 if (algebraic_indices.contains(i)) {
620 fillcolor =
"#e0f2fe";
621 }
else if (seed_indices.contains(i)) {
622 fillcolor =
"#a7f3d0";
624 fillcolor =
"#dcfce7";
626 dotFile <<
" \"" << species.name() <<
"\" [label=\"" << species.name() <<
"\", fillcolor=\"" << fillcolor <<
"\"];\n";
630 species_by_mass[species.a()].push_back(std::string(species.name()));
636 dotFile <<
" // --- Layout using Ranks ---\n";
637 for (
const auto &species_list: species_by_mass | std::views::values) {
638 dotFile <<
" { rank=same; ";
639 for (
const auto& name : species_list) {
640 dotFile <<
"\"" << name <<
"\"; ";
647 dotFile <<
" // --- Chain by Mass ---\n";
648 for (
const auto& [mass, species_list] : species_by_mass) {
650 int minLargestMass = std::numeric_limits<int>::max();
651 for (
const auto &next_mass: species_by_mass | std::views::keys) {
652 if (next_mass > mass && next_mass < minLargestMass) {
653 minLargestMass = next_mass;
656 if (minLargestMass != std::numeric_limits<int>::max()) {
658 dotFile <<
" \"" << species_list[0] <<
"\" -> \"" << species_by_mass[minLargestMass][0] <<
"\" [style=invis];\n";
664 dotFile <<
" // --- QSE Group Clusters ---\n";
665 int group_counter = 0;
667 if (!group.is_in_equilibrium || group.algebraic_indices.empty()) {
670 dotFile <<
" subgraph cluster_qse_" << group_counter++ <<
" {\n";
671 dotFile <<
" label = \"QSE Group " << group_counter <<
"\";\n";
672 dotFile <<
" style = \"filled,rounded\";\n";
673 dotFile <<
" color = \"#38bdf8\";\n";
674 dotFile <<
" penwidth = 2.0;\n";
675 dotFile <<
" bgcolor = \"#f0f9ff80\";\n";
676 dotFile <<
" subgraph cluster_seed_" << group_counter <<
" {\n";
677 dotFile <<
" label = \"Seed Species\";\n";
678 dotFile <<
" style = \"filled,rounded\";\n";
679 dotFile <<
" color = \"#a7f3d0\";\n";
680 dotFile <<
" penwidth = 1.5;\n";
681 std::vector<std::string> seed_node_ids;
682 seed_node_ids.reserve(group.seed_indices.size());
683 for (
const size_t species_idx : group.seed_indices) {
684 std::stringstream ss;
685 ss <<
"node_" << group_counter <<
"_seed_" << species_idx;
686 dotFile <<
" " << ss.str() <<
" [label=\"" << all_species[species_idx].name() <<
"\"];\n";
687 seed_node_ids.push_back(ss.str());
689 for (
size_t i = 0; i < seed_node_ids.size() - 1; ++i) {
690 dotFile <<
" " << seed_node_ids[i] <<
" -> " << seed_node_ids[i + 1] <<
" [style=invis];\n";
693 dotFile <<
" subgraph cluster_algebraic_" << group_counter <<
" {\n";
694 dotFile <<
" label = \"Algebraic Species\";\n";
695 dotFile <<
" style = \"filled,rounded\";\n";
696 dotFile <<
" color = \"#e0f2fe\";\n";
697 dotFile <<
" penwidth = 1.5;\n";
698 std::vector<std::string> algebraic_node_ids;
699 algebraic_node_ids.reserve(group.algebraic_indices.size());
700 for (
const size_t species_idx : group.algebraic_indices) {
701 std::stringstream ss;
702 ss <<
"node_" << group_counter <<
"_algebraic_" << species_idx;
703 dotFile <<
" " << ss.str() <<
" [label=\"" << all_species[species_idx].name() <<
"\"];\n";
704 algebraic_node_ids.push_back(ss.str());
707 for (
size_t i = 0; i < algebraic_node_ids.size() - 1; ++i) {
708 dotFile <<
" " << algebraic_node_ids[i] <<
" -> " << algebraic_node_ids[i + 1] <<
" [style=invis];\n";
718 dotFile <<
" // --- Legend ---\n";
719 dotFile <<
" subgraph cluster_legend {\n";
720 dotFile <<
" rank = sink";
721 dotFile <<
" label = \"Legend\";\n";
722 dotFile <<
" bgcolor = \"#ffffff\";\n";
723 dotFile <<
" color = \"#e2e8f0\";\n";
724 dotFile <<
" node [shape=box, style=filled, fontname=\"Helvetica\"];\n";
725 dotFile <<
" key_core [label=\"Core Dynamic\", fillcolor=\"#dcfce7\"];\n";
726 dotFile <<
" key_seed [label=\"Seed (Dynamic)\", fillcolor=\"#a7f3d0\"];\n";
727 dotFile <<
" key_qse [label=\"Algebraic (QSE)\", fillcolor=\"#e0f2fe\"];\n";
728 dotFile <<
" key_other [label=\"Other\", fillcolor=\"#f1f5f9\"];\n";
729 dotFile <<
" key_info [label=\"Edge Opacity ~ log(Reaction Flow)\", shape=plaintext];\n";
731 dotFile <<
" key_core -> key_seed -> key_qse -> key_other -> key_info [style=invis];\n";
736 dotFile <<
" // --- Reaction Edges ---\n";
737 for (
size_t i = 0; i < all_reactions.size(); ++i) {
738 const auto&
reaction = all_reactions[i];
739 const double flow = reaction_flows[i];
741 if (flow < 1e-99)
continue;
743 double log_flow_val = std::log10(flow);
744 double norm_alpha = (log_flow_val - min_log_flow) / log_flow_range;
745 int alpha_val = 0x30 +
static_cast<int>(norm_alpha * (0xFF - 0x30));
746 alpha_val = std::clamp(alpha_val, 0x00, 0xFF);
748 std::stringstream alpha_hex;
749 alpha_hex << std::setw(2) << std::setfill(
'0') << std::hex << alpha_val;
750 std::string edge_color =
"#475569" + alpha_hex.str();
752 std::string reactionNodeId =
"reaction_" + std::string(
reaction.id());
753 dotFile <<
" \"" << reactionNodeId <<
"\" [shape=point, fillcolor=black, width=0.05, height=0.05];\n";
754 for (
const auto& reactant :
reaction.reactants()) {
755 dotFile <<
" \"" << reactant.name() <<
"\" -> \"" << reactionNodeId <<
"\" [color=\"" << edge_color <<
"\", arrowhead=none];\n";
757 for (
const auto& product :
reaction.products()) {
758 dotFile <<
" \"" << reactionNodeId <<
"\" -> \"" << product.name() <<
"\" [color=\"" << edge_color <<
"\"];\n";
770 for (
const auto& [symbol, entry] : netIn.
composition) {
777 const auto& all_species =
m_baseEngine.getNetworkSpecies();
778 std::vector<Species> fast_species;
780 for (
const auto& species : all_species) {
783 fast_species.push_back(species);
798 const std::vector<double> &Y,
804 fourdst::composition::Composition composition;
806 std::vector<std::string> symbols;
807 symbols.reserve(
m_baseEngine.getNetworkSpecies().size());
808 for (
const auto& species :
m_baseEngine.getNetworkSpecies()) {
809 symbols.emplace_back(species.name());
811 composition.registerSymbol(symbols);
813 std::vector<double> X;
814 X.reserve(Y_equilibrated.size());
815 for (
size_t i = 0; i < Y_equilibrated.size(); ++i) {
816 const double molarMass =
m_baseEngine.getNetworkSpecies()[i].mass();
817 X.push_back(Y_equilibrated[i] * molarMass);
820 for (
size_t i = 0; i < Y_equilibrated.size(); ++i) {
821 const auto& species =
m_baseEngine.getNetworkSpecies()[i];
822 if (X[i] < 0.0 && std::abs(X[i]) < 1e-20) {
823 composition.setMassFraction(std::string(species.name()), 0.0);
825 composition.setMassFraction(std::string(species.name()), X[i]);
829 composition.finalize(
true);
841 const double rho = netIn.
density;
851 const std::vector<double>& Y_full,
855 LOG_TRACE_L1(
m_logger,
"Partitioning by timescale...");
856 const auto result=
m_baseEngine.getSpeciesDestructionTimescales(Y_full, T9, rho);
858 LOG_ERROR(
m_logger,
"Failed to get species timescales due to stale engine state");
862 std::unordered_map<Species, double> all_timescales = result.value();
863 const auto& all_species =
m_baseEngine.getNetworkSpecies();
865 std::vector<std::pair<double, size_t>> sorted_timescales;
866 for (
size_t i = 0; i < all_species.size(); ++i) {
867 double timescale = all_timescales.at(all_species[i]);
868 if (std::isfinite(timescale) && timescale > 0) {
869 sorted_timescales.push_back({timescale, i});
875 [](
const auto& a,
const auto& b)
877 return a.first > b.first;
881 std::vector<std::vector<size_t>> final_pools;
882 if (sorted_timescales.empty()) {
886 constexpr double ABSOLUTE_QSE_TIMESCALE_THRESHOLD = 3.156e7;
887 constexpr double MIN_GAP_THRESHOLD = 2.0;
889 LOG_TRACE_L1(
m_logger,
"Found {} species with finite timescales.", sorted_timescales.size());
890 LOG_TRACE_L1(
m_logger,
"Absolute QSE timescale threshold: {} seconds ({} years).",
891 ABSOLUTE_QSE_TIMESCALE_THRESHOLD, ABSOLUTE_QSE_TIMESCALE_THRESHOLD / 3.156e7);
892 LOG_TRACE_L1(
m_logger,
"Minimum gap threshold: {} orders of magnitude.", MIN_GAP_THRESHOLD);
894 std::vector<size_t> dynamic_pool_indices;
895 std::vector<std::pair<double, size_t>> fast_candidates;
898 for (
const auto& ts_pair : sorted_timescales) {
899 if (ts_pair.first > ABSOLUTE_QSE_TIMESCALE_THRESHOLD) {
900 LOG_TRACE_L3(
m_logger,
"Species {} with timescale {} is considered dynamic (slower than qse timescale threshold).",
901 all_species[ts_pair.second].name(), ts_pair.first);
902 dynamic_pool_indices.push_back(ts_pair.second);
904 LOG_TRACE_L3(
m_logger,
"Species {} with timescale {} is a candidate fast species (faster than qse timescale threshold).",
905 all_species[ts_pair.second].name(), ts_pair.first);
906 fast_candidates.push_back(ts_pair);
910 if (!dynamic_pool_indices.empty()) {
911 LOG_TRACE_L1(
m_logger,
"Found {} dynamic species (slower than QSE timescale threshold).", dynamic_pool_indices.size());
912 final_pools.push_back(dynamic_pool_indices);
915 if (fast_candidates.empty()) {
916 LOG_TRACE_L1(
m_logger,
"No fast candidates found.");
921 std::vector<size_t> split_points;
922 for (
size_t i = 0; i < fast_candidates.size() - 1; ++i) {
923 const double t1 = fast_candidates[i].first;
924 const double t2 = fast_candidates[i+1].first;
925 if (std::log10(t1) - std::log10(t2) > MIN_GAP_THRESHOLD) {
926 LOG_TRACE_L3(
m_logger,
"Detected gap between species {} (timescale {:0.2E}) and {} (timescale {:0.2E}).",
927 all_species[fast_candidates[i].second].name(), t1,
928 all_species[fast_candidates[i+1].second].name(), t2);
929 split_points.push_back(i + 1);
933 size_t last_split = 0;
934 for (
const size_t split : split_points) {
935 std::vector<size_t> pool;
936 for (
size_t i = last_split; i < split; ++i) {
937 pool.push_back(fast_candidates[i].second);
939 final_pools.push_back(pool);
943 std::vector<size_t> final_fast_pool;
944 for (
size_t i = last_split; i < fast_candidates.size(); ++i) {
945 final_fast_pool.push_back(fast_candidates[i].second);
947 final_pools.push_back(final_fast_pool);
949 LOG_TRACE_L1(
m_logger,
"Final partitioned pools: {}",
950 [&]() -> std::string {
951 std::stringstream ss;
953 for (
const auto& pool : final_pools) {
956 for (
const auto& idx : pool) {
957 ss << all_species[idx].name();
958 if (ic < pool.size() - 1) {
964 if (oc < final_pools.size() - 1) {
1001 std::vector<MultiscalePartitioningEngineView::QSEGroup>
1003 const std::vector<QSEGroup> &candidate_groups,
1004 const std::vector<double> &Y,
1005 const double T9,
const double rho
1007 constexpr double FLUX_RATIO_THRESHOLD = 100;
1008 std::vector<QSEGroup> validated_groups = candidate_groups;
1009 for (
auto& group : validated_groups) {
1010 double internal_flux = 0.0;
1011 double external_flux = 0.0;
1013 const std::unordered_set<size_t> group_members(
1014 group.species_indices.begin(),
1015 group.species_indices.end()
1023 bool has_internal_reactant =
false;
1024 bool has_external_reactant =
false;
1026 for (
const auto& reactant :
reaction.reactants()) {
1027 if (group_members.contains(
m_baseEngine.getSpeciesIndex(reactant))) {
1028 has_internal_reactant =
true;
1030 has_external_reactant =
true;
1034 bool has_internal_product =
false;
1035 bool has_external_product =
false;
1037 for (
const auto& product :
reaction.products()) {
1038 if (group_members.contains(
m_baseEngine.getSpeciesIndex(product))) {
1039 has_internal_product =
true;
1041 has_external_product =
true;
1046 if ((has_internal_reactant && has_internal_product) && !(has_external_reactant || has_external_product)) {
1049 "Reaction {} is internal to the group containing {} and contributes to internal flux by {}",
1051 [&]() -> std::string {
1052 std::stringstream ss;
1054 for (const auto& idx : group.algebraic_indices) {
1055 ss << m_baseEngine.getNetworkSpecies()[idx].name();
1056 if (count < group.species_indices.size() - 1) {
1065 internal_flux += flow;
1066 }
else if ((has_internal_reactant || has_internal_product) && (has_external_reactant || has_external_product)) {
1069 "Reaction {} is external to the group containing {} and contributes to external flux by {}",
1071 [&]() -> std::string {
1072 std::stringstream ss;
1074 for (const auto& idx : group.algebraic_indices) {
1075 ss << m_baseEngine.getNetworkSpecies()[idx].name();
1076 if (count < group.species_indices.size() - 1) {
1085 external_flux += flow;
1089 if (internal_flux > FLUX_RATIO_THRESHOLD * external_flux) {
1092 "Group containing {} is in equilibrium: internal flux = {}, external flux = {}, ratio = {}",
1093 [&]() -> std::string {
1094 std::stringstream ss;
1096 for (
const auto& idx : group.algebraic_indices) {
1097 ss << m_baseEngine.getNetworkSpecies()[idx].name();
1098 if (count < group.species_indices.size() - 1) {
1107 internal_flux / external_flux
1109 group.is_in_equilibrium =
true;
1113 "Group containing {} is NOT in equilibrium: internal flux = {}, external flux = {}, ratio = {}",
1114 [&]() -> std::string {
1115 std::stringstream ss;
1117 for (
const auto& idx : group.algebraic_indices) {
1118 ss << m_baseEngine.getNetworkSpecies()[idx].name();
1119 if (count < group.species_indices.size() - 1) {
1128 internal_flux / external_flux
1130 group.is_in_equilibrium =
false;
1133 return validated_groups;
1137 const std::vector<double> &Y_full,
1141 LOG_TRACE_L1(
m_logger,
"Solving for QSE abundances...");
1142 auto Y_out = Y_full;
1150 for (
const auto&[species_indices, is_in_equilibrium, algebraic_indices, seed_indices, mean_timescale] :
m_qse_groups) {
1151 if (!is_in_equilibrium || species_indices.empty()) {
1154 "Skipping QSE group with {} species ({} algebraic ({}), {} seeds ({})) as it is not in equilibrium.",
1155 species_indices.size(),
1156 algebraic_indices.size(),
1157 [&]() -> std::string {
1158 std::ostringstream os;
1160 for (const auto& idx : algebraic_indices) {
1161 os << m_baseEngine.getNetworkSpecies()[idx].name();
1162 if (count < algebraic_indices.size() - 1) {
1169 seed_indices.size(),
1170 [&]() -> std::string {
1171 std::ostringstream os;
1173 for (
const auto& idx : seed_indices) {
1175 if (count < seed_indices.size() - 1) {
1188 "Solving for QSE abundances for group with {} species ([{}] algebraic, [{}] seeds).",
1189 species_indices.size(),
1190 [&]() -> std::string {
1191 std::stringstream ss;
1193 for (const auto& idx : algebraic_indices) {
1194 ss << m_baseEngine.getNetworkSpecies()[idx].name();
1195 if (count < algebraic_indices.size() - 1) {
1202 [&]() -> std::string {
1203 std::stringstream ss;
1205 for (
const auto& idx : seed_indices) {
1206 ss << m_baseEngine.getNetworkSpecies()[idx].name();
1207 if (count < seed_indices.size() - 1) {
1216 std::vector<size_t> qse_solve_indices;
1217 std::vector<size_t> seed_indices_vec;
1219 seed_indices_vec.reserve(species_indices.size());
1220 qse_solve_indices.reserve(species_indices.size());
1222 for (
size_t seed_idx : seed_indices) {
1223 seed_indices_vec.emplace_back(seed_idx);
1226 for (
size_t algebraic_idx : algebraic_indices) {
1227 qse_solve_indices.emplace_back(algebraic_idx);
1230 if (qse_solve_indices.empty())
continue;
1232 Eigen::VectorXd Y_scale(qse_solve_indices.size());
1233 Eigen::VectorXd v_initial(qse_solve_indices.size());
1234 for (
size_t i = 0; i < qse_solve_indices.size(); ++i) {
1235 constexpr double abundance_floor = 1.0e-15;
1236 const double initial_abundance = Y_full[qse_solve_indices[i]];
1237 Y_scale(i) = std::max(initial_abundance, abundance_floor);
1238 v_initial(i) = std::asinh(initial_abundance / Y_scale(i));
1241 EigenFunctor functor(*
this, qse_solve_indices, Y_full, T9, rho, Y_scale);
1242 Eigen::LevenbergMarquardt lm(functor);
1243 lm.parameters.ftol = 1.0e-10;
1244 lm.parameters.xtol = 1.0e-10;
1246 LOG_TRACE_L1(m_logger,
"Minimizing functor...");
1247 Eigen::LevenbergMarquardtSpace::Status status = lm.minimize(v_initial);
1249 if (status <= 0 || status >= 4) {
1250 std::stringstream msg;
1251 msg <<
"QSE solver failed with status: " << status <<
" for group with seed ";
1252 if (seed_indices.size() == 1) {
1253 msg <<
"nucleus " << m_baseEngine.getNetworkSpecies()[seed_indices_vec[0]].name();
1258 for (
const auto& seed_idx : seed_indices) {
1259 msg << m_baseEngine.getNetworkSpecies()[seed_idx].name();
1260 if (counter < seed_indices.size() - 2) {
1262 }
else if (counter == seed_indices.size() - 2) {
1263 if (seed_indices.size() < 2) {
1272 throw std::runtime_error(msg.str());
1274 LOG_TRACE_L1(m_logger,
"Minimization succeeded!");
1275 Eigen::VectorXd Y_final_qse = Y_scale.array() * v_initial.array().sinh();
1276 for (
size_t i = 0; i < qse_solve_indices.size(); ++i) {
1279 "Species {} (index {}) started with abundance {} and ended with {}.",
1280 m_baseEngine.getNetworkSpecies()[qse_solve_indices[i]].name(),
1281 qse_solve_indices[i],
1282 Y_full[qse_solve_indices[i]],
1285 Y_out[qse_solve_indices[i]] = Y_final_qse(i);
1292 const std::vector<std::vector<size_t>> &pools,
1293 const std::vector<double> &Y,
1297 const auto& result =
m_baseEngine.getSpeciesDestructionTimescales(Y, T9, rho);
1299 LOG_ERROR(
m_logger,
"Failed to get species timescales due to stale engine state");
1303 const std::unordered_map<Species, double> all_timescales = result.value();
1306 const auto& all_species =
m_baseEngine.getNetworkSpecies();
1308 size_t slowest_pool_index = 0;
1309 double slowest_mean_timescale = std::numeric_limits<double>::min();
1311 for (
const auto& pool : pools) {
1312 double mean_timescale = 0.0;
1313 for (
const auto& species_idx : pool) {
1314 const double timescale = all_timescales.at(all_species[species_idx]);
1315 mean_timescale += timescale;
1317 mean_timescale = mean_timescale / pool.size();
1318 if (std::isinf(mean_timescale)) {
1319 LOG_CRITICAL(
m_logger,
"Encountered infinite mean timescale for pool {} with species: {}",
1320 count, [&]() -> std::string {
1321 std::stringstream ss;
1323 for (
const auto& idx : pool) {
1324 ss << all_species[idx].name() <<
": " << all_timescales.at(all_species[idx]);
1325 if (iCount < pool.size() - 1) {
1334 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...");
1336 if (mean_timescale > slowest_mean_timescale) {
1337 slowest_mean_timescale = mean_timescale;
1338 slowest_pool_index = &pool - &pools[0];
1341 return slowest_pool_index;
1345 const std::vector<size_t> &species_pool
1347 std::unordered_map<size_t, std::vector<size_t>> connectivity_graph;
1348 const std::set<size_t> pool_set(species_pool.begin(), species_pool.end());
1349 const std::unordered_set<Species> pool_species = [&]() -> std::unordered_set<Species> {
1350 std::unordered_set<Species> result;
1351 for (
const auto& species_idx : species_pool) {
1352 Species species =
m_baseEngine.getNetworkSpecies()[species_idx];
1353 result.insert(species);
1358 std::map<size_t, std::vector<reaction::LogicalReaction*>> speciesReactionMap;
1359 std::vector<const reaction::LogicalReaction*> candidate_reactions;
1361 auto getSpeciesIdx = [&](
const std::vector<Species> &species) -> std::vector<size_t> {
1362 std::vector<size_t> result;
1363 result.reserve(species.size());
1364 for (
const auto& s : species) {
1366 result.push_back(idx);
1372 const std::vector<Species> &reactants =
reaction.reactants();
1373 const std::vector<Species> &products =
reaction.products();
1375 std::unordered_set<Species> reactant_set(reactants.begin(), reactants.end());
1376 std::unordered_set<Species> product_set(products.begin(), products.end());
1379 if (has_distinct_reactant_and_product_species(pool_species, reactant_set, product_set)) {
1380 std::vector<size_t> involvedIDs = getSpeciesIdx(reactants);
1381 std::vector<size_t> involvedProducts = getSpeciesIdx(products);
1382 involvedIDs.insert(involvedIDs.end(), involvedProducts.begin(), involvedProducts.end());
1383 std::set<size_t> involvedSet(involvedIDs.begin(), involvedIDs.end());
1385 std::vector<size_t> intersection;
1386 intersection.reserve(involvedSet.size());
1388 std::ranges::set_intersection(pool_set, involvedSet, std::back_inserter(intersection));
1391 for (
const size_t& u : intersection) {
1392 for (
const size_t& v : intersection) {
1394 connectivity_graph[u].push_back(v);
1402 return connectivity_graph;
1406 const std::vector<std::vector<size_t>> &candidate_pools,
1407 const std::vector<double> &Y,
1408 const double T9,
const double rho
1410 const auto& all_species =
m_baseEngine.getNetworkSpecies();
1411 const auto& all_reactions =
m_baseEngine.getNetworkReactions();
1412 const auto& result =
m_baseEngine.getSpeciesDestructionTimescales(Y, T9, rho);
1414 LOG_ERROR(
m_logger,
"Failed to get species timescales due to stale engine state");
1418 const std::unordered_map<Species, double> destruction_timescales = result.value();
1420 std::vector<QSEGroup> candidate_groups;
1421 for (
const auto& pool : candidate_pools) {
1422 if (pool.empty())
continue;
1425 std::vector<std::pair<reaction::LogicalReaction, double>> bridge_reactions;
1426 for (
const auto& species_idx : pool) {
1427 Species ash = all_species[species_idx];
1428 for (
const auto&
reaction : all_reactions) {
1432 bool has_external_reactant =
false;
1433 for (
const auto& reactant :
reaction.reactants()) {
1434 if (std::ranges::find(pool,
m_baseEngine.getSpeciesIndex(reactant)) == pool.end()) {
1435 has_external_reactant =
true;
1436 LOG_TRACE_L3(
m_logger,
"Found external reactant {} in reaction {} for species {}.", reactant.name(),
reaction.id(), ash.name());
1440 if (has_external_reactant) {
1442 LOG_TRACE_L3(
m_logger,
"Found bridge reaction {} with flow {} for species {}.",
reaction.id(), flow, ash.name());
1443 bridge_reactions.push_back({
reaction, flow});
1450 [](
const auto& a,
const auto& b) {
1451 return a.second > b.second;
1454 constexpr double MIN_GAP_THRESHOLD = 1;
1455 std::vector<size_t> split_points;
1456 for (
size_t i = 0; i < bridge_reactions.size() - 1; ++i) {
1457 const double f1 = bridge_reactions[i].second;
1458 const double f2 = bridge_reactions[i + 1].second;
1459 if (std::log10(f1) - std::log10(f2) > MIN_GAP_THRESHOLD) {
1460 LOG_TRACE_L3(
m_logger,
"Detected gap between bridge reactions with flows {} and {}.", f1, f2);
1461 split_points.push_back(i + 1);
1465 if (split_points.empty()) {
1466 split_points.push_back(bridge_reactions.size() - 1);
1469 std::vector<size_t> seed_indices;
1470 for (
size_t i = 0; i < bridge_reactions.size(); ++i) {
1471 for (
const auto& fuel : bridge_reactions[i].first.reactants()) {
1474 if (std::ranges::find(pool, fuel_idx) == pool.end()) {
1475 seed_indices.push_back(fuel_idx);
1479 std::set<size_t> all_indices(pool.begin(), pool.end());
1480 for (
const auto& seed_idx : seed_indices) {
1481 all_indices.insert(seed_idx);
1483 const std::set<size_t> poolSet(pool.begin(), pool.end());
1484 const std::set<size_t> seedSet(seed_indices.begin(), seed_indices.end());
1486 double mean_timescale = 0.0;
1487 for (
const auto& pool_idx : poolSet) {
1488 const auto& species = all_species[pool_idx];
1489 if (destruction_timescales.contains(species)) {
1490 mean_timescale += std::min(destruction_timescales.at(species), species.halfLife());
1492 mean_timescale += species.halfLife();
1495 mean_timescale /= poolSet.size();
1496 QSEGroup qse_group(all_indices,
false, poolSet, seedSet, mean_timescale);
1497 candidate_groups.push_back(qse_group);
1499 return candidate_groups;
1506 Eigen::VectorXd y_qse =
m_Y_scale.array() * v_qse.array().sinh();
1512 const auto result =
m_view->getBaseEngine().calculateRHSAndEnergy(y_trial,
m_T9,
m_rho);
1516 const auto&[dydt, nuclearEnergyGenerationRate] = result.value();
1527 Eigen::VectorXd y_qse =
m_Y_scale.array() * v_qse.array().sinh();
1540 J_qse(i, j) =
m_view->getBaseEngine().getJacobianMatrixEntry(
1548 for (
long j = 0; j < J_qse.cols(); ++j) {
1549 const double dY_dv =
m_Y_scale(j) * std::cosh(v_qse(j));
1550 J_qse.col(j) *= dY_dv;
1559 const std::vector<double> &Y
1568 std::size_t seed = 0;
1570 hash_combine(seed,
m_Y.size());
1575 for (
double Yi :
m_Y) {
1576 if (Yi < 0.0 && std::abs(Yi) < 1e-20) {
1578 }
else if (Yi < 0.0 && std::abs(Yi) >= 1e-20) {
1579 throw std::invalid_argument(
"Yi should be positive for valid hashing (expected Yi > 0, received " + std::to_string(Yi) +
")");
1589 return static_cast<long>(std::floor(value / tol));
1609 return !(*
this == other);
1614 throw std::invalid_argument(
"Cannot use 'ALL' as an operator for a hit");
1622 throw std::invalid_argument(
"Cannot use 'ALL' as an operator for a miss");
Abstract class for engines supporting Jacobian and stoichiometry operations.
A reaction network engine that uses a graph-based representation.
const std::vector< fourdst::atomic::Species > & getNetworkSpecies() const override
Gets the list of species in the network.
GraphEngine & m_baseEngine
The base engine to which this view delegates calculations.
PrimingReport primeEngine(const NetIn &netIn) override
Primes the engine with a specific species.
MultiscalePartitioningEngineView(GraphEngine &baseEngine)
Constructs a MultiscalePartitioningEngineView.
void setScreeningModel(screening::ScreeningType model) override
Sets the electron screening model.
std::vector< QSEGroup > m_qse_groups
The list of identified equilibrium groups.
const std::vector< fourdst::atomic::Species > & getDynamicSpecies() const
Gets the dynamic species in the network.
const DynamicEngine & getBaseEngine() const override
Gets the base engine.
std::vector< size_t > m_dynamic_species_indices
Indices mapping the dynamic species back to the base engine's full species list.
std::vector< double > solveQSEAbundances(const std::vector< double > &Y_full, double T9, double rho)
Solves for the QSE abundances of the algebraic species in a given state.
std::vector< fourdst::atomic::Species > getFastSpecies() const
Gets the fast species in the network.
std::vector< fourdst::atomic::Species > m_algebraic_species
Species that are treated as algebraic (in QSE) in the QSE groups.
fourdst::composition::Composition equilibrateNetwork(const std::vector< double > &Y, double T9, double rho)
Equilibrates the network by partitioning and solving for QSE abundances.
int getStoichiometryMatrixEntry(int speciesIndex, int reactionIndex) const override
Gets an entry from the stoichiometry matrix.
std::vector< size_t > m_algebraic_species_indices
Indices of algebraic species in the full network.
size_t identifyMeanSlowestPool(const std::vector< std::vector< size_t > > &pools, const std::vector< double > &Y, double T9, double rho) const
Identifies the pool with the slowest mean timescale.
std::expected< std::unordered_map< fourdst::atomic::Species, double >, expectations::StaleEngineError > getSpeciesTimescales(const std::vector< double > &Y, double T9, double rho) const override
Computes timescales for all species in the network.
const std::vector< fourdst::atomic::Species > & getNetworkSpecies() const override
Gets the list of species in the network.
fourdst::composition::Composition update(const NetIn &netIn) override
Updates the internal state of the engine, performing partitioning and QSE equilibration.
std::unordered_map< QSECacheKey, std::vector< double > > m_qse_abundance_cache
Cache for QSE abundances based on T9, rho, and Y.
std::expected< StepDerivatives< double >, expectations::StaleEngineError > calculateRHSAndEnergy(const std::vector< double > &Y_full, double T9, double rho) const override
Calculates the right-hand side (dY/dt) and energy generation.
double calculateMolarReactionFlow(const reaction::Reaction &reaction, const std::vector< double > &Y_full, double T9, double rho) const override
Calculates the molar reaction flow for a given reaction.
screening::ScreeningType getScreeningModel() const override
Gets the current electron screening model.
void partitionNetwork(const std::vector< double > &Y, double T9, double rho)
Partitions the network into dynamic and algebraic (QSE) groups based on timescales.
quill::Logger * m_logger
Logger instance for logging messages.
int getSpeciesIndex(const fourdst::atomic::Species &species) const override
Gets the index of a species in the full network.
std::expected< std::unordered_map< fourdst::atomic::Species, double >, expectations::StaleEngineError > getSpeciesDestructionTimescales(const std::vector< double > &Y, double T9, double rho) const override
Computes destruction timescales for all species in the network.
std::vector< QSEGroup > validateGroupsWithFluxAnalysis(const std::vector< QSEGroup > &candidate_groups, const std::vector< double > &Y, double T9, double rho) const
Validates candidate QSE groups using flux analysis.
CacheStats m_cacheStats
Statistics for the QSE abundance cache.
std::vector< double > mapNetInToMolarAbundanceVector(const NetIn &netIn) const override
Maps a NetIn struct to a molar abundance vector for the full network.
std::unordered_map< size_t, std::vector< size_t > > buildConnectivityGraph(const std::unordered_set< size_t > &fast_reaction_indices) const
Builds a connectivity graph from a set of fast reaction indices.
void generateStoichiometryMatrix() override
Generates the stoichiometry matrix for the network.
std::vector< QSEGroup > constructCandidateGroups(const std::vector< std::vector< size_t > > &candidate_pools, const std::vector< double > &Y, double T9, double rho) const
Constructs candidate QSE groups from connected timescale pools.
double getJacobianMatrixEntry(int i_full, int j_full) const override
Gets an entry from the previously generated Jacobian matrix.
void setNetworkReactions(const reaction::LogicalReactionSet &reactions) override
Sets the set of logical reactions in the network.
void generateJacobianMatrix(const std::vector< double > &Y_full, double T9, double rho) const override
Generates the Jacobian matrix for the current state.
void exportToDot(const std::string &filename, const std::vector< double > &Y, const double T9, const double rho) const
Exports the network to a DOT file for visualization.
std::vector< std::vector< size_t > > partitionByTimescale(const std::vector< double > &Y_full, double T9, double rho) const
Partitions the network by timescale.
const reaction::LogicalReactionSet & getNetworkReactions() const override
Gets the set of logical reactions in the network.
bool isStale(const NetIn &netIn) override
Checks if the engine's internal state is stale relative to the provided conditions.
std::vector< fourdst::atomic::Species > m_dynamic_species
The simplified set of species presented to the solver (the "slow" species).
std::vector< std::vector< size_t > > analyzeTimescalePoolConnectivity(const std::vector< std::vector< size_t > > ×cale_pools, const std::vector< double > &Y, double T9, double rho) const
Analyzes the connectivity of timescale pools.
Represents a single nuclear reaction from a specific data source.
TemplatedReactionSet< LogicalReaction > LogicalReactionSet
A set of logical reactions.
ScreeningType
Enumerates the available plasma screening models.
static int s_operator_parens_called
size_t m_hit
Total number of cache hits.
size_t misses(const operators op=operators::All) const
Gets the number of misses for a specific operator or all operators.
void hit(const operators op=operators::Other)
Increments the hit counter for a given operator.
size_t m_miss
Total number of cache misses.
size_t hits(const operators op=operators::All) const
Gets the number of hits for a specific operator or all operators.
std::map< operators,size_t > m_operatorHits
Map from operators to the number of cache hits for that operator.
@ CalculateMolarReactionFlow
@ GetSpeciesDestructionTimescales
void miss(const operators op=operators::Other)
Increments the miss counter for a given operator.
std::map< operators,size_t > m_operatorMisses
Map from operators to the number of cache misses for that operator.
const std::vector< double > & m_Y_full_initial
Initial abundances of all species in the full network.
Eigen::Matrix< double, Eigen::Dynamic, 1 > InputType
Eigen::Matrix< double, Eigen::Dynamic, 1 > OutputType
const double m_rho
Density in g/cm^3.
const std::vector< size_t > & m_qse_solve_indices
Indices of the species to solve for in the QSE group.
const double m_T9
Temperature in units of 10^9 K.
const Eigen::VectorXd & m_Y_scale
Scaling factors for the species abundances, used to improve solver stability.
int df(const InputType &v_qse, JacobianType &J_qse) const
Evaluates the Jacobian of the functor, J_qse = d(f_qse)/d(v_qse).
Eigen::Matrix< double, Eigen::Dynamic, Eigen::Dynamic > JacobianType
int operator()(const InputType &v_qse, OutputType &f_qse) const
Evaluates the functor's residual vector f_qse = dY_alg/dt.
MultiscalePartitioningEngineView * m_view
Pointer to the MultiscalePartitioningEngineView instance.
Struct representing a QSE group.
bool operator<(const QSEGroup &other) const
Less-than operator for QSEGroup, used for sorting.
double mean_timescale
Mean timescale of the group.
bool operator>(const QSEGroup &other) const
Greater-than operator for QSEGroup.
bool operator==(const QSEGroup &other) const
Equality operator for QSEGroup.
bool operator!=(const QSEGroup &other) const
Inequality operator for QSEGroup.
double density
Density in g/cm^3.
fourdst::composition::Composition composition
Composition of the network.
double temperature
Temperature in Kelvin.
Captures the result of a network priming operation.
fourdst::composition::Composition primedComposition
Key struct for the QSE abundance cache.
QSECacheKey(const double T9, const double rho, const std::vector< double > &Y)
Constructs a QSECacheKey.
QSECacheConfig m_cacheConfig
size_t hash() const
Computes the hash value for this key.
std::size_t m_hash
Precomputed hash value for this key.
static long bin(double value, double tol)
Converts a value to a discrete bin based on a tolerance.
bool operator==(const QSECacheKey &other) const
Equality operator for QSECacheKey.
std::vector< double > m_Y
Note that the ordering of Y must match the dynamic species indices in the view.