fourdst::libcomposition v1.9.0
Robust atomic species information library
Loading...
Searching...
No Matches
composition.cpp
Go to the documentation of this file.
1/* ***********************************************************************
2//
3// Copyright (C) 2025 -- The 4D-STAR Collaboration
4// File Author: Emily Boudreaux
5// Last Modified: October 6, 2025
6//
7// 4DSSE is free software; you can use it and/or modify
8// it under the terms and restrictions the GNU General Library Public
9// License version 3 (GPLv3) as published by the Free Software Foundation.
10//
11// 4DSSE is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
14// See the GNU Library General Public License for more details.
15//
16// You should have received a copy of the GNU Library General Public License
17// along with this software; if not, write to the Free Software
18// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19//
20// *********************************************************************** */
21#include "quill/LogMacros.h"
22
23#include <stdexcept>
24#include <unordered_map>
25#include <vector>
26#include <array>
27#include <ranges>
28#include <algorithm>
29
30
31#include <utility>
32
36
37#include <numeric>
38
40
41namespace {
42 template<typename A, typename B>
43 std::vector<A> sortVectorBy(
44 std::vector<A> toSort,
45 const std::vector<B>& by
46 ) {
47 std::vector<std::size_t> indices(by.size());
48 for (size_t i = 0; i < indices.size(); i++) {
49 indices[i] = i;
50 }
51
52 std::ranges::sort(indices, [&](size_t a, size_t b) {
53 return by[a] < by[b];
54 });
55
56 std::vector<A> sorted;
57 sorted.reserve(indices.size());
58
59 for (const auto idx: indices) {
60 sorted.push_back(toSort[idx]);
61 }
62
63 return sorted;
64 }
65}
66
67namespace fourdst::composition {
68
70 m_symbol("H-1"),
71 m_isotope(fourdst::atomic::species.at("H-1")),
72 m_initialized(false) {}
73
75 const std::string& symbol,
76 const bool massFracMode
77 ) :
79 m_isotope(atomic::species.at(symbol)),
80 m_massFracMode(massFracMode) {
82 }
83
85
86 void CompositionEntry::setSpecies(const std::string& symbol) {
87 if (m_initialized) {
88 throw exceptions::EntryAlreadyInitializedError("Composition entry is already initialized.");
89 }
90 if (!fourdst::atomic::species.contains(symbol)) {
91 throw exceptions::InvalidSpeciesSymbolError("Invalid symbol.");
92 }
95 m_initialized = true;
96 }
97
98 std::string CompositionEntry::symbol() const {
99 return m_symbol;
100 }
101
103 if (!m_massFracMode) {
104 throw exceptions::CompositionModeError("Composition entry is in number fraction mode.");
105 }
106 // X_i = (moles_i / mass_total) * (mass_i / moles_i) = m_molesPerMass * A_i
107 return m_molesPerMass * m_isotope.mass();
108 }
109
111 if (m_massFracMode) {
112 throw exceptions::CompositionModeError("Composition entry is in mass fraction mode.");
113 }
114 // In number fraction mode, the value is cached during the mode switch.
116 }
117
119 const double totalMolesPerMass
120 ) const {
121 // n_i = (moles_i / mass_total) / (moles_total / mass_total)
122 if (totalMolesPerMass == 0.0) return 0.0;
123 return m_molesPerMass / totalMolesPerMass;
124 }
125
127 return m_molesPerMass;
128 }
129
133
135 const double mass_fraction
136 ) {
137 if (!m_massFracMode) {
138 throw exceptions::CompositionModeError("Composition entry is in number fraction mode.");
139 }
140 // Set the invariant from the given mass fraction
141 if (m_isotope.mass() == 0.0) {
142 m_molesPerMass = 0.0;
143 } else {
145 }
146 }
147
149 const double number_fraction
150 ) {
151 if (m_massFracMode) {
152 throw exceptions::CompositionModeError("Composition entry is in mass fraction mode.");
153 }
154 // In number fraction mode, we only cache the value. The invariant
155 // m_molesPerMass cannot be calculated until finalize() provides global context.
157 }
158
160 [[maybe_unused]] const double meanMolarMass
161 ) {
162 if (m_massFracMode) {
163 return false;
164 }
165 m_massFracMode = true;
166 // The invariant m_molesPerMass does not change when switching mode.
167 // The cached number fraction is now stale, but that's okay.
168 return true;
169 }
170
172 const double totalMolesPerMass
173 ) {
174 if (!m_massFracMode) {
175 return false;
176 }
177 m_massFracMode = false;
178 // Calculate and cache the number fraction for the new mode.
179 m_cachedNumberFraction = number_fraction(totalMolesPerMass);
180 return true;
181 }
182
184 return m_massFracMode;
185 }
186
188 const std::vector<std::string>& symbols
189 ) {
190 for (const auto& symbol : symbols) {
191 registerSymbol(symbol);
192 }
193 }
194
196 const std::set<std::string>& symbols
197 ) {
198 for (const auto& symbol : symbols) {
199 registerSymbol(symbol);
200 }
201 }
202
204 const std::vector<std::string>& symbols,
205 const std::vector<double>& fractions,
206 const bool massFracMode
207 ) : m_massFracMode(massFracMode) {
208 if (symbols.size() != fractions.size()) {
209 LOG_CRITICAL(m_logger, "The number of symbols and fractions must be equal (got {} symbols and {} fractions).", symbols.size(), fractions.size());
210 throw exceptions::InvalidCompositionError("The number of symbols and fractions must be equal. Got " + std::to_string(symbols.size()) + " symbols and " + std::to_string(fractions.size()) + " fractions.");
211 }
212
213 validateComposition(fractions);
214
215 for (const auto &symbol : symbols) {
217 }
218
219 for (size_t i = 0; i < symbols.size(); ++i) {
220 if (m_massFracMode) {
221 setMassFraction(symbols[i], fractions[i]);
222 } else {
223 setNumberFraction(symbols[i], fractions[i]);
224 }
225 }
226 if (const bool didFinalize = finalize(); !didFinalize) {
227 std::string msg = "Failed to finalize composition on construction. ";
228 msg += "Construction of a composition object requires that the sum of the fractions vector be 1.\n";
229 LOG_CRITICAL(m_logger, "{}", msg);
231 }
232 }
233
235 m_finalized = composition.m_finalized;
236 m_specificNumberDensity = composition.m_specificNumberDensity;
237 m_meanParticleMass = composition.m_meanParticleMass;
238 m_massFracMode = composition.m_massFracMode;
239 m_registeredSymbols = composition.m_registeredSymbols;
240 m_compositions = composition.m_compositions;
241 }
242
244 if (this != &other) {
245 m_finalized = other.m_finalized;
251 }
252 return *this;
253 }
254
256 const std::string& symbol,
257 const bool massFracMode
258 ) {
259 if (!isValidSymbol(symbol)) {
260 LOG_ERROR(m_logger, "Invalid symbol: {}", symbol);
261 throw exceptions::InvalidSymbolError("Invalid symbol: " + symbol);
262 }
263
264 if (m_registeredSymbols.empty()) {
265 m_massFracMode = massFracMode;
266 } else {
267 if (m_massFracMode != massFracMode) {
268 LOG_ERROR(m_logger, "Composition is in {} fraction mode. Cannot register symbol ({}) in {} fraction mode.", m_massFracMode ? "mass" : "number", symbol, massFracMode ? "mass" : "number");
269 throw exceptions::CompositionModeError("Composition mode mismatch.");
270 }
271 }
272
273 if (m_registeredSymbols.contains(symbol)) {
274 LOG_WARNING(m_logger, "Symbol {} is already registered.", symbol);
275 return;
276 }
277
278 m_registeredSymbols.insert(symbol);
280 m_finalized = false;
281 LOG_TRACE_L3(m_logger, "Registered symbol: {}", symbol);
282 }
283
285 const std::vector<std::string>& symbols,
286 const bool massFracMode
287 ) {
288 for (const auto& symbol : symbols) {
289 registerSymbol(symbol, massFracMode);
290 }
291 }
292
294 const atomic::Species &species,
295 const bool massFracMode
296 ) {
297 registerSymbol(std::string(species.name()), massFracMode);
298 }
299
301 const std::vector<atomic::Species> &species,
302 const bool massFracMode
303 ) {
304 for (const auto& s : species) {
305 registerSpecies(s, massFracMode);
306 }
307 }
308
309 std::set<std::string> Composition::getRegisteredSymbols() const {
310 return m_registeredSymbols;
311 }
312
313 std::set<atomic::Species> Composition::getRegisteredSpecies() const {
314 std::set<atomic::Species> result;
315 for (const auto& entry : m_compositions | std::views::values) {
316 result.insert(entry.isotope());
317 }
318 return result;
319 }
320
322 const std::string& symbol
323 ) {
324 return atomic::species.contains(symbol);
325 }
326
327 void Composition::validateComposition(const std::vector<double>& fractions) const {
328 if (!isValidComposition(fractions)) {
329 LOG_ERROR(m_logger, "Invalid composition.");
330 throw exceptions::InvalidCompositionError("Invalid composition.");
331 }
332 }
333
334 bool Composition::isValidComposition(const std::vector<double>& fractions) const {
335 const double sum = std::accumulate(fractions.begin(), fractions.end(), 0.0);
336 if (sum < 0.999999 || sum > 1.000001) {
337 LOG_ERROR(m_logger, "The sum of fractions must be equal to 1 (expected 1, got {}).", sum);
338 return false;
339 }
340 return true;
341 }
342
343 double Composition::setMassFraction(const std::string& symbol, const double& mass_fraction) {
344 if (!m_registeredSymbols.contains(symbol)) {
345 LOG_ERROR(m_logger, "Symbol {} is not registered.", symbol);
346 throw exceptions::UnregisteredSymbolError("Symbol (" + symbol + ") is not registered.");
347 }
348 if (!m_massFracMode) {
349 LOG_ERROR(m_logger, "Composition is in number fraction mode.");
350 throw exceptions::CompositionModeError("Composition is in number fraction mode.");
351 }
352 if (mass_fraction < 0.0 || mass_fraction > 1.0) {
353 LOG_ERROR(m_logger, "Mass fraction must be between 0 and 1 for symbol {}. Currently it is {}.", symbol, mass_fraction);
354 throw exceptions::InvalidCompositionError("Mass fraction must be between 0 and 1.");
355 }
356 m_finalized = false;
357 const double old_mass_fraction = m_compositions.at(symbol).mass_fraction();
358 m_compositions.at(symbol).setMassFraction(mass_fraction);
359 return old_mass_fraction;
360 }
361
362 std::vector<double> Composition::setMassFraction(const std::vector<std::string>& symbols, const std::vector<double>& mass_fractions) {
363 if (symbols.size() != mass_fractions.size()) {
364 throw exceptions::InvalidCompositionError("The number of symbols and mass fractions must be equal.");
365 }
366 std::vector<double> old_mass_fractions;
367 old_mass_fractions.reserve(symbols.size());
368 for (size_t i = 0; i < symbols.size(); ++i) {
369 old_mass_fractions.push_back(setMassFraction(symbols[i], mass_fractions[i]));
370 }
371 return old_mass_fractions;
372 }
373
375 const std::string& symbol,
376 const double& number_fraction
377 ) {
378 if (!m_registeredSymbols.contains(symbol)) {
379 LOG_ERROR(m_logger, "Symbol {} is not registered.", symbol);
380 throw exceptions::UnregisteredSymbolError("Symbol (" + symbol + ") is not registered.");
381 }
382 if (m_massFracMode) {
383 LOG_ERROR(m_logger, "Composition is in mass fraction mode.");
384 throw exceptions::CompositionModeError("Composition is in mass fraction mode.");
385 }
386 if (number_fraction < 0.0 || number_fraction > 1.0) {
387 LOG_ERROR(m_logger, "Number fraction must be between 0 and 1 for symbol {}. Currently it is {}.", symbol, number_fraction);
388 throw exceptions::InvalidCompositionError("Number fraction must be between 0 and 1.");
389 }
390 m_finalized = false;
391 const double old_number_fraction = m_compositions.at(symbol).number_fraction();
392 m_compositions.at(symbol).setNumberFraction(number_fraction);
393 return old_number_fraction;
394 }
395
397 const std::vector<std::string>& symbols,
398 const std::vector<double>& number_fractions
399 ) {
400 if (symbols.size() != number_fractions.size()) {
401 throw exceptions::InvalidCompositionError("The number of symbols and number fractions must be equal.");
402 }
403 std::vector<double> old_number_fractions;
404 old_number_fractions.reserve(symbols.size());
405 for (size_t i = 0; i < symbols.size(); ++i) {
406 old_number_fractions.push_back(setNumberFraction(symbols[i], number_fractions[i]));
407 }
408 return old_number_fractions;
409 }
410
412 const atomic::Species &species,
413 const double &mass_fraction
414 ) {
415 return setMassFraction(std::string(species.name()), mass_fraction);
416 }
417
418 std::vector<double> Composition::setMassFraction(
419 const std::vector<atomic::Species> &species,
420 const std::vector<double> &mass_fractions
421 ) {
422 std::vector<std::string> symbols;
423 symbols.reserve(species.size());
424 for(const auto& s : species) symbols.emplace_back(s.name());
425 return setMassFraction(symbols, mass_fractions);
426 }
427
429 const atomic::Species &species,
430 const double &number_fraction
431 ) {
432 return setNumberFraction(std::string(species.name()), number_fraction);
433 }
434
436 const std::vector<atomic::Species> &species,
437 const std::vector<double> &number_fractions
438 ) {
439 std::vector<std::string> symbols;
440 symbols.reserve(species.size());
441 for(const auto& s : species) symbols.push_back(std::string(s.name()));
442 return setNumberFraction(symbols, number_fractions);
443 }
444
445 bool Composition::finalize(const bool norm) {
447 m_meanParticleMass = 0.0;
449 m_cache.clear();
450 return m_finalized;
451 }
452
453 bool Composition::finalizeMassFracMode(const bool norm) {
454 std::vector<double> mass_fractions;
455 mass_fractions.reserve(m_compositions.size());
456 for (const auto &entry: m_compositions | std::views::values) {
457 mass_fractions.push_back(entry.mass_fraction());
458 }
459
460 double sum = std::accumulate(mass_fractions.begin(), mass_fractions.end(), 0.0);
461 if (norm && sum > 0) {
462 for (auto& [symbol, entry] : m_compositions) {
463 setMassFraction(symbol, entry.mass_fraction() / sum);
464 }
465 // Recalculate fractions vector after normalization for validation
466 mass_fractions.clear();
467 for (const auto &entry: m_compositions | std::views::values) {
468 mass_fractions.push_back(entry.mass_fraction());
469 }
470 }
471
472 try {
473 validateComposition(mass_fractions);
474 } catch ([[maybe_unused]] const exceptions::InvalidCompositionError& e) {
475 LOG_ERROR(m_logger, "Composition is invalid after mass frac finalization (Total mass {}).", sum);
476 return false;
477 }
478
479 for (const auto &entry: m_compositions | std::views::values) {
480 m_specificNumberDensity += entry.rel_abundance(); // rel_abundance is now consistently moles/mass
481 }
482
483 if (m_specificNumberDensity > 0) {
485 }
486 return true;
487 }
488
490 std::vector<double> number_fractions;
491 number_fractions.reserve(m_compositions.size());
492 for (const auto &entry: m_compositions | std::views::values) {
493 number_fractions.push_back(entry.number_fraction());
494 }
495
496 double sum = std::accumulate(number_fractions.begin(), number_fractions.end(), 0.0);
497 if (norm && sum > 0) {
498 for (auto& [symbol, entry] : m_compositions) {
499 setNumberFraction(symbol, entry.number_fraction() / sum);
500 }
501 // Recalculate fractions vector after normalization for validation
502 number_fractions.clear();
503 for (const auto &entry: m_compositions | std::views::values) {
504 number_fractions.push_back(entry.number_fraction());
505 }
506 }
507
508 try {
509 validateComposition(number_fractions);
510 } catch ([[maybe_unused]] const exceptions::InvalidCompositionError& e) {
511 LOG_ERROR(m_logger, "Composition is invalid after number frac finalization (Total number frac {}).", sum);
512 return false;
513 }
514
515 // Calculate mean particle mass <A> = sum(n_i * A_i)
516 for (const auto &entry: m_compositions | std::views::values) {
517 m_meanParticleMass += entry.number_fraction() * entry.isotope().mass();
518 }
519
520 for (auto &entry: m_compositions | std::views::values) {
521 const double X_i = (m_meanParticleMass > 0) ? (entry.number_fraction() * entry.isotope().mass() / m_meanParticleMass) : 0.0;
522 entry.m_massFracMode = true;
523 entry.setMassFraction(X_i);
524 entry.m_massFracMode = false;
525 }
526
527 if (m_meanParticleMass > 0) {
529 }
530 return true;
531 }
532
533 Composition Composition::mix(const Composition& other, const double fraction) const {
534 if (!m_finalized || !other.m_finalized) {
535 LOG_ERROR(m_logger, "Compositions have not both been finalized. Hint: Consider running .finalize() on both compositions before mixing.");
536 throw exceptions::CompositionNotFinalizedError("Compositions have not been finalized (Hint: Consider running .finalize() on both compositions before mixing).");
537 }
538
539 if (fraction < 0.0 || fraction > 1.0) {
540 LOG_ERROR(m_logger, "Mixing fraction must be between 0 and 1. Currently it is {}.", fraction);
541 throw exceptions::InvalidCompositionError("Mixing fraction must be between 0 and 1. Currently it is " + std::to_string(fraction) + ".");
542 }
543
544 std::set<std::string> mixedSymbols = other.getRegisteredSymbols();
545 // Get the union of the two sets of symbols to ensure all species are included in the new composition.
546 mixedSymbols.insert(m_registeredSymbols.begin(), m_registeredSymbols.end());
547
548 Composition mixedComposition(mixedSymbols);
549 for (const auto& symbol : mixedSymbols) {
550 double otherMassFrac = 0.0;
551
552 const double thisMassFrac = hasSymbol(symbol) ? getMassFraction(symbol) : 0.0;
553 otherMassFrac = other.hasSymbol(symbol) ? other.getMassFraction(symbol) : 0.0;
554
555 // The mixing formula is a linear interpolation of mass fractions.
556 double massFraction = fraction * thisMassFrac + otherMassFrac * (1-fraction);
557 mixedComposition.setMassFraction(symbol, massFraction);
558 }
559 if (const bool didFinalize = mixedComposition.finalize(); !didFinalize) {
560 std::string msg = "Failed to finalize mixed composition. ";
561 msg += "This likely indicates an issue with the input compositions not summing to 1.\n";
562 LOG_CRITICAL(m_logger, "{}", msg);
564 }
565 return mixedComposition;
566 }
567
568 double Composition::getMassFraction(const std::string& symbol) const {
569 if (!m_finalized) {
570 LOG_ERROR(m_logger, "Composition has not been finalized. Hint: Consider running .finalize().");
571 throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize().");
572 }
573 if (!m_compositions.contains(symbol)) {
574 LOG_ERROR(m_logger, "Symbol {} is not in the composition.", symbol);
575 std::string currentSymbols;
576 size_t count = 0;
577 for (const auto& sym : m_compositions | std::views::keys) {
578 currentSymbols += sym;
579 if (count < m_compositions.size() - 2) {
580 currentSymbols += ", ";
581 } else if (count == m_compositions.size() - 2) {
582 currentSymbols += ", and ";
583 }
584 count++;
585 }
586 throw exceptions::UnregisteredSymbolError("Symbol(" + symbol + ") is not in the current composition. Current composition has symbols: " + currentSymbols + ".");
587 }
588 if (m_massFracMode) {
589 return m_compositions.at(symbol).mass_fraction();
590 }
591
592 return m_compositions.at(symbol).mass_fraction();
593 }
594
596 const atomic::Species &species
597 ) const {
598 return getMassFraction(std::string(species.name()));
599 }
600
601 std::unordered_map<std::string, double> Composition::getMassFraction() const {
602 std::unordered_map<std::string, double> mass_fractions;
603 for (const auto &symbol: m_compositions | std::views::keys) {
604 mass_fractions[symbol] = getMassFraction(symbol);
605 }
606 return mass_fractions;
607 }
608
609
611 const std::string& symbol
612 ) const {
613 if (!m_finalized) {
614 LOG_ERROR(m_logger, "Composition has not been finalized. Hint: Consider running .finalize().");
615 throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize().");
616 }
617 if (!m_compositions.contains(symbol)) {
618 LOG_ERROR(m_logger, "Symbol {} is not in the composition.", symbol);
619 throw exceptions::CompositionNotFinalizedError("Symbol " + symbol + " is not in the composition.");
620 }
621 if (!m_massFracMode) {
622 return m_compositions.at(symbol).number_fraction();
623 }
624 return m_compositions.at(symbol).number_fraction(m_specificNumberDensity);
625 }
626
628 const atomic::Species &species
629 ) const {
630 return getNumberFraction(std::string(species.name()));
631 }
632
633 std::unordered_map<std::string, double> Composition::getNumberFraction() const {
634 std::unordered_map<std::string, double> number_fractions;
635 for (const auto &symbol: m_compositions | std::views::keys) {
636 number_fractions[symbol] = getNumberFraction(symbol);
637 }
638 return number_fractions;
639 }
640
642 const std::string &symbol
643 ) const {
644 if (!m_finalized) {
645 LOG_ERROR(m_logger, "Composition has not been finalized. Hint: Consider running .finalize().");
646 throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize().");
647 }
648 if (!m_compositions.contains(symbol)) {
649 LOG_ERROR(m_logger, "Symbol {} is not in the composition.", symbol);
650 throw exceptions::UnregisteredSymbolError("Symbol " + symbol + " is not in the composition.");
651 }
652 return getMassFraction(symbol) / m_compositions.at(symbol).isotope().mass();
653
654 }
655
657 const atomic::Species &species
658 ) const {
659 return getMolarAbundance(std::string(species.name()));
660 }
661
662 std::pair<CompositionEntry, GlobalComposition> Composition::getComposition(
663 const std::string& symbol
664 ) const {
665 if (!m_finalized) {
666 LOG_ERROR(m_logger, "Composition has not been finalized. Hint: Consider running .finalize().");
667 throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize().");
668 }
669 if (!m_compositions.contains(symbol)) {
670 LOG_ERROR(m_logger, "Symbol {} is not in the composition.", symbol);
671 throw exceptions::UnregisteredSymbolError("Symbol " + symbol + " is not in the composition.");
672 }
674 }
675
676 std::pair<CompositionEntry, GlobalComposition> Composition::getComposition(
677 const atomic::Species &species
678 ) const {
679 return getComposition(std::string(species.name()));
680 }
681
682 std::pair<std::unordered_map<std::string, CompositionEntry>, GlobalComposition> Composition::getComposition() const {
683 if (!m_finalized) {
684 LOG_ERROR(m_logger, "Composition has not been finalized. Hint: Consider running .finalize().");
685 throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize().");
686 }
688 }
689
691 if (!m_finalized) {
692 LOG_ERROR(m_logger, "Composition has not been finalized. Hint: Consider running .finalize().");
693 throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize().");
694 }
695 return m_meanParticleMass;
696 }
697
699 if (!m_finalized) {
700 LOG_ERROR(m_logger, "Composition must be finalized before getting the mean atomic mass number. Hint: Consider running .finalize().");
701 throw exceptions::CompositionNotFinalizedError("Composition not finalized. Cannot retrieve mean atomic mass number. Hint: Consider running .finalize().");
702 }
703
704 double zSum = 0.0;
705
706 for (const auto &val: m_compositions | std::views::values) {
707 // Sum of (X_i * Z_i / A_i)
708 zSum += (val.mass_fraction() * val.m_isotope.z())/val.m_isotope.a();
709 }
710
711 // <Z> = <A> * sum(X_i * Z_i / A_i)
712 const double mean_A = m_meanParticleMass * zSum;
713 return mean_A;
714 }
715
717 if (!m_finalized) {
718 LOG_ERROR(m_logger, "Composition must be finalized before getting the electron abundance. Hint: Consider running .finalize().");
719 throw exceptions::CompositionNotFinalizedError("Composition not finalized. Cannot retrieve electron abundance. Hint: Consider running .finalize().");
720 }
721
722 if (m_cache.Ye.has_value()) {
723 return m_cache.Ye.value();
724 }
725
726 double Ye = 0.0;
727 for (const auto &val: m_compositions | std::views::values) {
728 Ye += (val.mass_fraction() * val.m_isotope.z())/val.m_isotope.a();
729 }
730 m_cache.Ye = Ye;
731 return Ye;
732 }
733
735 const std::vector<std::string>& symbols,
736 const std::string& method
737 ) const {
738 if (const std::array<std::string, 2> methods = {"norm", "none"}; std::ranges::find(methods, method) == methods.end()) {
739 const std::string errorMessage = "Invalid method: " + method + ". Valid methods are 'norm' and 'none'.";
740 LOG_ERROR(m_logger, "Invalid method: {}. Valid methods are norm and none.", method);
741 throw exceptions::InvalidMixingMode(errorMessage);
742 }
743
744 Composition subsetComposition;
745 for (const auto& symbol : symbols) {
746 if (!m_compositions.contains(symbol)) {
747 LOG_ERROR(m_logger, "Symbol {} is not in the composition.", symbol);
748 throw exceptions::UnregisteredSymbolError("Symbol " + symbol + " is not in the composition.");
749 }
750 subsetComposition.registerSymbol(symbol);
751 subsetComposition.setMassFraction(symbol, m_compositions.at(symbol).mass_fraction());
752 }
753 if (method == "norm") {
754 if (const bool isNorm = subsetComposition.finalize(true); !isNorm) {
755 LOG_ERROR(m_logger, "Subset composition is invalid. (Unable to finalize with normalization).");
756 throw exceptions::FailedToFinalizeCompositionError("Subset composition is invalid. (Unable to finalize with normalization).");
757 }
758 }
759 return subsetComposition;
760 }
761
763 const bool massFracMode
764 ) {
765 if (!m_finalized) {
766 LOG_ERROR(m_logger, "Composition has not been finalized. Mode cannot be set unless composition is finalized. Hint: Consider running .finalize().");
767 throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Mode cannot be set unless composition is finalized. Hint: Consider running .finalize().");
768 }
769
770 bool okay;
771 for (auto &entry: m_compositions | std::views::values) {
772 if (massFracMode) {
773 okay = entry.setMassFracMode(m_meanParticleMass);
774 } else {
775 okay = entry.setNumberFracMode(m_specificNumberDensity);
776 }
777 if (!okay) {
778 LOG_ERROR(m_logger, "Composition mode could not be set due to some unknown error.");
779 throw std::runtime_error("Composition mode could not be set due to an unknown error.");
780 }
781 }
782 m_massFracMode = massFracMode;
783 }
784
786 const bool harsh
787 ) const {
788 if (!m_finalized) {
789 LOG_ERROR(m_logger, "Composition has not been finalized. Hint: Consider running .finalize().");
790 throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize().");
791 }
792 if (m_cache.canonicalComp.has_value()) {
793 return m_cache.canonicalComp.value(); // Short circuit if we have cached the canonical composition
794 }
795 CanonicalComposition canonicalComposition;
796 const std::array<std::string, 7> canonicalH = {
797 "H-1", "H-2", "H-3", "H-4", "H-5", "H-6", "H-7"
798 };
799 const std::array<std::string, 8> canonicalHe = {
800 "He-3", "He-4", "He-5", "He-6", "He-7", "He-8", "He-9", "He-10"
801 };
802 for (const auto& symbol : canonicalH) {
803 if (hasSymbol(symbol)) {
804 canonicalComposition.X += getMassFraction(symbol);
805 }
806 }
807 for (const auto& symbol : canonicalHe) {
808 if (hasSymbol(symbol)) {
809 canonicalComposition.Y += getMassFraction(symbol);
810 }
811 }
812
813 for (const auto& symbol : getRegisteredSymbols()) {
814 const bool isHSymbol = std::ranges::find(canonicalH, symbol) != std::end(canonicalH);
815 // ReSharper disable once CppTooWideScopeInitStatement
816 const bool isHeSymbol = std::ranges::find(canonicalHe, symbol) != std::end(canonicalHe);
817
818 if (isHSymbol || isHeSymbol) {
819 continue; // Skip canonical H and He symbols
820 }
821
822 canonicalComposition.Z += getMassFraction(symbol);
823 }
824
825 // ReSharper disable once CppTooWideScopeInitStatement
826 const double Z = 1.0 - (canonicalComposition.X + canonicalComposition.Y);
827 if (std::abs(Z - canonicalComposition.Z) > 1e-6) {
828 if (!harsh) {
829 LOG_WARNING(m_logger, "Validation composition Z (X-Y = {}) is different than canonical composition Z ({}) (∑a_i where a_i != H/He).", Z, canonicalComposition.Z);
830 }
831 else {
832 LOG_ERROR(m_logger, "Validation composition Z (X-Y = {}) is different than canonical composition Z ({}) (∑a_i where a_i != H/He).", Z, canonicalComposition.Z);
833 throw std::runtime_error("Validation composition Z (X-Y = " + std::to_string(Z) + ") is different than canonical composition Z (" + std::to_string(canonicalComposition.Z) + ") (∑a_i where a_i != H/He).");
834 }
835 }
836 m_cache.canonicalComp = canonicalComposition;
837 return canonicalComposition;
838 }
839
840 std::vector<double> Composition::getMassFractionVector() const {
841 if (!m_finalized) {
842 LOG_ERROR(m_logger, "Composition has not been finalized. Hint: Consider running .finalize().");
843 throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize().");
844 }
845 if (m_cache.massFractions.has_value()) {
846 return m_cache.massFractions.value(); // Short circuit if we have cached the mass fractions
847 }
848
849 std::vector<double> massFractionVector;
850 std::vector<double> speciesMass;
851
852 massFractionVector.reserve(m_compositions.size());
853 speciesMass.reserve(m_compositions.size());
854
855 for (const auto &entry: m_compositions | std::views::values) {
856 massFractionVector.push_back(entry.mass_fraction());
857 speciesMass.push_back(entry.isotope().mass());
858 }
859
860 std::vector<double> massFractions = sortVectorBy(massFractionVector, speciesMass);
861 m_cache.massFractions = massFractions; // Cache the result
862 return massFractions;
863
864 }
865
866 std::vector<double> Composition::getNumberFractionVector() const {
867 if (!m_finalized) {
868 LOG_ERROR(m_logger, "Composition has not been finalized. Hint: Consider running .finalize().");
869 throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize().");
870 }
871 if (m_cache.numberFractions.has_value()) {
872 return m_cache.numberFractions.value(); // Short circuit if we have cached the number fractions
873 }
874
875 std::vector<double> numberFractionVector;
876 std::vector<double> speciesMass;
877
878 numberFractionVector.reserve(m_compositions.size());
879 speciesMass.reserve(m_compositions.size());
880
881 for (const auto &entry: m_compositions | std::views::values) {
882 numberFractionVector.push_back(entry.number_fraction());
883 speciesMass.push_back(entry.isotope().mass());
884 }
885
886 std::vector<double> numberFractions = sortVectorBy(numberFractionVector, speciesMass);
887 m_cache.numberFractions = numberFractions; // Cache the result
888 return numberFractions;
889 }
890
891 std::vector<double> Composition::getMolarAbundanceVector() const {
892 if (!m_finalized) {
893 LOG_ERROR(m_logger, "Composition has not been finalized. Hint: Consider running .finalize().");
894 throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize().");
895 }
896 if (m_cache.molarAbundances.has_value()) {
897 return m_cache.molarAbundances.value(); // Short circuit if we have cached the molar abundances
898 }
899
900 std::vector<double> molarAbundanceVector;
901 std::vector<double> speciesMass;
902
903 molarAbundanceVector.reserve(m_compositions.size());
904 speciesMass.reserve(m_compositions.size());
905
906 for (const auto &entry: m_compositions | std::views::values) {
907 molarAbundanceVector.push_back(getMolarAbundance(entry.isotope()));
908 speciesMass.push_back(entry.isotope().mass());
909 }
910
911 std::vector<double> molarAbundances = sortVectorBy(molarAbundanceVector, speciesMass);
912 m_cache.molarAbundances = molarAbundances; // Cache the result
913 return molarAbundances;
914
915 }
916
918 const std::string &symbol
919 ) const {
920 if (!m_finalized) {
921 LOG_ERROR(m_logger, "Composition has not been finalized. Hint: Consider running .finalize().");
922 throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize().");
923 }
924 if (!m_compositions.contains(symbol)) {
925 LOG_ERROR(m_logger, "Symbol {} is not in the composition.", symbol);
926 throw exceptions::UnregisteredSymbolError("Symbol " + symbol + " is not in the composition.");
927 }
928 if (m_cache.sortedSymbols.has_value()) {
929 return std::distance(
930 m_cache.sortedSymbols->begin(),
931 std::ranges::find(
932 m_cache.sortedSymbols.value().begin(),
933 m_cache.sortedSymbols.value().end(),
934 symbol
935 )
936 );
937 }
938
939 std::vector<std::string> symbols;
940 std::vector<double> speciesMass;
941
942 symbols.reserve(m_compositions.size());
943 speciesMass.reserve(m_compositions.size());
944
945 for (const auto &entry: m_compositions | std::views::values) {
946 symbols.emplace_back(entry.isotope().name());
947 speciesMass.push_back(entry.isotope().mass());
948 }
949
950 std::vector<std::string> sortedSymbols = sortVectorBy(symbols, speciesMass);
951 m_cache.sortedSymbols = sortedSymbols;
952 return std::distance(sortedSymbols.begin(), std::ranges::find(sortedSymbols, symbol));
953 }
954
956 const atomic::Species &species
957 ) const {
958 if (!m_finalized) {
959 LOG_ERROR(m_logger, "Composition has not been finalized. Hint: Consider running .finalize().");
960 throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize().");
961 }
962 if (!m_compositions.contains(static_cast<std::string>(species.name()))) {
963 LOG_ERROR(m_logger, "Species {} is not in the composition.", species.name());
964 throw exceptions::UnregisteredSymbolError("Species " + std::string(species.name()) + " is not in the composition.");
965 }
966 if (m_cache.sortedSpecies.has_value()) {
967 return std::distance(
968 m_cache.sortedSpecies->begin(),
969 std::ranges::find(
970 m_cache.sortedSpecies.value().begin(),
971 m_cache.sortedSpecies.value().end(),
972 species
973 )
974 );
975 }
976
977 std::vector<atomic::Species> speciesVector;
978 std::vector<double> speciesMass;
979
980 speciesVector.reserve(m_compositions.size());
981 speciesMass.reserve(m_compositions.size());
982
983 for (const auto &entry: m_compositions | std::views::values) {
984 speciesVector.emplace_back(entry.isotope());
985 speciesMass.push_back(entry.isotope().mass());
986 }
987
988 std::vector<atomic::Species> sortedSpecies = sortVectorBy(speciesVector, speciesMass);
989 m_cache.sortedSpecies = sortedSpecies;
990 return std::distance(sortedSpecies.begin(), std::ranges::find(sortedSpecies, species));
991 }
992
994 size_t index
995 ) const {
996 if (!m_finalized) {
997 LOG_ERROR(m_logger, "Composition has not been finalized. Hint: Consider running .finalize().");
998 throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize().");
999 }
1000 if (index >= m_compositions.size()) {
1001 LOG_ERROR(m_logger, "Index {} is out of bounds for composition of size {}.", index, m_compositions.size());
1002 throw std::out_of_range("Index " + std::to_string(index) + " is out of bounds for composition of size " + std::to_string(m_compositions.size()) + ".");
1003 }
1004 if (m_cache.sortedSpecies.has_value()) {
1005 return m_cache.sortedSpecies.value().at(index);
1006 }
1007
1008 std::vector<atomic::Species> speciesVector;
1009 std::vector<double> speciesMass;
1010
1011 speciesVector.reserve(m_compositions.size());
1012 speciesMass.reserve(m_compositions.size());
1013
1014 for (const auto &entry: m_compositions | std::views::values) {
1015 speciesVector.emplace_back(entry.isotope());
1016 speciesMass.push_back(entry.isotope().mass());
1017 }
1018
1019 std::vector<atomic::Species> sortedSymbols = sortVectorBy(speciesVector, speciesMass);
1020 return sortedSymbols.at(index);
1021 }
1022
1024 const std::string& symbol
1025 ) const {
1026 return m_compositions.contains(symbol);
1027 }
1028
1030 for (const auto &entry: m_compositions | std::views::values) {
1031 if (entry.isotope() == species) {
1032 return true;
1033 }
1034 }
1035 return false;
1036 }
1037
1039 const atomic::Species &isotope
1040 ) const {
1041 // Check if the isotope's symbol is in the composition
1042 if (!m_finalized) {
1043 LOG_ERROR(m_logger, "Composition has not been finalized. Hint: Consider running .finalize().");
1044 throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize().");
1045 }
1046 if (const auto symbol = static_cast<std::string>(isotope.name()); m_compositions.contains(symbol)) {
1047 return true;
1048 }
1049 return false;
1050 }
1051
1053
1055 const Composition& other
1056 ) const {
1057 return mix(other, 0.5);
1058 }
1059
1060 std::ostream& operator<<(
1061 std::ostream& os,
1062 const GlobalComposition& comp
1063 ) {
1064 os << "Global Composition: \n";
1065 os << "\tSpecific Number Density: " << comp.specificNumberDensity << "\n";
1066 os << "\tMean Particle Mass: " << comp.meanParticleMass << "\n";
1067 return os;
1068 }
1069
1070 std::ostream& operator<<(
1071 std::ostream& os,
1072 const CompositionEntry& entry
1073 ) {
1074 os << "<" << entry.m_symbol << " : m_frac = " << entry.mass_fraction() << ">";
1075 return os;
1076 }
1077
1078 std::ostream& operator<<(
1079 std::ostream& os,
1081 ) {
1082 os << "Composition(finalized: " << (composition.m_finalized ? "true" : "false") << ", " ;
1083 size_t count = 0;
1084 for (const auto &entry: composition.m_compositions | std::views::values) {
1085 os << entry;
1086 if (count < composition.m_compositions.size() - 1) {
1087 os << ", ";
1088 }
1089 count++;
1090 }
1091 os << ")";
1092 return os;
1093 }
1094
1095} // namespace fourdst::composition
CompositionCache m_cache
Cache for computed properties to avoid redundant calculations.
void setCompositionMode(bool massFracMode)
Sets the composition mode (mass fraction vs. number fraction).
size_t getSpeciesIndex(const std::string &symbol) const override
get the index in the sorted vector representation for a given symbol
std::pair< std::unordered_map< std::string, CompositionEntry >, GlobalComposition > getComposition() const
Gets all composition entries and the global composition data.
Composition subset(const std::vector< std::string > &symbols, const std::string &method="norm") const
Creates a new Composition object containing a subset of species from this one.
void registerSymbol(const std::string &symbol, bool massFracMode=true)
Registers a new symbol for inclusion in the composition.
Composition()=default
Default constructor.
Composition operator+(const Composition &other) const
Overloads the + operator to mix two compositions with a 50/50 fraction.
std::set< std::string > m_registeredSymbols
The registered symbols.
Composition mix(const Composition &other, double fraction) const
Mixes this composition with another to produce a new composition.
std::set< std::string > getRegisteredSymbols() const override
Gets the registered symbols.
bool finalizeNumberFracMode(bool norm)
Finalizes the composition in number fraction mode.
double setMassFraction(const std::string &symbol, const double &mass_fraction)
Sets the mass fraction for a given symbol.
std::vector< double > getNumberFractionVector() const override
Get a uniform vector representation of the number fractions stored in the composition object sorted s...
double m_meanParticleMass
The mean particle mass of the composition (\sum_{i} \frac{n_i}{m_i}. where n_i is the number fraction...
void registerSpecies(const fourdst::atomic::Species &species, bool massFracMode=true)
Registers a new species by extracting its symbol.
Composition & operator=(Composition const &other)
Assignment operator.
bool hasSpecies(const fourdst::atomic::Species &species) const override
Checks if a species is registered in the composition.
double getElectronAbundance() const override
Compute the electron abundance of the composition.
bool m_massFracMode
True if mass fraction mode, false if number fraction mode.
bool finalize(bool norm=false)
Finalizes the composition, making it ready for querying.
double getMeanParticleMass() const override
Compute the mean particle mass of the composition.
double setNumberFraction(const std::string &symbol, const double &number_fraction)
Sets the number fraction for a given symbol.
bool contains(const atomic::Species &isotope) const override
Checks if a given isotope is present in the composition.
std::vector< double > getMassFractionVector() const override
Get a uniform vector representation of the mass fraction stored in the composition object sorted such...
void validateComposition(const std::vector< double > &fractions) const
Validates the given fractions, throwing an exception on failure.
bool finalizeMassFracMode(bool norm)
Finalizes the composition in mass fraction mode.
static bool isValidSymbol(const std::string &symbol)
Checks if the given symbol is valid by checking against the global species database.
bool m_finalized
True if the composition is finalized.
std::unordered_map< std::string, CompositionEntry > m_compositions
The compositions.
std::unordered_map< std::string, double > getMassFraction() const override
Gets the mass fractions of all species in the composition.
std::vector< double > getMolarAbundanceVector() const override
Get a uniform vector representation of the molar abundances stored in the composition object sorted s...
bool hasSymbol(const std::string &symbol) const override
Checks if a symbol is registered in the composition.
CanonicalComposition getCanonicalComposition(bool harsh=false) const
Gets the current canonical composition (X, Y, Z).
double getMolarAbundance(const std::string &symbol) const override
Gets the molar abundance (X_i / A_i) for a given symbol.
double m_specificNumberDensity
The specific number density of the composition (\sum_{i} X_i m_i. Where X_i is the number fraction of...
bool isValidComposition(const std::vector< double > &fractions) const
Checks if the given fractions are valid (sum to ~1.0).
std::unordered_map< std::string, double > getNumberFraction() const override
Gets the number fractions of all species in the composition.
atomic::Species getSpeciesAtIndex(size_t index) const override
Get the species at a given index in the sorted vector representation.
std::set< fourdst::atomic::Species > getRegisteredSpecies() const override
Get a set of all species that are registered in the composition.
double getMeanAtomicNumber() const override
Compute the mean atomic number of the composition.
Exception thrown due to a conflict in composition modes at the entry level.
Exception thrown when an operation is attempted on a composition that has not been finalized.
Exception thrown when attempting to initialize a composition entry that has already been initialized.
Exception thrown when the finalization process of a composition fails.
Exception thrown when a composition is in an invalid or inconsistent state.
Exception thrown for an invalid or unsupported mixing mode.
Exception thrown for an invalid chemical species symbol in a composition entry.
Exception thrown when a symbol used in a composition is invalid.
Exception thrown when a symbol is used that has not been registered.
Contains classes and functions related to atomic data, such as properties of atomic species.
static const std::unordered_map< std::string, const Species & > species
Map of species names to their corresponding Species objects.
Definition species.h:3579
std::ostream & operator<<(std::ostream &os, const GlobalComposition &comp)
Represents an atomic species (isotope) with its fundamental physical properties.
std::string_view name() const
Gets the name of the species.
Represents the canonical (X, Y, Z) composition of stellar material.
Definition composition.h:44
double Y
Mass fraction of Helium.
Definition composition.h:46
double X
Mass fraction of Hydrogen.
Definition composition.h:45
double Z
Mass fraction of Metals.
Definition composition.h:47
Represents a single entry (an isotope) within a composition.
Definition composition.h:84
bool setNumberFracMode(double totalMolesPerMass)
Switches the mode to number fraction mode.
bool getMassFracMode() const
Gets the mode of the composition entry.
CompositionEntry()
Default constructor. Initializes a default entry (H-1), but in an uninitialized state.
bool m_massFracMode
The mode of the composition entry. True if mass fraction, false if number fraction.
Definition composition.h:87
double number_fraction() const
Gets the number fraction of the species.
bool m_initialized
True if the composition entry has been initialized with a valid species.
Definition composition.h:96
double m_cachedNumberFraction
Cached number fraction for conversions when in mass fraction mode.
Definition composition.h:93
bool setMassFracMode(double meanMolarMass)
Switches the mode to mass fraction mode.
void setMassFraction(double mass_fraction)
Sets the mass fraction of the species.
std::string symbol() const
Gets the chemical symbol of the species.
void setSpecies(const std::string &symbol)
Sets the species for the composition entry. This can only be done once.
double mass_fraction() const
Gets the mass fraction of the species.
atomic::Species m_isotope
The atomic::Species object containing detailed isotope data.
Definition composition.h:86
void setNumberFraction(double number_fraction)
Sets the number fraction of the species.
double rel_abundance() const
Gets the relative abundance of the species.
std::string m_symbol
The chemical symbol of the species (e.g., "H-1", "Fe-56").
Definition composition.h:85
double m_molesPerMass
Definition composition.h:92
atomic::Species isotope() const
Gets the isotope data for the species.
Represents global properties of a finalized composition.
Definition composition.h:70
double specificNumberDensity
The specific number density (moles per unit mass, sum of X_i/M_i), where X_i is mass fraction and M_i...
Definition composition.h:71
double meanParticleMass
The mean mass per particle (inverse of specific number density). Units: g/mol.
Definition composition.h:72