-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathdecay.cc
More file actions
1460 lines (1289 loc) · 67.7 KB
/
Copy pathdecay.cc
File metadata and controls
1460 lines (1289 loc) · 67.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Radioactive decays: nuclide properties, decay paths, time-dependent nuclide abundances
// (Bateman equation solutions), and the energy release rates and decay-time sampling used to
// set up the radioactive energy pellets.
//
// The alpha, beta, and fission decay treatment for kilonovae is described by Shingles et al.
// (2023), ApJ, 954, L41.
#include "decay.h"
#include <algorithm>
#include <array>
#include <cctype>
#include <cmath>
#include <cstddef>
#include <cstdlib>
#include <cstring>
#include <format>
#include <fstream>
#include <functional>
#include <iostream>
#include <iterator>
#include <numbers>
#include <print>
#include <ranges>
#include <set>
#include <span>
#include <sstream>
#include <string>
#include <string_view>
#include <tuple>
#include <vector>
#include "artisoptions.h"
#include "atomic.h"
#include "constants.h"
#include "gammapkt.h"
#include "globals.h"
#include "grid.h"
#include "input.h"
#include "mpi_logging.h"
#include "packet.h"
#include "random.h"
#include "sn3d.h"
namespace decay {
namespace {
constexpr std::array elsymbols{
"n", "H", "He", "Li", "Be", "B", "C", "N", "O", "F", "Ne", "Na", "Mg", "Al", "Si", "P", "S",
"Cl", "Ar", "K", "Ca", "Sc", "Ti", "V", "Cr", "Mn", "Fe", "Co", "Ni", "Cu", "Zn", "Ga", "Ge", "As",
"Se", "Br", "Kr", "Rb", "Sr", "Y", "Zr", "Nb", "Mo", "Tc", "Ru", "Rh", "Pd", "Ag", "Cd", "In", "Sn",
"Sb", "Te", "I", "Xe", "Cs", "Ba", "La", "Ce", "Pr", "Nd", "Pm", "Sm", "Eu", "Gd", "Tb", "Dy", "Ho",
"Er", "Tm", "Yb", "Lu", "Hf", "Ta", "W", "Re", "Os", "Ir", "Pt", "Au", "Hg", "Tl", "Pb", "Bi", "Po",
"At", "Rn", "Fr", "Ra", "Ac", "Th", "Pa", "U", "Np", "Pu", "Am", "Cm", "Bk", "Cf", "Es", "Fm", "Md",
"No", "Lr", "Rf", "Db", "Sg", "Bh", "Hs", "Mt", "Ds", "Rg", "Cn", "Uut", "Fl", "Uup", "Lv", "Uus", "Uuo",
};
constexpr int Z_MAX = elsymbols.size() - 1;
struct DecayDaughter {
int z{-1};
int a{-1};
double probability{0.};
};
struct Nuclide {
int z{-1}; // atomic number
int a{-1}; // mass number
double meanlife{-1}; // mean lifetime before decay [s]
double endecay_electron{0.}; // average energy per beta- decay in kinetic energy of emitted electrons [erg]
double endecay_positron{0.}; // average energy per beta+ decay in kinetic energy of emitted positrons [erg]
double endecay_gamma{0.}; // average energy per decay in gamma rays [erg]
double endecay_alpha{0.}; // average energy per alpha decay in kinetic energy of alpha particles [erg]
double endecay_fission{0.}; // average energy per fission decay in kinetic energy of fission fragments [erg]
std::array<double, DecayType::DECAYTYPE_COUNT> endecay_q = {
0., 0., 0., 0., 0., 0.,
}; // Q-value (reactant minus product energy) for each decay type
std::array<double, DecayType::DECAYTYPE_COUNT> branchprobs = {
0., 0., 0., 0., 0., 0.,
}; // branch probability of each decay type
// (Z, A, yield) of the fission daughters. The value held in DecayDaughter::probability is a yield
// rather than a probability: it is the expected number of these daughter nuclei produced per
// fission, so it sums over the daughters to the number of fragments per fission, not to one.
std::vector<DecayDaughter> fission_daughters_z_a_prob{}; // NOLINT(readability-redundant-member-init)
// expected number of daughter nuclei produced per decay, i.e. the sum of the daughter yields.
// One for the single-daughter decays, and two for the binary fission of the tabulated nuclides
double decay_daughters_probsum{1.};
};
// a decay path follows the contribution from an initial nuclear abundance
// to another (daughter of last nuclide in decaypath) via decays
// every different path within the network is considered, e.g. 56Ni -> 56Co -> 56Fe is separate to 56Ni -> 56Co
struct DecayPath {
std::vector<int> z; // atomic number
std::vector<int> a; // mass number
std::vector<int> nucindex; // index into nuclides list
std::vector<DecayType> decaytypes;
std::vector<double> lambdas;
double branchproduct{
0.,
}; // product of all branching factors along the path, accumulated as the path is built
};
std::vector<Nuclide> nuclides;
std::vector<DecayPath> decaypaths;
std::vector<int> decaypath_topnucindex; // the chain-top nuclide index of each decaypath (flat copy for fast access)
std::vector<int> decaypath_endnucindex; // the chain-end nuclide index of each decaypath (flat copy for fast access)
int he4_nucindex = -1; // the nuclide index of He4 (or -1 if not in the network), the alpha-decay byproduct
struct ZIsotope {
int a; // mass number
int nucindex;
};
// for each atomic number, the (mass number, nuclide index) pairs of that element's nuclides sorted by mass number
std::vector<std::vector<ZIsotope>> isotopes_of_z;
std::vector<bool> alldecaytypes_is_used;
[[nodiscard]] constexpr auto decay_daughters_z_a_prob(const int z_parent, const int a_parent, const DecayType decaytype)
-> std::vector<DecayDaughter> {
assert_always(decaytype >= 0);
assert_always(decaytype < DecayType::DECAYTYPE_COUNT);
switch (static_cast<enum DecayType>(decaytype)) {
case DecayType::DECAYTYPE_ALPHA: {
return {
DecayDaughter{
.z = z_parent - 2,
.a = a_parent - 4,
.probability = 1.,
},
}; // lose two protons and two neutrons (He4 handled separately)
}
case DecayType::DECAYTYPE_BETAPLUS:
case DecayType::DECAYTYPE_ELECTRONCAPTURE: {
return {DecayDaughter{.z = z_parent - 1, .a = a_parent, .probability = 1.}}; // lose a proton, gain a neutron
}
case DecayType::DECAYTYPE_BETAMINUS: {
return {DecayDaughter{.z = z_parent + 1, .a = a_parent, .probability = 1.}}; // lose a neutron, gain a proton
}
case DecayType::DECAYTYPE_SPONTFISSION: {
const auto nucindex = get_nucindex(z_parent, a_parent);
assert_always(!nuclides[nucindex].fission_daughters_z_a_prob.empty());
return nuclides[nucindex].fission_daughters_z_a_prob;
}
case DecayType::DECAYTYPE_NONE: {
return {DecayDaughter{.z = -1, .a = -1, .probability = 0.}}; // no daughter
}
case DecayType::DECAYTYPE_COUNT: {
assert_always(false);
}
}
return {DecayDaughter{.z = -1, .a = -1, .probability = 0.}}; // no daughter
}
// Get the probability that a decay of decaytype occurs
[[nodiscard]] auto get_nuc_decaybranchprob(const int nucindex, const DecayType decaytype) -> double {
assert_testmodeonly(nucindex >= 0);
assert_testmodeonly(nucindex < std::ssize(nuclides));
assert_testmodeonly(decaytype >= 0);
assert_testmodeonly(decaytype < DecayType::DECAYTYPE_COUNT);
return nuclides[nucindex].branchprobs[decaytype];
}
[[nodiscard]] auto nucmass(int nucindex) -> double { return get_nuc_a(nucindex) * MH; }
// get the nuclide array index from the atomic number and mass number
[[nodiscard]] auto get_nucindex_or_neg_one(const int z, const int a) -> int {
assert_testmodeonly(std::ssize(nuclides) > 0);
const auto it = std::ranges::find_if(nuclides, [z, a](const auto& nuc) { return nuc.z == z && nuc.a == a; });
return it != nuclides.end() ? static_cast<int>(std::ranges::distance(nuclides.begin(), it)) : -1;
}
[[nodiscard]] auto get_meanlife(const int nucindex) -> double {
assert_testmodeonly(nucindex >= 0);
assert_testmodeonly(nucindex < std::ssize(nuclides));
return nuclides[nucindex].meanlife;
}
void printout_nuclidename(const int z, const int a) { printlog("(Z={}){}{}", z, get_elname(z), a); }
void printout_nuclidemeanlife(const int z, const int a) {
const int nucindex = get_nucindex(z, a);
if (get_meanlife(nucindex) > 0.) {
printlog("[tau {:.1e}s]", get_meanlife(nucindex));
} else {
printlog("[stable]");
}
}
// decay energy in the form of kinetic energy of electrons, positrons, or alpha particles,
// depending on the relevant decay type (but not including neutrinos)
[[nodiscard]] auto nucdecayenergyparticle(const int nucindex, const DecayType decaytype) -> double {
assert_testmodeonly(decaytype >= 0);
assert_testmodeonly(decaytype < DecayType::DECAYTYPE_COUNT);
switch (decaytype) {
case DecayType::DECAYTYPE_ALPHA: {
return nuclides[nucindex].endecay_alpha;
}
case DecayType::DECAYTYPE_BETAPLUS: {
return nuclides[nucindex].endecay_positron;
}
case DecayType::DECAYTYPE_ELECTRONCAPTURE: {
return 0.;
}
case DecayType::DECAYTYPE_BETAMINUS: {
return nuclides[nucindex].endecay_electron;
}
case DecayType::DECAYTYPE_SPONTFISSION: {
return nuclides[nucindex].endecay_fission;
}
case DecayType::DECAYTYPE_NONE: {
return 0.;
}
default: {
assert_always(false);
return 0.;
}
}
}
// contributed energy release per decay [erg] for decaytype (e.g. decaytypes::DECAYTYPE_BETAPLUS) (excludes neutrinos!)
[[nodiscard]] auto nucdecayenergy(const int nucindex, const DecayType decaytype) -> double {
const double endecay = nuclides[nucindex].endecay_gamma + nucdecayenergyparticle(nucindex, decaytype);
return endecay;
}
[[nodiscard]] auto nucdecayenergyqval(const int nucindex, const DecayType decaytype) -> double {
return nuclides[nucindex].endecay_q[decaytype];
}
[[nodiscard]] auto get_num_decaypaths() -> int { return static_cast<int>(decaypaths.size()); }
// a decaypath's energy is the decay energy of the last nuclide and decaytype in the chain
[[nodiscard]] auto get_decaypath_lastdecayenergy(const DecayPath& decaypath) -> double {
const auto secondlastindex = decaypath.nucindex.size() - 2;
assert_testmodeonly(decaypath.decaytypes[secondlastindex] != DecayType::DECAYTYPE_NONE);
assert_testmodeonly(decaypath.lambdas[secondlastindex] > 0.);
assert_testmodeonly(decaypath.decaytypes.back() == DecayType::DECAYTYPE_NONE);
// Normalize decay energy by decay_daughters_probsum to properly distribute energy among probabilistic fission product
// outcomes. This avoids multiply counting the decay energy, since each daughter product will have it's own
// separate decaypath that differs only in the last nuclide.
const auto nucindex = decaypath.nucindex[secondlastindex];
return nucdecayenergy(nucindex, decaypath.decaytypes[secondlastindex]) / nuclides[nucindex].decay_daughters_probsum;
}
[[nodiscard]] auto get_str_decaytype(const DecayType decaytype) -> std::string_view {
switch (decaytype) {
case DecayType::DECAYTYPE_ALPHA: {
return "alpha";
}
case DecayType::DECAYTYPE_BETAPLUS: {
return "beta+";
}
case DecayType::DECAYTYPE_ELECTRONCAPTURE: {
return "ec";
}
case DecayType::DECAYTYPE_BETAMINUS: {
return "beta-";
}
case DecayType::DECAYTYPE_SPONTFISSION: {
return "sf";
}
case DecayType::DECAYTYPE_NONE: {
return "none";
}
default:
return "unknown";
}
}
void printout_decaypath(const int decaypathindex) {
assert_always(!decaypaths.empty());
const auto& decaypath = decaypaths[decaypathindex];
printlog(" decaypath {}: ", decaypathindex);
for (const auto [decaytype, z, a] : std::views::zip(decaypath.decaytypes, decaypath.z, decaypath.a)) {
printout_nuclidename(z, a);
printout_nuclidemeanlife(z, a);
if (decaytype != DecayType::DECAYTYPE_NONE) {
printlog(" -> {} -> ", get_str_decaytype(decaytype));
}
}
printlnlog("");
}
// The Bateman solution in calculate_decaychain() is singular when two nuclides in a chain have exactly
// equal decay constants. Rather than failing an assertion deep inside packet setup, detect this
// input-data problem up front and report the offending chain.
void abort_if_decaypath_has_duplicate_lambdas() {
for (int decaypathindex = 0; decaypathindex < get_num_decaypaths(); decaypathindex++) {
const auto& lambdas = decaypaths[decaypathindex].lambdas;
for (auto i = 0Z; i < std::ssize(lambdas); i++) {
for (auto j = i + 1; j < std::ssize(lambdas); j++) {
if (lambdas[i] > 0. && lambdas[i] == lambdas[j]) {
printout_decaypath(decaypathindex);
fatal_crash(
"decay path contains two nuclides with identical decay constants {:g} [1/s], which makes the "
"Bateman solution singular. Slightly perturb one of the mean lifetimes in the decay data to break the "
"degeneracy.",
lambdas[i]);
}
}
}
}
}
// follow decays at the ends of the current list of decaypaths to get decaypaths from all descendants
void extend_lastdecaypath(std::vector<DecayPath>& localdecaypaths) {
const auto initial_last_decaypath = localdecaypaths.back();
const int end_nucindex = initial_last_decaypath.nucindex.back();
if ((get_meanlife(end_nucindex) <= 0.)) {
// daughter is stable: no extension possible
return;
}
const int prev_end_z = initial_last_decaypath.z.back();
const int prev_end_a = initial_last_decaypath.a.back();
for (const auto decaytypeindex : all_decaytypes) {
if (get_nuc_decaybranchprob(end_nucindex, decaytypeindex) == 0.) {
continue;
}
for (const auto& daughter : decay_daughters_z_a_prob(prev_end_z, prev_end_a, decaytypeindex)) {
// check for nuclide in existing path, which would indicate a loop
for (const auto [z, a] : std::views::zip(initial_last_decaypath.z, initial_last_decaypath.a)) {
if (z == daughter.z && a == daughter.a) {
std::string chain;
for (const auto [chain_z, chain_a] : std::views::zip(initial_last_decaypath.z, initial_last_decaypath.a)) {
chain += std::format("(Z={},A={}) -> ", chain_z, chain_a);
}
fatal_crash("loop in nuclear decay chain: {}(Z={},A={}) already occurred", chain, daughter.z, daughter.a);
}
}
const auto daughter_nucindex = get_nucindex(daughter.z, daughter.a);
auto newdecaypath = initial_last_decaypath;
newdecaypath.z.push_back(daughter.z);
newdecaypath.a.push_back(daughter.a);
newdecaypath.nucindex.push_back(daughter_nucindex);
newdecaypath.decaytypes.back() = decaytypeindex; // replace the DECAYTYPE_NONE at end with this decay type
newdecaypath.decaytypes.push_back(DecayType::DECAYTYPE_NONE); // add new DECAYTYPE_NONE at end
newdecaypath.branchproduct *= get_nuc_decaybranchprob(end_nucindex, decaytypeindex) * daughter.probability;
localdecaypaths.push_back(newdecaypath);
extend_lastdecaypath(localdecaypaths);
}
}
}
auto find_decaypaths(const std::span<const int> custom_zlist, const std::span<const int> custom_alist,
const std::vector<Nuclide>& standard_nuclides) -> std::vector<DecayPath> {
std::vector<DecayPath> localdecaypaths;
for (int startnucindex = 0; startnucindex < std::ssize(nuclides); startnucindex++) {
if (get_meanlife(startnucindex) <= 0.) {
continue; // skip stable nuclides as start points
}
const int z = get_nuc_z(startnucindex);
const int a = get_nuc_a(startnucindex);
for (const auto decaytype : all_decaytypes) {
if (get_nuc_decaybranchprob(startnucindex, decaytype) == 0.) {
continue;
}
bool is_custom_nuclide = false;
for (auto i = 0Z; i < std::ssize(custom_zlist); i++) {
if ((z == custom_zlist[i]) && (a == custom_alist[i])) {
is_custom_nuclide = true;
break;
}
}
// skip path if it doesn't start from a nuclide in the custom or standard input lists
// i.e. the first nuclide will have zero initial abundance anyway
if (!is_custom_nuclide && !std::ranges::any_of(standard_nuclides, [z, a](const auto& stdnuc) {
return (z == stdnuc.z) && (a == stdnuc.a);
})) {
continue;
}
for (const auto& daughter : decay_daughters_z_a_prob(z, a, decaytype)) {
localdecaypaths.push_back({
.z = {z, daughter.z},
.a = {a, daughter.a},
.nucindex = {startnucindex, get_nucindex(daughter.z, daughter.a)},
.decaytypes = {decaytype, DecayType::DECAYTYPE_NONE},
.lambdas = {},
.branchproduct = get_nuc_decaybranchprob(startnucindex, decaytype) * daughter.probability,
});
extend_lastdecaypath(localdecaypaths); // take this single step chain and find all descendants
}
}
}
std::ranges::SORT_OR_STABLE_SORT(localdecaypaths, [](const DecayPath& d1, const DecayPath& d2) {
// true if d1 < d2
// chains are sorted by mass number, then atomic number, then length
const auto d1_length = std::ssize(d1.z);
const auto d2_length = std::ssize(d2.z);
// -1 to ignore last item, which keeps bit-identical results as before when final daughter nuclide was not included
// TODO: it would probably be better to sort by all items in reverse order
const auto smallestpathlength = std::min(d1_length, d2_length) - 1;
for (auto i = 0Z; i < smallestpathlength; i++) {
if (d1.a[i] != d2.a[i]) {
return d1.a[i] < d2.a[i];
}
if (d1.z[i] != d2.z[i]) {
return d1.z[i] < d2.z[i];
}
}
// one is an extension of the other, so place the shorter one first
return d1_length < d2_length;
});
for (auto& decaypath : localdecaypaths) {
// all nuclei in the path (except for the last one, which is allowed to be stable) must have a mean life >0
assert_always(std::all_of(decaypath.nucindex.cbegin(), decaypath.nucindex.cend() - 1,
[](const auto nucindex) { return get_meanlife(nucindex) > 0.; }));
assert_always(decaypath.decaytypes.back() == DecayType::DECAYTYPE_NONE);
// convert mean lifetimes to decay constants
decaypath.lambdas.resize(decaypath.nucindex.size(), -1.);
std::ranges::transform(decaypath.nucindex, decaypath.lambdas.begin(), [](const auto nucindex) {
const double meanlife = get_meanlife(nucindex);
// last nuclide might be stable (meanlife <= 0.)
const double lambda = (meanlife > 0.) ? 1. / meanlife : 0.;
return lambda;
});
}
localdecaypaths.shrink_to_fit();
return localdecaypaths;
}
// remove nuclides that are not a standard or custom input-specified nuclide, or connected to these by decays
// true if any decay path takes an alpha decay step. The alpha decay steps produce He4 outside of the
// decay paths (see decay_daughters_z_a_prob), so the network then needs the He4 nuclide.
[[nodiscard]] auto decaypaths_produce_he4() -> bool {
return std::ranges::any_of(decaypaths, [](const auto& decaypath) {
return std::ranges::contains(decaypath.decaytypes, DecayType::DECAYTYPE_ALPHA);
});
}
void filter_unused_nuclides(const std::span<const int> custom_zlist, const std::span<const int> custom_alist,
const std::vector<Nuclide>& standard_nuclides) {
const bool keep_he4 = decaypaths_produce_he4();
std::erase_if(nuclides, [&](const auto& nuc) {
// keep He4 if the decay paths produce it, because it is in no decay path itself
if (keep_he4 && nuc.z == 2 && nuc.a == 4) {
return false;
}
// keep nucleus if it is in the standard list
if (std::ranges::any_of(standard_nuclides,
[&](const auto& stdnuc) { return (stdnuc.z == nuc.z) && (stdnuc.a == nuc.a); })) {
return false;
}
// keep nucleus if it is in the custom list
for (const auto [z, a] : std::views::zip(custom_zlist, custom_alist)) {
if ((z == nuc.z) && (a == nuc.a)) {
return false;
}
}
const bool in_any_decaypath = std::ranges::any_of(decaypaths, [&nuc](const auto& decaypath) {
for (const auto [z, a] : std::views::zip(decaypath.z, decaypath.a)) {
if (z == nuc.z && a == nuc.a) {
// nuc is in the decay path
return true;
}
}
// return true if nuc is the final daughter of a decay path
return (decaypath.z.back() == nuc.z && decaypath.a.back() == nuc.a);
});
return !in_any_decaypath;
});
nuclides.shrink_to_fit();
// update the nuclide indices in the decay paths after we possibly removed some nuclides
for (auto& decaypath : decaypaths) {
std::ranges::transform(decaypath.z, decaypath.a, decaypath.nucindex.begin(),
[](const auto z, const auto a) { return get_nucindex(z, a); });
}
}
auto sample_decaytime(const int decaypathindex, const double tdecaymin, const double tdecaymax, rngstate_type& rngstate)
-> double {
double tdecay = -1;
const double t_model = grid::get_t_model();
// rejection method. draw random times with the right distribution until they are within the correct range.
while (tdecay <= tdecaymin || tdecay >= tdecaymax) {
tdecay = t_model; // can't decay before initial model snapshot time
const auto maxlength = std::ssize(decaypaths[decaypathindex].nucindex) - 1;
for (auto i = 0Z; i < maxlength; i++) {
tdecay -= get_meanlife(decaypaths[decaypathindex].nucindex[i]) *
std::log(static_cast<double>(rng_uniform_pos(rngstate)));
}
}
return tdecay;
}
// the chain-end abundance per unit chain-top initial abundance for one decaypath at the given time, with the
// chain end treated as stable (so it counts the fraction of chain-top nuclei that decayed past the chain end)
auto calc_decaypath_unitfactor(const int decaypathindex, const double time, const bool useexpansionfactor) -> double {
auto lambdas = decaypaths[decaypathindex].lambdas;
lambdas[lambdas.size() - 1] = 0.;
return calculate_decaychain(1., lambdas, time - grid::get_t_model(), useexpansionfactor);
}
// Get the total decay power per mass [erg/s/g] for a given decaypath
// We only count the power from the last decay in the chain to avoid double counting of decay energy (all sub paths are
// handled separately)
[[nodiscard]] auto get_decaypath_power_per_ejectamass(const int decaypathindex, const int nonemptymgi,
const double time) -> double {
// only decays at the end of the chain contributed from the initial abundance of the top of the chain are counted
// (these can be can be same for a chain of length one)
const auto& decaypath = decaypaths[decaypathindex];
const int nucindex_top = decaypath.nucindex[0];
const int modelgridindex = grid::get_mgi_of_nonemptymgi(nonemptymgi);
const double top_initabund = grid::get_modelinitnucmassfrac(modelgridindex, nucindex_top) / nucmass(nucindex_top);
if (top_initabund <= 0.) {
return 0.;
}
const double t_afterinit = time - grid::get_t_model();
const auto lambdas = std::span{decaypath.lambdas}.first(decaypath.lambdas.size() - 1); // exclude the decay daughter
const double endecay = get_decaypath_lastdecayenergy(decaypath);
// contribution to the end nuclide abundance from the top of chain (could be a length-one chain
// Z,A_top = Z,A_end so contribution would be from init abundance only)
const double decayingnucabund =
decaypath.branchproduct * calculate_decaychain(top_initabund, lambdas, t_afterinit, false);
const int lastdecay_nucindex = decaypath.nucindex[decaypath.nucindex.size() - 2];
const double decaypower = endecay * decayingnucabund / get_meanlife(lastdecay_nucindex);
assert_always(decaypower >= 0.);
assert_always(std::isfinite(decaypower));
return decaypower;
}
auto write_nuclides_list() {
auto nuclides_file = fstream_required("nuclides.out", std::ofstream::out | std::ofstream::trunc);
std::println(nuclides_file, "#nucindex Z A");
for (int nucindex = 0; nucindex < std::ssize(nuclides); nucindex++) {
std::println(nuclides_file, "{} {} {}", nucindex, get_nuc_z(nucindex), get_nuc_a(nucindex));
}
}
} // anonymous namespace
[[gnu::pure]] [[nodiscard]] auto get_num_nuclides() -> ptrdiff_t { return std::ssize(nuclides); }
[[nodiscard]] auto get_elname(const int z) -> std::string {
assert_testmodeonly(z <= Z_MAX);
return elsymbols.at(z);
}
[[nodiscard]] auto get_nuc_z(const int nucindex) -> int {
assert_testmodeonly(nucindex >= 0);
assert_testmodeonly(nucindex < std::ssize(nuclides));
return nuclides[nucindex].z;
}
[[nodiscard]] auto get_nuc_a(const int nucindex) -> int {
assert_testmodeonly(nucindex >= 0);
assert_testmodeonly(nucindex < std::ssize(nuclides));
return nuclides[nucindex].a;
}
// get the nuclide array index from the atomic number and mass number
[[nodiscard]] auto get_nucindex(const int z, const int a) -> int {
const int nucindex = get_nucindex_or_neg_one(z, a);
if (nucindex >= 0) {
return nucindex;
}
printlnlog("[error] nuclide Z={} A={} not found in nuclide list", z, a);
assert_always(false); // nuclide not found
return -1;
}
// check if nuclide exists in the simulation
[[nodiscard]] auto nuc_exists(const int z, const int a) -> bool { return get_nucindex_or_neg_one(z, a) >= 0; }
// average energy per decay in the form of gamma rays [erg]
[[nodiscard]] auto nucdecayenergygamma(const int nucindex) -> double { return nuclides[nucindex].endecay_gamma; }
[[nodiscard]] auto nucdecayenergygamma(const int z, const int a) -> double {
return nucdecayenergygamma(get_nucindex(z, a));
}
// set average energy per decay in the form of gamma rays [erg]
void set_nucdecayenergygamma(const int nucindex, const double value) { nuclides[nucindex].endecay_gamma = value; }
// convert something like Ni56 to integer 28
auto get_nucstring_z(const std::string& strnuc) -> int {
std::string elcode = strnuc;
std::erase_if(elcode, &isdigit);
for (int z = 0; z <= Z_MAX; z++) {
if (elcode == get_elname(z)) {
return z;
}
}
assert_always(false); // could not match to an element
return -1;
}
// convert something like Ni56 to integer 56
auto get_nucstring_a(const std::string& strnuc) -> int {
// find first digit character
auto i = 0ZU;
for (; i < strnuc.length(); i++) {
if (isdigit(strnuc[i]) != 0) {
break;
}
}
// remove the non-digit charts
const std::string strmassnum = strnuc.substr(i);
const int a = std::atoi(strmassnum.c_str());
assert_always(a > 0);
return a;
}
namespace {
// add the standard set of nuclides for the classical double-decay chains (56Ni, 57Ni, 48Cr,
// and 52Fe parents) with hardcoded decay data
void add_standard_nuclides() {
// Ni57
nuclides.push_back({.z = 28, .a = 57, .meanlife = 51.36 * HOUR});
nuclides.back().endecay_positron = 0.354 * MEV;
nuclides.back().branchprobs[DECAYTYPE_BETAPLUS] = 0.436;
nuclides.back().branchprobs[DECAYTYPE_ELECTRONCAPTURE] = 1. - 0.436;
const double ni57_q_ec = 3.261697 * MEV;
nuclides.back().endecay_q[DECAYTYPE_ELECTRONCAPTURE] = ni57_q_ec;
nuclides.back().endecay_q[DECAYTYPE_BETAPLUS] = ni57_q_ec - (2. * ME * CLIGHTSQUARED);
// Ni56
nuclides.push_back({.z = 28, .a = 56, .meanlife = 8.80 * DAY});
nuclides.back().branchprobs[DECAYTYPE_ELECTRONCAPTURE] = 1.;
nuclides.back().endecay_q[DECAYTYPE_ELECTRONCAPTURE] = 2.132869 * MEV;
// Co56
nuclides.push_back({.z = 27, .a = 56, .meanlife = 113.7 * DAY});
nuclides.back().endecay_positron = 0.63 * MEV;
nuclides.back().branchprobs[DECAYTYPE_BETAPLUS] = 0.19;
nuclides.back().branchprobs[DECAYTYPE_ELECTRONCAPTURE] = 1 - 0.19;
const double co56_q_ec = 4.566645 * MEV;
nuclides.back().endecay_q[DECAYTYPE_ELECTRONCAPTURE] = co56_q_ec;
nuclides.back().endecay_q[DECAYTYPE_BETAPLUS] = co56_q_ec - (2. * ME * CLIGHTSQUARED);
// Cr48
nuclides.push_back({.z = 24, .a = 48, .meanlife = 1.29602 * DAY});
nuclides.back().branchprobs[DECAYTYPE_ELECTRONCAPTURE] = 1.;
nuclides.back().endecay_q[DECAYTYPE_ELECTRONCAPTURE] = 1.656692 * MEV;
// V48
nuclides.push_back({.z = 23, .a = 48, .meanlife = 23.0442 * DAY});
nuclides.back().branchprobs[DECAYTYPE_BETAPLUS] = 0.499;
nuclides.back().branchprobs[DECAYTYPE_ELECTRONCAPTURE] = 1. - 0.499;
nuclides.back().endecay_positron = 0.290 * MEV;
const double v48_q_ec = 4.014947 * MEV;
nuclides.back().endecay_q[DECAYTYPE_ELECTRONCAPTURE] = v48_q_ec;
nuclides.back().endecay_q[DECAYTYPE_BETAPLUS] = v48_q_ec - (2. * ME * CLIGHTSQUARED);
// Co57
nuclides.push_back({.z = 27, .a = 57, .meanlife = 392.03 * DAY});
nuclides.back().branchprobs[DECAYTYPE_ELECTRONCAPTURE] = 1.;
nuclides.back().endecay_q[DECAYTYPE_ELECTRONCAPTURE] = 0.836359 * MEV;
// Fe52
nuclides.push_back({.z = 26, .a = 52, .meanlife = 0.497429 * DAY});
nuclides.back().branchprobs[DECAYTYPE_ELECTRONCAPTURE] = 1.;
nuclides.back().endecay_q[DECAYTYPE_ELECTRONCAPTURE] = 2.001543 * MEV;
// Mn52
nuclides.push_back({.z = 25, .a = 52, .meanlife = 0.0211395 * DAY});
nuclides.back().branchprobs[DECAYTYPE_ELECTRONCAPTURE] = 1.;
nuclides.back().endecay_q[DECAYTYPE_ELECTRONCAPTURE] = 5.085870 * MEV;
}
// add the nuclides with beta-minus decay data from betaminusdecays.txt
void read_betaminus_decaydata() {
auto fbetaminus = fstream_required("betaminusdecays.txt", std::ios::in);
std::string line;
while (get_noncommentline(fbetaminus, line)) {
// energies are averages per decay of the parent nuclide, summed over its decay branches, as
// are the ENDF MF=8/MT=457 average decay energies they are taken from. Every nuclide added
// here is given a beta-minus branching ratio of one, so the two conventions coincide unless
// read_alpha_decaydata() later gives the nuclide a fractional branch, where it divides the
// ratio back out. E_gamma covers X-rays as well as gamma rays, so it is not zero merely because
// ENDF tabulates no spectrum for the nuclide. E_elec likewise covers conversion and Auger
// electrons, but only where the branching ratio stays one: a nuclide that alphadecays.txt gives
// a fractional beta-minus branch carries the beta continuum alone, because ELP sums the discrete
// electrons over every branch and there is no branch to divide that by.
// columns: # A, Z, Q[MeV], E_gamma[MeV], E_elec[MeV], E_neutrino[MeV], meanlife[s]
int a = -1;
int z = -1;
double Q_betadecay_mev = 0.;
double e_gamma_mev = 0.;
double e_elec_mev = 0.;
double e_neutrino = 0.;
double tau_sec = 0.;
assert_always(std::stringstream(line) >> a >> z >> Q_betadecay_mev >> e_gamma_mev >> e_elec_mev >> e_neutrino >>
tau_sec);
if (Q_betadecay_mev > 0.) {
assert_always(!nuc_exists(z, a));
nuclides.push_back({.z = z, .a = a, .meanlife = tau_sec});
nuclides.back().branchprobs[DECAYTYPE_BETAMINUS] = 1.;
nuclides.back().endecay_q[DECAYTYPE_BETAMINUS] = Q_betadecay_mev * MEV;
nuclides.back().endecay_electron = e_elec_mev * MEV;
nuclides.back().endecay_gamma = e_gamma_mev * MEV;
assert_always(e_elec_mev >= 0.);
}
}
}
// add or update the nuclides with alpha decay data from alphadecays.txt (also ensures that He4
// exists as a decay product)
void read_alpha_decaydata() {
auto falpha = fstream_required("alphadecays.txt", std::ios::in);
std::string line;
if (!nuc_exists(2, 4)) {
nuclides.push_back({.z = 2, .a = 4, .meanlife = -1});
}
std::set<std::tuple<int, int>> seen_z_a{};
while (get_noncommentline(falpha, line)) {
// columns: # A, Z, branch_alpha, branch_beta, halflife[s], Q_total_alphadec[MeV], Q_total_betadec[MeV],
// E_alpha[MeV], E_gamma[MeV], E_beta[MeV]
int a = -1;
int z = -1;
double branch_alpha = 0.;
double branch_beta = 0.;
double halflife = 0.;
double Q_alphadecay_mev = 0.;
double Q_betadecay_mev = 0.;
double e_alpha_mev = 0.;
double e_gamma_mev = 0.;
double e_beta_mev = 0.;
assert_always(std::stringstream(line) >> a >> z >> branch_alpha >> branch_beta >> halflife >> Q_alphadecay_mev >>
Q_betadecay_mev >> e_alpha_mev >> e_gamma_mev >> e_beta_mev);
const bool keeprow = ((branch_alpha > 0. || branch_beta > 0.) && halflife > 0.);
if (keeprow) {
// one row per nuclide: the branching ratios below are divided out of the tabulated energies
// in place, so a second row for the same nuclide would apply that division twice
assert_always(!seen_z_a.contains({z, a}));
seen_z_a.insert({z, a});
const double tau_sec = halflife / std::numbers::ln2;
int alphanucindex = -1;
if (nuc_exists(z, a)) {
alphanucindex = get_nucindex(z, a);
} else {
// The electron and gamma energies are only taken for a nuclide that this file introduces.
// betaminusdecays.txt is read first and stays authoritative for a nuclide listed in both
// (its E_elec agrees with the E_beta column here for every such nuclide anyway).
nuclides.push_back({
.z = z,
.a = a,
.meanlife = tau_sec,
.endecay_electron = e_beta_mev * MEV,
.endecay_gamma = e_gamma_mev * MEV,
});
alphanucindex = static_cast<int>(nuclides.size() - 1);
}
nuclides[alphanucindex].branchprobs[DECAYTYPE_BETAMINUS] = branch_beta;
nuclides[alphanucindex].endecay_q[DECAYTYPE_BETAMINUS] = Q_betadecay_mev * MEV;
nuclides[alphanucindex].branchprobs[DECAYTYPE_ALPHA] = branch_alpha;
nuclides[alphanucindex].endecay_q[DECAYTYPE_ALPHA] = Q_alphadecay_mev * MEV;
// The decay data files tabulate the particle energies as averages per decay of the parent
// nuclide, so a branch taken by only a fraction of the decays contributes an energy that is
// already scaled by that fraction. endecay_alpha and endecay_electron are instead averages
// per decay of their own type, which is the convention of the hardcoded nuclides in
// add_standard_nuclides() and the one the consumers assume when they multiply by
// get_nuc_decaybranchprob(). Divide the branching ratio out here so it is applied once.
// The gamma energy needs no such division: it is a single per-nuclide value covering all
// branches at once, and is used without a branching factor.
// A nuclide with no beta-minus branch keeps no electron energy, which drops the conversion
// and Auger electrons that follow its alpha decay. Of the pure alpha emitters, U-235 leaves
// the most of its Q value unaccounted for by E_alpha and E_gamma, 3.7% counting the recoil
// nucleus and 2.0% without it, then Pa-231 and Ra-223. ENDF sums those electrons over all
// branches, so there is no branch to attribute them to, and they cannot go into the gamma
// energy without being transported as photons.
nuclides[alphanucindex].endecay_alpha = (branch_alpha > 0.) ? (e_alpha_mev * MEV / branch_alpha) : 0.;
nuclides[alphanucindex].endecay_electron =
(branch_beta > 0.) ? (nuclides[alphanucindex].endecay_electron / branch_beta) : 0.;
}
}
}
// add the nuclides with spontaneous fission decay data from fissiondecays.txt
void read_spontfission_decaydata() {
auto ffission = fstream_required("fissiondecays.txt", std::ios::in);
std::string line;
while (get_noncommentline(ffission, line)) {
int z_in = -1;
int a_in = -1;
double q_fission_mev = 0.;
double e_gamma_mev = 0.;
double e_1_mev = 0.;
double e_2_mev = 0.;
double m1 = 0.;
double m2 = 0.;
double z1 = 0.;
double z2 = 0.;
double tau_sec = 0.;
assert_always(std::stringstream(line) >> a_in >> z_in >> q_fission_mev >> e_gamma_mev >> e_1_mev >> e_2_mev >> m1 >>
m2 >> z1 >> z2 >> tau_sec);
assert_always(!nuc_exists(z_in, a_in));
nuclides.push_back({.z = z_in, .a = a_in, .meanlife = tau_sec});
nuclides.back().branchprobs[DECAYTYPE_SPONTFISSION] = 1.;
nuclides.back().endecay_q[DECAYTYPE_SPONTFISSION] = q_fission_mev * MEV;
// only the kinetic energy carried by the two fragments is deposited locally. The Q column of
// this file happens to hold almost the same value, but E1 and E2 are the quantity itself.
nuclides.back().endecay_fission = (e_1_mev + e_2_mev) * MEV;
printlnlog(" added spontaneous fission nuclide: (Z={}){}{} meanlife {:.1e} [days]", z_in, get_elname(z_in), a_in,
tau_sec / DAY);
}
}
// read the fission product tables from fissionproducts_GEF_100keV.txt for the spontaneous
// fission nuclides that are in use
void read_fissionproduct_data() {
auto ffission_products = fstream_required("fissionproducts_GEF_100keV.txt", std::ios::in);
std::string line;
while (get_noncommentline(ffission_products, line)) {
int z_parent = -1;
int a_parent = -1;
assert_always(std::stringstream(line) >> z_parent >> a_parent);
assert_always(get_noncommentline(ffission_products, line));
double num_neutrons = 0;
int tablesize = 0;
double q_fission_mev = 0.;
assert_always(std::stringstream(line) >> num_neutrons >> tablesize >> q_fission_mev);
const int nucindex = get_nucindex_or_neg_one(z_parent, a_parent);
const bool keep_table = (nucindex >= 0) && (nuclides[nucindex].branchprobs[DECAYTYPE_SPONTFISSION] > 0.);
if (keep_table) {
// This is the total energy released by the fission event, which is shared between the kinetic
// energy of the fragments, the prompt neutrons, and the prompt gamma rays. Only the fragment
// kinetic energy is deposited locally as heavy charged particles, so endecay_fission keeps the
// value from fissiondecays.txt and only the total Q value is taken from this table.
nuclides[nucindex].endecay_q[DECAYTYPE_SPONTFISSION] = q_fission_mev * MEV;
nuclides[nucindex].fission_daughters_z_a_prob.clear();
nuclides[nucindex].fission_daughters_z_a_prob.reserve(tablesize);
}
double daughter_yield_sum = 0.;
for (int i = 0; i < tablesize; i++) {
assert_always(get_noncommentline(ffission_products, line));
if (keep_table) {
int daughter_a = -1;
int daughter_z = -1;
double yieldpercent_before_neutron_emission = 0.;
double yieldpercent = 0.;
assert_always(std::stringstream(line) >> daughter_a >> daughter_z >> yieldpercent_before_neutron_emission >>
yieldpercent);
// the table lists yields as a percentage per fission, so convert to the expected number of
// these daughter nuclei produced per fission
const double yield_per_fission = yieldpercent / 100.;
nuclides[nucindex].fission_daughters_z_a_prob.push_back(
{.z = daughter_z, .a = daughter_a, .probability = yield_per_fission});
daughter_yield_sum += yield_per_fission;
}
}
if (keep_table) {
// the yields sum to the number of fragments produced per fission, which is two for the binary
// fission of the tabulated nuclides. This also guards against a table in different units.
assert_always(daughter_yield_sum > 1.9);
assert_always(daughter_yield_sum < 2.1);
nuclides[nucindex].decay_daughters_probsum = daughter_yield_sum;
}
}
}
// make sure that the input-specified nuclides and all decay daughters exist in the nuclide
// list, adding any missing ones as stable nuclides
void add_custom_and_daughter_nuclides(const std::span<const int> custom_zlist,
const std::span<const int> custom_alist) {
std::set<std::tuple<int, int>> nuclides_ensure_list{};
// add any extra nuclides that were specified but not in the decay data files
for (const auto [z, a] : std::views::zip(custom_zlist, custom_alist)) {
nuclides_ensure_list.insert({z, a});
}
// ensure any daughters nuclides are included
for (const auto& nuc : nuclides) {
for (const auto& decaytype : all_decaytypes) {
if (nuc.branchprobs[decaytype] > 0.) {
for (const auto& daughter : decay_daughters_z_a_prob(nuc.z, nuc.a, decaytype)) {
nuclides_ensure_list.insert({daughter.z, daughter.a});
}
}
}
}
// add any required nuclides (assume they are stable)
for (const auto& [z, a] : nuclides_ensure_list) {
if (!nuc_exists(z, a)) {
nuclides.push_back({.z = z, .a = a, .meanlife = -1});
}
}
}
// consistency checks on the nuclide list: no duplicate nuclides, branching probabilities that
// sum to one for unstable nuclides, and decay energies present for every active decay type
void check_nuclide_data() {
std::set<std::tuple<int, int>> seen_z_a{};
for (const auto& nuc : nuclides) {
assert_always(!seen_z_a.contains({nuc.z, nuc.a})); // duplicate nuclide detected
seen_z_a.insert({nuc.z, nuc.a});
const auto branchprob_sum = std::ranges::fold_left(nuc.branchprobs, 0.0, std::plus{});
if (branchprob_sum > 0.) {
assert_always(nuc.meanlife > 0.); // unstable nuclide has decay modes
assert_always(std::abs(branchprob_sum - 1.) < 1e-3); // branching ratios sum to 100%
} else {
assert_always(nuc.meanlife < 0.); // stable nuclide has no decay modes
}
if (nuc.branchprobs[DECAYTYPE_BETAPLUS] > 0.) {
assert_always(nuc.endecay_positron >= 0.);
assert_always(nuc.endecay_q[DECAYTYPE_BETAPLUS] >= 0.);
}
if (nuc.branchprobs[DECAYTYPE_BETAMINUS] > 0.) {
assert_always(nuc.endecay_electron >= 0.);
assert_always(nuc.endecay_q[DECAYTYPE_BETAMINUS] >= 0.);
// the mean kinetic energy carried away by the electron is a share of the decay energy, the
// rest going to the neutrino and the gamma rays. The largest share in the current data is
// Pb-210 at 0.62, whose Q value of 63.5 keV is small enough that the conversion electrons
// dominate, so this has room to spare and would only fire if a branching ratio were divided
// out of the tabulated energy more than once.
assert_always(nuc.endecay_electron <= nuc.endecay_q[DECAYTYPE_BETAMINUS]);
}
if (nuc.branchprobs[DECAYTYPE_ALPHA] > 0.) {
assert_always(nuc.endecay_alpha >= 0.);
assert_always(nuc.endecay_q[DECAYTYPE_ALPHA] >= 0.);
// Two-body kinematics gives the alpha particle a share (A-4)/A of whatever the gamma rays
// leave of the Q value, so scaling back up by A/(A-4) has to recover most of it. The floor is
// Rn-221 at 0.90, which is low only because its tabulated gamma energy is dominated by the
// competing beta branch. A branching ratio left in the tabulated energy would land orders of
// magnitude below this: it would put Fr-223 at 6e-5 of its Q value.
assert_always((nuc.endecay_alpha * static_cast<double>(nuc.a) / (nuc.a - 4)) >
(0.85 * nuc.endecay_q[DECAYTYPE_ALPHA]));
// and it cannot exceed the Q value either, since the alpha and the recoil are all the decay
// has to give. The highest real row is Fr-221 at 1.0025, which is the precision of the
// four-significant-figure energies, so 1% is the tolerance. This catches an E_alpha
// normalised to a different branching ratio than the one in the branch column: Ac-227 was
// tabulated against 1.419% rather than the 1.38% beside it, which left its alpha carrying
// 5.09 MeV out of a 5.04 MeV decay.
assert_always((nuc.endecay_alpha * static_cast<double>(nuc.a) / (nuc.a - 4)) <
(1.01 * nuc.endecay_q[DECAYTYPE_ALPHA]));
}
if (nuc.branchprobs[DECAYTYPE_SPONTFISSION] > 0.) {
assert_always(nuc.endecay_fission >= 0.);
assert_always(nuc.endecay_q[DECAYTYPE_SPONTFISSION] >= 0.);
// the fragment kinetic energy is only part of the energy released by the fission, the rest
// going to the prompt neutrons and gamma rays
assert_always(nuc.endecay_fission <= nuc.endecay_q[DECAYTYPE_SPONTFISSION]);
}
assert_always(nuc.endecay_gamma >= 0.);
}
}
} // anonymous namespace
// add all nuclides and decays, and later trim any irrelevant ones (not connected to input-specified nuclei)
void init_nuclides(const std::span<const int> custom_zlist, const std::span<const int> custom_alist) {
assert_always(custom_zlist.size() == custom_alist.size());
add_standard_nuclides();
// deliberately a copy, not a reference: the decay-data readers below add further nuclides
// cppcheck-suppress redundantCopyLocalConst
const auto standard_nuclides = nuclides;
read_betaminus_decaydata();
read_alpha_decaydata();
read_spontfission_decaydata();
read_fissionproduct_data();
add_custom_and_daughter_nuclides(custom_zlist, custom_alist);
check_nuclide_data();