fourdst::libcomposition v1.5.2
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: March 26, 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
37
38namespace {
39 template<typename A, typename B>
40 std::vector<A> sortVectorBy(std::vector<A> toSort, const std::vector<B>& by) {
41 std::vector<std::size_t> indices(by.size());
42 for (size_t i = 0; i < indices.size(); i++) {
43 indices[i] = i;
44 }
45
46 std::ranges::sort(indices, [&](size_t a, size_t b) {
47 return by[a] < by[b];
48 });
49
50 std::vector<A> sorted;
51 sorted.reserve(indices.size());
52
53 for (const auto idx: indices) {
54 sorted.push_back(toSort[idx]);
55 }
56
57 return sorted;
58 }
59}
60
61namespace fourdst::composition {
62
64 m_symbol("H-1"),
65 m_isotope(fourdst::atomic::species.at("H-1")),
66 m_initialized(false) {} // Note: Default entry is uninitialized, must be explicitly set.
67
68 CompositionEntry::CompositionEntry(const std::string& symbol, const bool massFracMode) : m_symbol(symbol), m_isotope(fourdst::atomic::species.at(symbol)), m_massFracMode(massFracMode) {
70 }
71
80
81 void CompositionEntry::setSpecies(const std::string& symbol) {
82 if (m_initialized) {
83 throw exceptions::EntryAlreadyInitializedError("Composition entry is already initialized.");
84 }
85 if (!fourdst::atomic::species.contains(symbol)) {
86 throw exceptions::InvalidSpeciesSymbolError("Invalid symbol.");
87 }
90 m_initialized = true;
91 }
92
93 std::string CompositionEntry::symbol() const {
94 return m_symbol;
95 }
96
98 if (!m_massFracMode) {
99 throw exceptions::CompositionModeError("Composition entry is in number fraction mode.");
100 }
101 return m_massFraction;
102 }
103
104 double CompositionEntry::mass_fraction(const double meanMolarMass) const {
105 if (m_massFracMode) {
106 return m_massFraction;
107 }
108 // Convert from number fraction to mass fraction using: X_i = n_i * A_i / <A>
109 // where m_relAbundance is n_i * A_i and meanMolarMass is <A>.
110 return m_relAbundance / meanMolarMass;
111 }
112
113
115 if (m_massFracMode) {
116 throw exceptions::CompositionModeError("Composition entry is in mass fraction mode.");
117 }
118 return m_numberFraction;
119 }
120
121 double CompositionEntry::number_fraction(const double totalMoles) const {
122 if (m_massFracMode) {
123 // Convert from mass fraction to number fraction using: n_i = (X_i / A_i) / sum(X_j / A_j)
124 // where m_relAbundance is X_i / A_i and totalMoles is sum(X_j / A_j).
125 return m_relAbundance / totalMoles;
126 }
127 return m_numberFraction;
128 }
129
131 return m_relAbundance;
132 }
133
137
139 if (!m_massFracMode) {
140 throw exceptions::CompositionModeError("Composition entry is in number fraction mode.");
141 }
144 }
145
147 if (m_massFracMode) {
148 throw exceptions::CompositionModeError("Composition entry is in mass fraction mode.");
149 }
152 }
153
154 bool CompositionEntry::setMassFracMode(const double meanParticleMass) {
155 if (m_massFracMode) {
156 return false;
157 }
158 m_massFracMode = true;
159 m_massFraction = m_relAbundance / meanParticleMass;
160 return true;
161 }
162
163 bool CompositionEntry::setNumberFracMode(const double specificNumberDensity) {
164 if (!m_massFracMode) {
165 return false;
166 }
167 m_massFracMode = false;
168 m_numberFraction = m_relAbundance / specificNumberDensity;
169 return true;
170 }
171
173 return m_massFracMode;
174 }
175
176 Composition::Composition(const std::vector<std::string>& symbols) {
177 for (const auto& symbol : symbols) {
178 registerSymbol(symbol);
179 }
180 }
181
182 Composition::Composition(const std::set<std::string>& symbols) {
183 for (const auto& symbol : symbols) {
184 registerSymbol(symbol);
185 }
186 }
187
188 Composition::Composition(const std::vector<std::string>& symbols, const std::vector<double>& fractions, const bool massFracMode) : m_massFracMode(massFracMode) {
189 if (symbols.size() != fractions.size()) {
190 LOG_CRITICAL(m_logger, "The number of symbols and fractions must be equal (got {} symbols and {} fractions).", symbols.size(), fractions.size());
191 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.");
192 }
193
194 validateComposition(fractions);
195
196 for (const auto &symbol : symbols) {
197 registerSymbol(symbol);
198 }
199
200 for (size_t i = 0; i < symbols.size(); ++i) {
201 if (m_massFracMode) {
202 setMassFraction(symbols[i], fractions[i]);
203 } else {
204 setNumberFraction(symbols[i], fractions[i]);
205 }
206 }
207 finalize();
208 }
209
211 m_finalized = composition.m_finalized;
212 m_specificNumberDensity = composition.m_specificNumberDensity;
213 m_meanParticleMass = composition.m_meanParticleMass;
214 m_massFracMode = composition.m_massFracMode;
215 m_registeredSymbols = composition.m_registeredSymbols;
216 m_compositions = composition.m_compositions;
217 }
218
220 if (this != &other) {
221 m_finalized = other.m_finalized;
227 // note: m_config remains bound to the same singleton, so we skip it
228 }
229 return *this;
230
231 }
232
233 void Composition::registerSymbol(const std::string& symbol, bool massFracMode) {
234 if (!isValidSymbol(symbol)) {
235 LOG_ERROR(m_logger, "Invalid symbol: {}", symbol);
236 throw exceptions::InvalidSymbolError("Invalid symbol: " + symbol);
237 }
238
239 // If no symbols have been registered allow mode to be set
240 if (m_registeredSymbols.empty()) {
241 m_massFracMode = massFracMode;
242 } else {
243 if (m_massFracMode != massFracMode) {
244 LOG_ERROR(m_logger, "Composition is in mass fraction mode. Cannot register symbol ({}) in number fraction mode.", symbol);
245 throw exceptions::CompositionModeError("Composition is in mass fraction mode. Cannot register symbol (" + symbol + ") in number fraction mode.");
246 }
247 }
248
249 if (m_registeredSymbols.contains(symbol)) {
250 LOG_WARNING(m_logger, "Symbol {} is already registered.", symbol);
251 return;
252 }
253
254 m_registeredSymbols.insert(symbol);
255 const CompositionEntry entry(symbol, m_massFracMode);
256 m_compositions[symbol] = entry;
257 LOG_TRACE_L3(m_logger, "Registered symbol: {}", symbol);
258 }
259
260 void Composition::registerSymbol(const std::vector<std::string>& symbols, bool massFracMode) {
261 for (const auto& symbol : symbols) {
262 registerSymbol(symbol, massFracMode);
263 }
264 }
265
266 void Composition::registerSpecies(const fourdst::atomic::Species &species, bool massFracMode) {
267 registerSymbol(std::string(species.name()));
268 }
269
270 void Composition::registerSpecies(const std::vector<fourdst::atomic::Species> &species, bool massFracMode) {
271 for (const auto& s : species) {
272 registerSpecies(s, massFracMode);
273 }
274 }
275
276 std::set<std::string> Composition::getRegisteredSymbols() const {
277 return m_registeredSymbols;
278 }
279
280 std::set<fourdst::atomic::Species> Composition::getRegisteredSpecies() const {
281 std::set<fourdst::atomic::Species> result;
282 for (const auto& entry : m_compositions | std::views::values) {
283 result.insert(entry.isotope());
284 }
285 return result;
286 }
287
288 void Composition::validateComposition(const std::vector<double>& fractions) const {
289 if (!isValidComposition(fractions)) {
290 LOG_ERROR(m_logger, "Invalid composition.");
291 throw exceptions::InvalidCompositionError("Invalid composition.");
292 }
293 }
294
295 bool Composition::isValidComposition(const std::vector<double>& fractions) const {
296 double sum = 0.0;
297 for (const auto& fraction : fractions) {
298 sum += fraction;
299 }
300 if (sum < 0.999999 || sum > 1.000001) {
301 LOG_ERROR(m_logger, "The sum of fractions must be equal to 1 (expected 1, got {}).", sum);
302 return false;
303 }
304
305 return true;
306 }
307
308 bool Composition::isValidSymbol(const std::string& symbol) {
309 return fourdst::atomic::species.contains(symbol);
310 }
311
312 double Composition::setMassFraction(const std::string& symbol, const double& mass_fraction) {
313 if (!m_registeredSymbols.contains(symbol)) {
314 LOG_ERROR(m_logger, "Symbol {} is not registered.", symbol);
315 throw exceptions::UnregisteredSymbolError("Symbol (" + symbol + ") is not registered.");
316 }
317
318 if (!m_massFracMode) {
319 LOG_ERROR(m_logger, "Composition is in number fraction mode.");
320 throw exceptions::CompositionModeError("Composition is in number fraction mode.");
321 }
322
323 if (mass_fraction < 0.0 || mass_fraction > 1.0) {
324 LOG_ERROR(m_logger, "Mass fraction must be between 0 and 1 for symbol {}. Currently it is {}.", symbol, mass_fraction);
325 throw exceptions::InvalidCompositionError("Mass fraction must be between 0 and 1 for symbol " + symbol + ". Currently it is " + std::to_string(mass_fraction) + ".");
326 }
327
328 m_finalized = false;
329 const double old_mass_fraction = m_compositions.at(symbol).mass_fraction();
330 m_compositions.at(symbol).setMassFraction(mass_fraction);
331
332 return old_mass_fraction;
333 }
334
335 std::vector<double> Composition::setMassFraction(const std::vector<std::string>& symbols, const std::vector<double>& mass_fractions) {
336 if (symbols.size() != mass_fractions.size()) {
337 LOG_ERROR(m_logger, "The number of symbols and mass fractions must be equal (currently {} symbols and {} mass fractions).", symbols.size(), mass_fractions.size());
338 throw exceptions::InvalidCompositionError("The number of symbols and mass fractions must be equal (currently " + std::to_string(symbols.size()) + " symbols and " + std::to_string(mass_fractions.size()) + " mass fractions).");
339 }
340
341 std::vector<double> old_mass_fractions;
342 old_mass_fractions.reserve(symbols.size());
343 for (size_t i = 0; i < symbols.size(); ++i) {
344 old_mass_fractions.push_back(setMassFraction(symbols[i], mass_fractions[i]));
345 }
346 return old_mass_fractions;
347 }
348
349 double Composition::setMassFraction(const fourdst::atomic::Species &species, const double &mass_fraction) {
350 return setMassFraction(std::string(species.name()), mass_fraction);
351 }
352
353 std::vector<double> Composition::setMassFraction(const std::vector<fourdst::atomic::Species> &species,
354 const std::vector<double> &mass_fractions) {
355 std::vector<double> old_mass_fractions;
356 old_mass_fractions.reserve(species.size());
357 for (const auto& spec : species) {
358 old_mass_fractions.push_back(setMassFraction(spec, mass_fractions[&spec - &species[0]]));
359 }
360 return old_mass_fractions;
361 }
362
363 double Composition::setNumberFraction(const std::string& symbol, const double& number_fraction) {
364 if (!m_registeredSymbols.contains(symbol)) {
365 LOG_ERROR(m_logger, "Symbol {} is not registered.", symbol);
366 throw exceptions::UnregisteredSymbolError("Symbol (" + symbol + ") is not registered.");
367 }
368
369 if (m_massFracMode) {
370 LOG_ERROR(m_logger, "Composition is in mass fraction mode, should be in number fraction mode to call setNumberFraction. Hint: The mode can be switched by first finalizing and then calling setCompositionMode(false).");
371 throw exceptions::CompositionModeError("Composition is in mass fraction mode, should be in number fraction mode to call setNumberFraction. Hint: The mode can be switched by first finalizing and then calling setCompositionMode(false).");
372 }
373
374 if (number_fraction < 0.0 || number_fraction > 1.0) {
375 LOG_ERROR(m_logger, "Number fraction must be between 0 and 1 for symbol {}. Currently it is {}.", symbol, number_fraction);
376 throw exceptions::InvalidCompositionError("Number fraction must be between 0 and 1 for symbol " + symbol + ". Currently it is " + std::to_string(number_fraction) + ".");
377 }
378
379 m_finalized = false;
380 const double old_number_fraction = m_compositions.at(symbol).number_fraction();
381 m_compositions.at(symbol).setNumberFraction(number_fraction);
382
383 return old_number_fraction;
384 }
385
386 std::vector<double> Composition::setNumberFraction(const std::vector<std::string>& symbols, const std::vector<double>& number_fractions) {
387 if (symbols.size() != number_fractions.size()) {
388 LOG_ERROR(m_logger, "The number of symbols and number fractions must be equal. (Currently {} symbols and {} number fractions).", symbols.size(), number_fractions.size());
389 throw exceptions::InvalidCompositionError("The number of symbols and number fractions must be equal. (Currently " + std::to_string(symbols.size()) + " symbols and " + std::to_string(number_fractions.size()) + " number fractions).");
390 }
391
392 std::vector<double> old_number_fractions;
393 old_number_fractions.reserve(symbols.size());
394 for (size_t i = 0; i < symbols.size(); ++i) {
395 old_number_fractions.push_back(setNumberFraction(symbols[i], number_fractions[i]));
396 }
397 return old_number_fractions;
398 }
399
400 double Composition::setNumberFraction(const fourdst::atomic::Species &species, const double &number_fraction) {
401 return setNumberFraction(std::string(species.name()), number_fraction);
402 }
403
404 std::vector<double> Composition::setNumberFraction(const std::vector<fourdst::atomic::Species> &species,
405 const std::vector<double> &number_fractions) {
406 std::vector<double> old_number_fractions;
407 old_number_fractions.reserve(species.size());
408 for (const auto& spec : species) {
409 old_number_fractions.push_back(setNumberFraction(spec, number_fractions[&spec - &species[0]]));
410 }
411 return old_number_fractions;
412 }
413
414 bool Composition::finalize(const bool norm) {
415 bool finalized = false;
416 if (m_massFracMode) {
417 finalized = finalizeMassFracMode(norm);
418 } else {
419 finalized = finalizeNumberFracMode(norm);
420 }
421 if (finalized) {
422 m_finalized = true;
423 }
424 return finalized;
425 }
426
428 std::vector<double> mass_fractions;
429 mass_fractions.reserve(m_compositions.size());
430 for (const auto &entry: m_compositions | std::views::values) {
431 mass_fractions.push_back(entry.mass_fraction());
432 }
433 if (norm) {
434 double sum = 0.0;
435 for (const auto& mass_fraction : mass_fractions) {
436 sum += mass_fraction;
437 }
438 for (double & mass_fraction : mass_fractions) {
439 mass_fraction /= sum;
440 }
441 for (auto& [symbol, entry] : m_compositions) {
442 setMassFraction(symbol, entry.mass_fraction() / sum);
443 }
444 }
445 try {
446 validateComposition(mass_fractions);
447 } catch ([[maybe_unused]] const exceptions::InvalidCompositionError& e) {
448 double massSum = 0.0;
449 for (const auto &entry: m_compositions | std::views::values) {
450 massSum += entry.mass_fraction();
451 }
452 LOG_ERROR(m_logger, "Composition is invalid (Total mass {}).", massSum);
453 m_finalized = false;
454 return false;
455 }
456 // After validation, calculate the specific number density (total moles per unit mass).
457 for (const auto &entry: m_compositions | std::views::values) {
458 m_specificNumberDensity += entry.rel_abundance();
459 }
461 return true;
462 }
463
465 std::vector<double> number_fractions;
466 number_fractions.reserve(m_compositions.size());
467 for (const auto &entry: m_compositions | std::views::values) {
468 number_fractions.push_back(entry.number_fraction());
469 }
470 if (norm) {
471 double sum = 0.0;
472 for (const auto& number_fraction : number_fractions) {
473 sum += number_fraction;
474 }
475 for (auto& [symbol, entry] : m_compositions) {
476 setNumberFraction(symbol, entry.number_fraction() / sum);
477 }
478 }
479 try {
480 validateComposition(number_fractions);
481 } catch ([[maybe_unused]] const std::runtime_error& e) {
482 double numberSum = 0.0;
483 for (const auto &entry: m_compositions | std::views::values) {
484 numberSum += entry.number_fraction();
485 }
486 LOG_ERROR(m_logger, "Composition is invalid (Total number {}).", numberSum);
487 m_finalized = false;
488 return false;
489 }
490 // After validation, calculate the mean particle mass.
491 for (const auto &entry: m_compositions | std::views::values) {
492 m_meanParticleMass += entry.rel_abundance();
493 }
495 return true;
496 }
497
498 Composition Composition::mix(const Composition& other, const double fraction) const {
499 if (!m_finalized || !other.m_finalized) {
500 LOG_ERROR(m_logger, "Compositions have not both been finalized. Hint: Consider running .finalize() on both compositions before mixing.");
501 throw exceptions::CompositionNotFinalizedError("Compositions have not been finalized (Hint: Consider running .finalize() on both compositions before mixing).");
502 }
503
504 if (fraction < 0.0 || fraction > 1.0) {
505 LOG_ERROR(m_logger, "Mixing fraction must be between 0 and 1. Currently it is {}.", fraction);
506 throw exceptions::InvalidCompositionError("Mixing fraction must be between 0 and 1. Currently it is " + std::to_string(fraction) + ".");
507 }
508
509 std::set<std::string> mixedSymbols = other.getRegisteredSymbols();
510 // Get the union of the two sets of symbols to ensure all species are included in the new composition.
511 mixedSymbols.insert(m_registeredSymbols.begin(), m_registeredSymbols.end());
512
513 Composition mixedComposition(mixedSymbols);
514 for (const auto& symbol : mixedSymbols) {
515 double otherMassFrac = 0.0;
516
517 const double thisMassFrac = hasSymbol(symbol) ? getMassFraction(symbol) : 0.0;
518 otherMassFrac = other.hasSymbol(symbol) ? other.getMassFraction(symbol) : 0.0;
519
520 // The mixing formula is a linear interpolation of mass fractions.
521 double massFraction = fraction * thisMassFrac + otherMassFrac * (1-fraction);
522 mixedComposition.setMassFraction(symbol, massFraction);
523 }
524 mixedComposition.finalize();
525 return mixedComposition;
526 }
527
528 double Composition::getMassFraction(const std::string& symbol) const {
529 if (!m_finalized) {
530 LOG_ERROR(m_logger, "Composition has not been finalized. Hint: Consider running .finalize().");
531 throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize().");
532 }
533 if (!m_compositions.contains(symbol)) {
534 LOG_ERROR(m_logger, "Symbol {} is not in the composition.", symbol);
535 std::string currentSymbols;
536 size_t count = 0;
537 for (const auto& sym : m_compositions | std::views::keys) {
538 currentSymbols += sym;
539 if (count < m_compositions.size() - 2) {
540 currentSymbols += ", ";
541 } else if (count == m_compositions.size() - 2) {
542 currentSymbols += ", and ";
543 }
544 count++;
545 }
546 throw exceptions::UnregisteredSymbolError("Symbol(" + symbol + ") is not in the current composition. Current composition has symbols: " + currentSymbols + ".");
547 }
548 if (m_massFracMode) {
549 return m_compositions.at(symbol).mass_fraction();
550 } else {
551 return m_compositions.at(symbol).mass_fraction(m_meanParticleMass);
552 }
553 }
554
556 return getMassFraction(std::string(species.name()));
557 }
558
559 std::unordered_map<std::string, double> Composition::getMassFraction() const {
560 std::unordered_map<std::string, double> mass_fractions;
561 for (const auto &symbol: m_compositions | std::views::keys) {
562 mass_fractions[symbol] = getMassFraction(symbol);
563 }
564 return mass_fractions;
565 }
566
567
568 double Composition::getNumberFraction(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 throw exceptions::CompositionNotFinalizedError("Symbol " + symbol + " is not in the composition.");
576 }
577 if (!m_massFracMode) {
578 return m_compositions.at(symbol).number_fraction();
579 } else {
580 return m_compositions.at(symbol).number_fraction(m_specificNumberDensity);
581 }
582 }
583
585 return getNumberFraction(std::string(species.name()));
586 }
587
588 std::unordered_map<std::string, double> Composition::getNumberFraction() const {
589 std::unordered_map<std::string, double> number_fractions;
590 for (const auto &symbol: m_compositions | std::views::keys) {
591 number_fractions[symbol] = getNumberFraction(symbol);
592 }
593 return number_fractions;
594 }
595
596 double Composition::getMolarAbundance(const std::string &symbol) const {
597 if (!m_finalized) {
598 LOG_ERROR(m_logger, "Composition has not been finalized. Hint: Consider running .finalize().");
599 throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize().");
600 }
601 if (!m_compositions.contains(symbol)) {
602 LOG_ERROR(m_logger, "Symbol {} is not in the composition.", symbol);
603 throw exceptions::UnregisteredSymbolError("Symbol " + symbol + " is not in the composition.");
604 }
605 return getMassFraction(symbol) / m_compositions.at(symbol).isotope().mass();
606
607 }
608
610 return getMolarAbundance(std::string(species.name()));
611 }
612
613 std::pair<CompositionEntry, GlobalComposition> Composition::getComposition(const std::string& symbol) const {
614 if (!m_finalized) {
615 LOG_ERROR(m_logger, "Composition has not been finalized. Hint: Consider running .finalize().");
616 throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize().");
617 }
618 if (!m_compositions.contains(symbol)) {
619 LOG_ERROR(m_logger, "Symbol {} is not in the composition.", symbol);
620 throw exceptions::UnregisteredSymbolError("Symbol " + symbol + " is not in the composition.");
621 }
623 }
624
625 std::pair<CompositionEntry, GlobalComposition> Composition::getComposition(
626 const fourdst::atomic::Species &species) const {
627 return getComposition(std::string(species.name()));
628 }
629
630 std::pair<std::unordered_map<std::string, CompositionEntry>, GlobalComposition> Composition::getComposition() const {
631 if (!m_finalized) {
632 LOG_ERROR(m_logger, "Composition has not been finalized. Hint: Consider running .finalize().");
633 throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize().");
634 }
636 }
637
639 if (!m_finalized) {
640 LOG_ERROR(m_logger, "Composition has not been finalized. Hint: Consider running .finalize().");
641 throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize().");
642 }
643 return m_meanParticleMass;
644 }
645
647 if (!m_finalized) {
648 LOG_ERROR(m_logger, "Composition must be finalized before getting the mean atomic mass number. Hint: Consider running .finalize().");
649 throw exceptions::CompositionNotFinalizedError("Composition not finalized. Cannot retrieve mean atomic mass number. Hint: Consider running .finalize().");
650 }
651
652 double zSum = 0.0;
653
654 // Loop through all registered species in the composition.
655 for (const auto &val: m_compositions | std::views::values) {
656 // Sum of (X_i * Z_i / A_i)
657 zSum += (val.mass_fraction() * val.m_isotope.z())/val.m_isotope.a();
658 }
659
660 // Calculate mean atomic number <Z> = <A> * sum(X_i * Z_i / A_i)
661 const double mean_A = m_meanParticleMass * zSum;
662 return mean_A;
663 }
664
665 Composition Composition::subset(const std::vector<std::string>& symbols, const std::string& method) const {
666 const std::array<std::string, 2> methods = {"norm", "none"};
667
668 if (std::ranges::find(methods, method) == methods.end()) {
669 const std::string errorMessage = "Invalid method: " + method + ". Valid methods are 'norm' and 'none'.";
670 LOG_ERROR(m_logger, "Invalid method: {}. Valid methods are norm and none.", method);
671 throw exceptions::InvalidMixingMode(errorMessage);
672 }
673
674 Composition subsetComposition;
675 for (const auto& symbol : symbols) {
676 if (!m_compositions.contains(symbol)) {
677 LOG_ERROR(m_logger, "Symbol {} is not in the composition.", symbol);
678 throw exceptions::UnregisteredSymbolError("Symbol " + symbol + " is not in the composition.");
679 } else {
680 subsetComposition.registerSymbol(symbol);
681 }
682 subsetComposition.setMassFraction(symbol, m_compositions.at(symbol).mass_fraction());
683 }
684 if (method == "norm") {
685 const bool isNorm = subsetComposition.finalize(true);
686 if (!isNorm) {
687 LOG_ERROR(m_logger, "Subset composition is invalid. (Unable to finalize with normalization).");
688 throw exceptions::FailedToFinalizeCompositionError("Subset composition is invalid. (Unable to finalize with normalization).");
689 }
690 }
691 return subsetComposition;
692 }
693
694 void Composition::setCompositionMode(const bool massFracMode) {
695 if (!m_finalized) {
696 LOG_ERROR(m_logger, "Composition has not been finalized. Mode cannot be set unless composition is finalized. Hint: Consider running .finalize().");
697 throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Mode cannot be set unless composition is finalized. Hint: Consider running .finalize().");
698 }
699
700 bool okay = true;
701 for (auto &entry: m_compositions | std::views::values) {
702 if (massFracMode) {
703 okay = entry.setMassFracMode(m_meanParticleMass);
704 } else {
705 okay = entry.setNumberFracMode(m_specificNumberDensity);
706 }
707 if (!okay) {
708 LOG_ERROR(m_logger, "Composition mode could not be set due to some unknown error.");
709 throw std::runtime_error("Composition mode could not be set due to an unknown error.");
710 }
711 }
712 m_massFracMode = massFracMode;
713 }
714
716 if (!m_finalized) {
717 LOG_ERROR(m_logger, "Composition has not been finalized. Hint: Consider running .finalize().");
718 throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize().");
719 }
720 CanonicalComposition canonicalComposition;
721 const std::array<std::string, 7> canonicalH = {
722 "H-1", "H-2", "H-3", "H-4", "H-5", "H-6", "H-7"
723 };
724 const std::array<std::string, 8> canonicalHe = {
725 "He-3", "He-4", "He-5", "He-6", "He-7", "He-8", "He-9", "He-10"
726 };
727 for (const auto& symbol : canonicalH) {
728 if (hasSymbol(symbol)) {
729 canonicalComposition.X += getMassFraction(symbol);
730 }
731 }
732 for (const auto& symbol : canonicalHe) {
733 if (hasSymbol(symbol)) {
734 canonicalComposition.Y += getMassFraction(symbol);
735 }
736 }
737
738 for (const auto& symbol : getRegisteredSymbols()) {
739 const bool isHSymbol = std::ranges::find(canonicalH, symbol) != std::end(canonicalH);
740 const bool isHeSymbol = std::ranges::find(canonicalHe, symbol) != std::end(canonicalHe);
741
742 if (isHSymbol || isHeSymbol) {
743 continue; // Skip canonical H and He symbols
744 }
745
746 canonicalComposition.Z += getMassFraction(symbol);
747 }
748
749 const double Z = 1.0 - (canonicalComposition.X + canonicalComposition.Y);
750 if (std::abs(Z - canonicalComposition.Z) > 1e-6) {
751 if (!harsh) {
752 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);
753 }
754 else {
755 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);
756 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).");
757 }
758 }
759 return canonicalComposition;
760 }
761
762 std::vector<double> Composition::getMassFractionVector() const {
763 if (!m_finalized) {
764 LOG_ERROR(m_logger, "Composition has not been finalized. Hint: Consider running .finalize().");
765 throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize().");
766 }
767
768 std::vector<double> massFractionVector;
769 std::vector<double> speciesMass;
770
771 massFractionVector.reserve(m_compositions.size());
772 speciesMass.reserve(m_compositions.size());
773
774 for (const auto &entry: m_compositions | std::views::values) {
775 massFractionVector.push_back(entry.mass_fraction());
776 speciesMass.push_back(entry.isotope().mass());
777 }
778
779 return sortVectorBy(massFractionVector, speciesMass);
780
781 }
782
783 std::vector<double> Composition::getNumberFractionVector() const {
784 if (!m_finalized) {
785 LOG_ERROR(m_logger, "Composition has not been finalized. Hint: Consider running .finalize().");
786 throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize().");
787 }
788
789 std::vector<double> numberFractionVector;
790 std::vector<double> speciesMass;
791
792 numberFractionVector.reserve(m_compositions.size());
793 speciesMass.reserve(m_compositions.size());
794
795 for (const auto &entry: m_compositions | std::views::values) {
796 numberFractionVector.push_back(entry.number_fraction());
797 speciesMass.push_back(entry.isotope().mass());
798 }
799
800 return sortVectorBy(numberFractionVector, speciesMass);
801 }
802
803 std::vector<double> Composition::getMolarAbundanceVector() const {
804 if (!m_finalized) {
805 LOG_ERROR(m_logger, "Composition has not been finalized. Hint: Consider running .finalize().");
806 throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize().");
807 }
808
809 std::vector<double> molarAbundanceVector;
810 std::vector<double> speciesMass;
811
812 molarAbundanceVector.reserve(m_compositions.size());
813 speciesMass.reserve(m_compositions.size());
814
815 for (const auto &entry: m_compositions | std::views::values) {
816 molarAbundanceVector.push_back(getMolarAbundance(entry.isotope()));
817 speciesMass.push_back(entry.isotope().mass());
818 }
819
820 return sortVectorBy(molarAbundanceVector, speciesMass);
821 }
822
823 size_t Composition::getSpeciesIndex(const std::string &symbol) const {
824 if (!m_finalized) {
825 LOG_ERROR(m_logger, "Composition has not been finalized. Hint: Consider running .finalize().");
826 throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize().");
827 }
828 if (!m_compositions.contains(symbol)) {
829 LOG_ERROR(m_logger, "Symbol {} is not in the composition.", symbol);
830 throw exceptions::UnregisteredSymbolError("Symbol " + symbol + " is not in the composition.");
831 }
832
833 std::vector<std::string> symbols;
834 std::vector<double> speciesMass;
835
836 symbols.reserve(m_compositions.size());
837 speciesMass.reserve(m_compositions.size());
838
839 for (const auto &entry: m_compositions | std::views::values) {
840 symbols.emplace_back(entry.isotope().name());
841 speciesMass.push_back(entry.isotope().mass());
842 }
843
844 std::vector<std::string> sortedSymbols = sortVectorBy(symbols, speciesMass);
845 return std::distance(sortedSymbols.begin(), std::ranges::find(sortedSymbols, symbol));
846 }
847
848 size_t Composition::getSpeciesIndex(const atomic::Species &species) const {
849 if (!m_finalized) {
850 LOG_ERROR(m_logger, "Composition has not been finalized. Hint: Consider running .finalize().");
851 throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize().");
852 }
853 if (!m_compositions.contains(static_cast<std::string>(species.name()))) {
854 LOG_ERROR(m_logger, "Species {} is not in the composition.", species.name());
855 throw exceptions::UnregisteredSymbolError("Species " + std::string(species.name()) + " is not in the composition.");
856 }
857
858 std::vector<atomic::Species> speciesVector;
859 std::vector<double> speciesMass;
860
861 speciesVector.reserve(m_compositions.size());
862 speciesMass.reserve(m_compositions.size());
863
864 for (const auto &entry: m_compositions | std::views::values) {
865 speciesVector.emplace_back(entry.isotope());
866 speciesMass.push_back(entry.isotope().mass());
867 }
868
869 std::vector<atomic::Species> sortedSpecies = sortVectorBy(speciesVector, speciesMass);
870 return std::distance(sortedSpecies.begin(), std::ranges::find(sortedSpecies, species));
871 }
872
874 if (!m_finalized) {
875 LOG_ERROR(m_logger, "Composition has not been finalized. Hint: Consider running .finalize().");
876 throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize().");
877 }
878 if (index >= m_compositions.size()) {
879 LOG_ERROR(m_logger, "Index {} is out of bounds for composition of size {}.", index, m_compositions.size());
880 throw std::out_of_range("Index " + std::to_string(index) + " is out of bounds for composition of size " + std::to_string(m_compositions.size()) + ".");
881 }
882 if (index < 0) {
883 LOG_ERROR(m_logger, "Index {} is negative. Cannot get species at negative index.", index);
884 throw std::out_of_range("Index " + std::to_string(index) + " is negative. Cannot get species at negative index.");
885 }
886
887 std::vector<atomic::Species> speciesVector;
888 std::vector<double> speciesMass;
889
890 speciesVector.reserve(m_compositions.size());
891 speciesMass.reserve(m_compositions.size());
892
893 for (const auto &entry: m_compositions | std::views::values) {
894 speciesVector.emplace_back(entry.isotope());
895 speciesMass.push_back(entry.isotope().mass());
896 }
897
898 std::vector<atomic::Species> sortedSymbols = sortVectorBy(speciesVector, speciesMass);
899 return sortedSymbols.at(index);
900 }
901
902 bool Composition::hasSymbol(const std::string& symbol) const {
903 return m_compositions.contains(symbol);
904 }
905
907 // Check if the isotope's symbol is in the composition
908 if (!m_finalized) {
909 LOG_ERROR(m_logger, "Composition has not been finalized. Hint: Consider running .finalize().");
910 throw exceptions::CompositionNotFinalizedError("Composition has not been finalized. Hint: Consider running .finalize().");
911 }
912 const auto symbol = static_cast<std::string>(isotope.name());
913 if (m_compositions.contains(symbol)) {
914 return true;
915 }
916 return false;
917 }
918
920
922 return mix(other, 0.5);
923 }
924
925 std::ostream& operator<<(std::ostream& os, const GlobalComposition& comp) {
926 os << "Global Composition: \n";
927 os << "\tSpecific Number Density: " << comp.specificNumberDensity << "\n";
928 os << "\tMean Particle Mass: " << comp.meanParticleMass << "\n";
929 return os;
930 }
931
932 std::ostream& operator<<(std::ostream& os, const CompositionEntry& entry) {
933 os << "<" << entry.m_symbol << " : m_frac = " << entry.mass_fraction() << ">";
934 return os;
935 }
936
937 std::ostream& operator<<(std::ostream& os, const Composition& composition) {
938 os << "Composition(finalized: " << (composition.m_finalized ? "true" : "false") << ", " ;
939 size_t count = 0;
940 for (const auto &entry: composition.m_compositions | std::views::values) {
941 os << entry;
942 if (count < composition.m_compositions.size() - 1) {
943 os << ", ";
944 }
945 }
946 os << ")";
947 return os;
948 }
949
950} // namespace fourdst::composition
void setCompositionMode(bool massFracMode)
Sets the composition mode (mass fraction vs. number fraction).
std::pair< std::unordered_map< std::string, CompositionEntry >, GlobalComposition > getComposition() const
Gets all composition entries and the global composition data.
size_t getSpeciesIndex(const std::string &symbol) const
get the index in the sorted vector representation for a given symbol
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::vector< double > getNumberFractionVector() const
Get a uniform vector representation of the number fractions stored in the composition object sorted s...
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< fourdst::atomic::Species > getRegisteredSpecies() const
Get a set of all species that are registered in the composition.
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.
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.
double getMeanParticleMass() const
Compute the mean particle mass of the composition.
bool m_massFracMode
True if mass fraction mode, false if number fraction mode.
double getMolarAbundance(const std::string &symbol) const
Gets the molar abundance (X_i / A_i) for a given symbol.
bool hasSymbol(const std::string &symbol) const
Checks if a symbol is registered in the composition.
bool finalize(bool norm=false)
Finalizes the composition, making it ready for querying.
std::unordered_map< std::string, double > getNumberFraction() const
Gets the number fractions of all species in the composition.
double setNumberFraction(const std::string &symbol, const double &number_fraction)
Sets the number fraction for a given symbol.
std::set< std::string > getRegisteredSymbols() const
Gets the registered symbols.
std::vector< double > getMolarAbundanceVector() const
Get a uniform vector representation of the molar abundances stored in the composition object sorted s...
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.
double getMeanAtomicNumber() const
Compute the mean atomic number of the composition.
bool m_finalized
True if the composition is finalized.
atomic::Species getSpeciesAtIndex(size_t index) const
Get the species at a given index in the sorted vector representation.
std::unordered_map< std::string, CompositionEntry > m_compositions
The compositions.
CanonicalComposition getCanonicalComposition(bool harsh=false) const
Gets the current canonical composition (X, Y, Z).
bool contains(const fourdst::atomic::Species &isotope) const
Checks if a given isotope is present in the composition.
std::vector< double > getMassFractionVector() const
Get a uniform vector representation of the mass fraction stored in the composition object sorted such...
std::unordered_map< std::string, double > getMassFraction() const
Gets the mass fractions of all species in the composition.
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).
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
Definition species.h:3581
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:43
double Y
Mass fraction of Helium.
Definition composition.h:45
double X
Mass fraction of Hydrogen.
Definition composition.h:44
double Z
Mass fraction of Metals.
Definition composition.h:46
Represents a single entry (an isotope) within a composition.
Definition composition.h:83
double m_relAbundance
The relative abundance, used internally for conversions. For mass fraction mode, this is X_i / A_i; f...
Definition composition.h:90
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:86
double m_numberFraction
The number fraction (mole fraction) of the species. Valid only if m_massFracMode is false.
Definition composition.h:89
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:92
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.
bool setNumberFracMode(double totalMoles)
Switches the mode to number fraction mode.
atomic::Species m_isotope
The atomic::Species object containing detailed isotope data.
Definition composition.h:85
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:84
double m_massFraction
The mass fraction of the species. Valid only if m_massFracMode is true.
Definition composition.h:88
atomic::Species isotope() const
Gets the isotope data for the species.
Represents global properties of a finalized composition.
Definition composition.h:69
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:70
double meanParticleMass
The mean mass per particle (inverse of specific number density). Units: g/mol.
Definition composition.h:71