-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathdecay.cc
More file actions
1389 lines (1194 loc) · 57.1 KB
/
decay.cc
File metadata and controls
1389 lines (1194 loc) · 57.1 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
#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 <numbers>
#include <numeric>
#include <ranges>
#include <set>
#include <span>
#include <sstream>
#include <string>
#include <string_view>
#include <tuple>
#include <vector>
#pragma clang unsafe_buffer_usage begin
#include <mpi.h>
#pragma clang unsafe_buffer_usage end
#include "artisoptions.h"
#include "atomic.h"
#include "constants.h"
#include "gammapkt.h"
#include "globals.h"
#include "grid.h"
#include "input.h"
#include "packet.h"
#include "random.h"
#include "sn3d.h"
namespace decay {
namespace {
constexpr auto elsymbols = std::array<const std::string, 119>{
"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 electons [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, decaytypes::DECAYTYPE_COUNT> endecay_q = {
0.}; // Q-value (reactant minus product energy) for each decay type
std::array<double, decaytypes::DECAYTYPE_COUNT> branchprobs = {0.}; // branch probability of each decay type
// (Z, A, probability) of fission daughters
std::vector<DecayDaughter> fission_daughters_z_a_prob{}; // NOLINT(readability-redundant-member-init)
// sum of daughter probabilities for all decay types
// default to 1.0 for the single-daughter decays and replace for fission
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<int> decaytypes;
std::vector<double> lambdas;
double branchproduct{
0.}; // product of all branching factors along the path set by calculate_decaypath_branchproduct()
};
std::vector<Nuclide> nuclides;
std::vector<DecayPath> decaypaths;
[[nodiscard]] constexpr auto decay_daughters_z_a_prob(const int z_parent, const int a_parent, const int decaytype)
-> std::vector<DecayDaughter> {
assert_always(decaytype >= 0);
assert_always(decaytype < decaytypes::DECAYTYPE_COUNT);
switch (static_cast<enum decaytypes>(decaytype)) {
case decaytypes::DECAYTYPE_ALPHA: {
return {DecayDaughter{.z = z_parent - 2,
.a = a_parent - 4,
.probability = 1.}}; // lose two protons and two neutrons (He4 handled separately)
}
case decaytypes::DECAYTYPE_BETAPLUS:
case decaytypes::DECAYTYPE_ELECTRONCAPTURE: {
return {DecayDaughter{.z = z_parent - 1, .a = a_parent, .probability = 1.}}; // lose a proton, gain a neutron
}
case decaytypes::DECAYTYPE_BETAMINUS: {
return {DecayDaughter{.z = z_parent + 1, .a = a_parent, .probability = 1.}}; // lose a neutron, gain a proton
}
case decaytypes::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 decaytypes::DECAYTYPE_NONE: {
return {DecayDaughter{.z = -1, .a = -1, .probability = 0.}}; // no daughter
}
case decaytypes::DECAYTYPE_COUNT: {
assert_always(false);
}
}
return {DecayDaughter{.z = -1, .a = -1, .probability = 0.}}; // no daughter
}
// decaypath_energy_per_mass points to an array of length npts_model * num_decaypaths
// the index [mgi * num_decaypaths + i] will hold the decay energy per mass [erg/g] released by chain i in cell mgi
// during the simulation time range tmin to tmax
std::span<double> decaypath_energy_per_mass{};
MPI_Win win_decaypath_energy_per_mass{MPI_WIN_NULL};
// Get the probability that a decay of decaytype occurs
[[nodiscard]] auto get_nuc_decaybranchprob(const int nucindex, const int decaytype) -> double {
assert_testmodeonly(nucindex >= 0);
assert_testmodeonly(nucindex < get_num_nuclides());
assert_testmodeonly(decaytype >= 0);
assert_testmodeonly(decaytype < decaytypes::DECAYTYPE_COUNT);
return nuclides[nucindex].branchprobs[decaytype];
}
[[nodiscard]] auto nucmass(int nucindex) -> double { return get_nuc_a(nucindex) * MH; }
[[nodiscard]] auto get_nuc_z_a(const int nucindex) -> std::tuple<int, int> {
assert_testmodeonly(nucindex >= 0);
assert_testmodeonly(nucindex < get_num_nuclides());
return {nuclides[nucindex].z, nuclides[nucindex].a};
}
// 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(get_num_nuclides() > 0);
const auto num_nuclides = get_num_nuclides();
for (int nucindex = 0; nucindex < num_nuclides; nucindex++) {
if (nuclides[nucindex].z == z && nuclides[nucindex].a == a) {
return nucindex;
}
}
return -1; // nuclide not found
}
[[nodiscard]] auto get_meanlife(const int nucindex) -> double {
assert_testmodeonly(nucindex >= 0);
assert_testmodeonly(nucindex < get_num_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 int decaytype) -> double {
assert_testmodeonly(decaytype >= 0);
assert_testmodeonly(decaytype < decaytypes::DECAYTYPE_COUNT);
switch (decaytype) {
case decaytypes::DECAYTYPE_ALPHA: {
return nuclides[nucindex].endecay_alpha;
}
case decaytypes::DECAYTYPE_BETAPLUS: {
return nuclides[nucindex].endecay_positron;
}
case decaytypes::DECAYTYPE_ELECTRONCAPTURE: {
return 0.;
}
case decaytypes::DECAYTYPE_BETAMINUS: {
return nuclides[nucindex].endecay_electron;
}
case decaytypes::DECAYTYPE_SPONTFISSION: {
return nuclides[nucindex].endecay_fission;
}
case decaytypes::DECAYTYPE_NONE: {
return 0.;
}
default: {
assert_always(false);
return 0.;
}
}
}
// average energy (erg) per decay in the form of gammas and particles [erg]
[[nodiscard]] auto nucdecayenergytotal(const int z, const int a) -> double {
const int nucindex = get_nucindex(z, a);
const auto endecay_particles = std::accumulate(
all_decaytypes.cbegin(), all_decaytypes.cend(), 0., [nucindex](const double ensum, const auto& decaytype) {
return ensum + (nucdecayenergyparticle(nucindex, decaytype) * get_nuc_decaybranchprob(nucindex, decaytype));
});
return nuclides[nucindex].endecay_gamma + endecay_particles;
}
// contributed energy release per decay [erg] for decaytype (e.g. decaytypes::DECAYTYPE_BETAPLUS) (excludes neutrinos!)
[[nodiscard]] auto nucdecayenergy(const int nucindex, const int decaytype) -> double {
const double endecay = nuclides[nucindex].endecay_gamma + nucdecayenergyparticle(nucindex, decaytype);
return endecay;
}
[[nodiscard]] auto nucdecayenergyqval(const int nucindex, const int 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_NONE);
assert_testmodeonly(decaypath.lambdas[secondlastindex] > 0.);
assert_testmodeonly(decaypath.decaytypes.back() == 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 int decaytype) -> std::string {
switch (decaytype) {
case decaytypes::DECAYTYPE_ALPHA: {
return "alpha";
}
case decaytypes::DECAYTYPE_BETAPLUS: {
return "beta+";
}
case decaytypes::DECAYTYPE_ELECTRONCAPTURE: {
return "ec";
}
case decaytypes::DECAYTYPE_BETAMINUS: {
return "beta-";
}
case decaytypes::DECAYTYPE_SPONTFISSION: {
return "sf";
}
case decaytypes::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_NONE) {
printlog(" -> {} -> ", get_str_decaytype(decaytype));
}
}
printlnlog("");
}
// 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& inital_last_decaypath = localdecaypaths.back();
const int end_nucindex = inital_last_decaypath.nucindex.back();
if ((get_meanlife(end_nucindex) <= 0.)) {
// daughter is stable: no extension possible
return;
}
const int prev_end_z = inital_last_decaypath.z.back();
const int prev_end_a = inital_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(inital_last_decaypath.z, inital_last_decaypath.a)) {
if (z == daughter.z && a == daughter.a) {
printlnlog("\nERROR: Loop found in nuclear decay chain.");
std::abort();
}
}
const auto daughter_nucindex = get_nucindex(daughter.z, daughter.a);
auto newdecaypath = inital_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_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 < get_num_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_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_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
void filter_unused_nuclides(const std::span<const int> custom_zlist, const std::span<const int> custom_alist,
const std::vector<Nuclide>& standard_nuclides) {
std::erase_if(nuclides, [&](const auto& nuc) {
// 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);
});
if (in_any_decaypath) {
return false;
}
printout("removing unused nuclide (Z=%d)%s%d\n", nuc.z, get_elname(nuc.z).c_str(), nuc.a);
return true;
});
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) -> 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()));
}
}
return tdecay;
}
// calculate final number abundance from multiple decays, e.g., Ni56 -> Co56 -> Fe56 (nuc[0] -> nuc[1] -> nuc[2])
// the top nuclide initial abundance is set and the chain-end abundance is returned (all intermediates nuclides
// are assumed to start with zero abundance)
// note: first and last can be nuclide can be the same if num_nuclides==1, reducing to simple decay formula
//
// timediff: time elapsed for decays [seconds]
// lambdas: array of 1/(mean lifetime) for nuc[0]..nuc[num_nuclides-1] [seconds^-1]
// useexpansionfactor: if true, return a modified 'abundance' at the end of the chain, with a weighting factor
// accounting for adiabatic loss from expansion since the decays occurred
// (This is needed to get the initial temperature)
constexpr auto calculate_decaychain(const double firstinitabund, const std::span<const double> lambdas,
const double timediff, const bool useexpansionfactor) -> double {
const int num_nuclides = static_cast<int>(lambdas.size());
assert_testmodeonly(num_nuclides >= 1);
double lambdaproduct = 1.;
for (int j = 0; j < (num_nuclides - 1); j++) {
lambdaproduct *= lambdas[j];
}
double sum = 0;
for (int j = 0; j < num_nuclides; j++) {
const auto lambda_j = lambdas[j];
double denominator = 1.;
for (int p = 0; p < num_nuclides; p++) {
if (p != j) {
denominator *= (lambdas[p] - lambda_j);
}
}
if (!useexpansionfactor) {
// get abundance output
sum += exp(-lambda_j * timediff) / denominator;
} else {
if (lambda_j > 0.) {
const double sumtermtop =
((1 + (1 / lambda_j / timediff)) * exp(-timediff * lambda_j)) - (1. / lambda_j / timediff);
sum += sumtermtop / denominator;
}
}
}
const double lastabund = firstinitabund * lambdaproduct * sum;
return lastabund;
}
// Get the mass fraction of a nuclide accounting for all decays and initial abundances.
// e.g., Co56 abundance may first increase with time due to Ni56 decays, then decrease due to Co56 decay
auto get_nuc_massfrac(const int nonemptymgi, const int nucindex, const double time) -> double {
const auto modelgridindex = grid::get_mgi_of_nonemptymgi(nonemptymgi);
assert_always(time >= 0.);
const double t_afterinit = time - grid::get_t_model();
const auto [z, a] = get_nuc_z_a(nucindex);
double nuctotal = 0.; // abundance or decay rate, depending on mode parameter
for (const auto& decaypath : decaypaths) {
const auto last_decaytype = decaypath.decaytypes[decaypath.nucindex.size() - 2];
// match 4He abundance to alpha decay of any nucleus (no continue), otherwise check daughter nuclide matches
if (z != 2 || a != 4 || last_decaytype != decaytypes::DECAYTYPE_ALPHA) {
if (decaypath.z.back() != z || decaypath.a.back() != a) {
continue;
}
}
const int nucindex_top = decaypath.nucindex[0];
const double top_initabund = grid::get_modelinitnucmassfrac(modelgridindex, nucindex_top) / nucmass(nucindex_top);
if (top_initabund <= 0.) {
continue;
}
auto lambdas = decaypath.lambdas;
if (z == 2 && a == 4) {
// treat the end nuclide as stable He4
lambdas[lambdas.size() - 1] = 0.;
}
const double massfraccontrib =
(decaypath.branchproduct * calculate_decaychain(top_initabund, lambdas, t_afterinit, false) *
nucmass(nucindex));
nuctotal += massfraccontrib;
}
const auto meanlife = get_meanlife(nucindex);
const auto lambda = (meanlife > 0.) ? 1. / meanlife : 0.;
// add the initial abundance
nuctotal += grid::get_modelinitnucmassfrac(modelgridindex, nucindex) * exp(-t_afterinit * lambda);
return nuctotal;
}
// Get the decay energy [erg/g] that would be released from time tstart [s] to time infinity by a given decaypath
// e.g. Ni56 -> Co56, represents the decays of Co56 nuclei that were produced from Ni56 in the initial abundance.
// Decays from Co56 due to the initial abundance of Co56 are not counted here, nor is the energy from Ni56 decays
auto get_endecay_to_tinf_per_ejectamass_at_time(const int modelgridindex, const int decaypathindex, const double time)
-> double {
assert_testmodeonly(decaypathindex >= 0);
assert_testmodeonly(decaypathindex < get_num_decaypaths());
const auto& decaypath = decaypaths[decaypathindex];
const int nucindex_top = decaypath.nucindex[0];
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();
// count the number of chain-top nuclei that haven't decayed past the end of the chain
auto lambdas = decaypath.lambdas;
// treat the end nuclide as stable to count how many got produced
lambdas[lambdas.size() - 1] = 0.;
const double abund_endsink = calculate_decaychain(top_initabund, lambdas, t_afterinit, false);
const double ndecays_remaining = decaypath.branchproduct * (top_initabund - abund_endsink);
// TODO ensure non-negative due to numerical precision?
const double endecay = ndecays_remaining * get_decaypath_lastdecayenergy(decaypath);
return endecay;
}
// Simple Euler integration as a check on the analytic result from
// get_endecay_per_ejectamass_tmodel_to_time_withexpansion()
auto get_endecay_per_ejectamass_tmodel_to_time_withexpansion_chain_numerical(const int nonemptymgi,
const int decaypathindex,
const double tstart) -> double {
const auto modelgridindex = grid::get_mgi_of_nonemptymgi(nonemptymgi);
const double min_meanlife =
std::ranges::min(decaypaths[decaypathindex].nucindex |
std::views::transform([](const auto nucindex) { return get_meanlife(nucindex); }) |
std::views::filter([](const auto meanlife) { return meanlife > 0.; }));
// min steps across the meanlifetime
const int nsteps = static_cast<int>(ceil((tstart - grid::get_t_model()) / min_meanlife) * 100000);
double chain_endecay = 0.;
double last_chain_endecay = -1.;
double last_t = -1.;
for (int i = 0; i < nsteps; i++) {
const double t = grid::get_t_model() + ((tstart - grid::get_t_model()) * i / nsteps);
const double chain_endecay_t = get_endecay_to_tinf_per_ejectamass_at_time(modelgridindex, decaypathindex, t);
if (last_chain_endecay >= 0) {
const double chain_step_endecay_diff = last_chain_endecay - chain_endecay_t;
const double expansionfactor =
0.5 * (t + last_t) / tstart; // photons lose energy as 1/t for homologous expansion
chain_endecay += chain_step_endecay_diff * expansionfactor;
}
last_chain_endecay = chain_endecay_t;
last_t = t;
}
const double chain_endecay_noexpansion =
(get_endecay_to_tinf_per_ejectamass_at_time(modelgridindex, decaypathindex, grid::get_t_model()) -
get_endecay_to_tinf_per_ejectamass_at_time(modelgridindex, decaypathindex, tstart));
printlnlog(" chain_endecay: {:g}", chain_endecay);
printlnlog(" chain_endecay_noexpansion: {:g}", chain_endecay_noexpansion);
printlnlog(" expansion energy factor: {:g}", chain_endecay / chain_endecay_noexpansion);
return chain_endecay;
}
// get decay energy per mass [erg/g] released by a decaypath between times tlow [s] and thigh [s]
auto get_endecay_per_ejectamass_between_times(const int mgi, const int decaypathindex, const double tlow,
const double thigh) -> double {
assert_testmodeonly(mgi < grid::get_npts_model());
assert_testmodeonly(tlow <= thigh);
const double energy_tlow = get_endecay_to_tinf_per_ejectamass_at_time(mgi, decaypathindex, tlow);
const double energy_thigh = get_endecay_to_tinf_per_ejectamass_at_time(mgi, decaypathindex, thigh);
assert_always(energy_tlow >= energy_thigh);
const double endiff = energy_tlow - energy_thigh;
assert_always(std::isfinite(endiff));
return endiff;
}
// get the decay energy released during the simulation [(tmodel if initial packets else tmin) to tmax] per unit mass
// [erg/g]
auto get_simtime_endecay_per_ejectamass(const int nonemptymgi, const int decaypathindex) -> double {
assert_testmodeonly(!decaypath_energy_per_mass.empty());
const double chainendecay = decaypath_energy_per_mass[(nonemptymgi * get_num_decaypaths()) + decaypathindex];
assert_testmodeonly(chainendecay >= 0.);
assert_testmodeonly(std::isfinite(chainendecay));
return chainendecay;
}
// 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 = std::fstream("nuclides.out", std::ofstream::out | std::ofstream::trunc);
assert_always(nuclides_file.is_open());
nuclides_file << "#nucindex Z A\n";
for (int nucindex = 0; nucindex < get_num_nuclides(); nucindex++) {
nuclides_file << nucindex << ' ' << get_nuc_z(nucindex) << ' ' << get_nuc_a(nucindex) << '\n';
}
}
} // anonymous namespace
[[nodiscard]] auto get_decay_neutrino_frac(const int nucindex, const int decaytype) -> double {
// subtract fraction of other decay products, nucdecayenergy() excludes neutrinos!
const double nu_frac = 1. - (nucdecayenergy(nucindex, decaytype) / nuclides[nucindex].endecay_q[decaytype]);
return nu_frac;
}
[[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 < get_num_nuclides());
return nuclides[nucindex].z;
}
[[nodiscard]] auto get_nuc_a(const int nucindex) -> int {
assert_testmodeonly(nucindex >= 0);
assert_testmodeonly(nucindex < get_num_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;
}
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;
}
// 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());
// Ni57
nuclides.push_back({.z = 28, .a = 57, .meanlife = 51.36 * 60});
nuclides.back().endecay_positron = 0.354 * MEV;
nuclides.back().branchprobs[DECAYTYPE_BETAPLUS] = 0.436;
nuclides.back().branchprobs[DECAYTYPE_ELECTRONCAPTURE] = 1. - 0.436;
// Ni56
nuclides.push_back({.z = 28, .a = 56, .meanlife = 8.80 * DAY});
nuclides.back().branchprobs[DECAYTYPE_ELECTRONCAPTURE] = 1.;
// 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;
// Cr48
nuclides.push_back({.z = 24, .a = 48, .meanlife = 1.29602 * DAY});
nuclides.back().branchprobs[DECAYTYPE_ELECTRONCAPTURE] = 1.;
// V48
nuclides.push_back({.z = 23, .a = 48, .meanlife = 23.0442 * DAY});
nuclides.back().endecay_positron = 0.290 * MEV * 0.499;
nuclides.back().branchprobs[DECAYTYPE_BETAPLUS] = 1.;
// Co57
nuclides.push_back({.z = 27, .a = 57, .meanlife = 392.03 * DAY});
nuclides.back().branchprobs[DECAYTYPE_ELECTRONCAPTURE] = 1.;
// Fe52
nuclides.push_back({.z = 26, .a = 52, .meanlife = 0.497429 * DAY});
nuclides.back().branchprobs[DECAYTYPE_ELECTRONCAPTURE] = 1.;
// Mn52
nuclides.push_back({.z = 25, .a = 52, .meanlife = 0.0211395 * DAY});
nuclides.back().branchprobs[DECAYTYPE_ELECTRONCAPTURE] = 1.;
const auto standard_nuclides = nuclides;
// any nuclides in the custom list that are not in the standard list need beta and alpha decay data
bool use_custom_nuclides = false;
for (auto i = 0Z; i < std::ssize(custom_zlist); i++) {
if (custom_zlist[i] < 0 || custom_alist[i] < 0) {
continue;
}
const bool in_std_list = std::ranges::any_of(standard_nuclides, [=](const auto& stdnuc) {
return (custom_zlist[i] == stdnuc.z) && (custom_alist[i] == stdnuc.a);
});
if (!in_std_list) {
use_custom_nuclides = true;
break;
}
}
if (use_custom_nuclides) {
auto fbetaminus = fstream_required("betaminusdecays.txt", std::ios::in);
std::string line;
while (get_noncommentline(fbetaminus, line)) {
// energies are average per beta decay
// 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.);
}
}
auto falpha = fstream_required("alphadecays.txt", std::ios::in);
if (!nuc_exists(2, 4)) {
nuclides.push_back({.z = 2, .a = 4, .meanlife = -1});
}
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) {
const double tau_sec = halflife / std::numbers::ln2;
int alphanucindex = -1;
if (nuc_exists(z, a)) {
alphanucindex = get_nucindex(z, a);
} else {
nuclides.push_back({.z = z, .a = a, .meanlife = tau_sec, .endecay_gamma = e_gamma_mev * MEV});
alphanucindex = static_cast<int>(nuclides.size() - 1);
}
nuclides[alphanucindex].endecay_alpha = e_alpha_mev * MEV;
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;
}
}
if (DECAY_SPONTFISSION_ON) {
printlnlog("Including spontaneous fission decay data from fissiondecays.txt and fissionproducts_GEF_100keV.txt");
auto ffission = fstream_required("fissiondecays.txt", std::ios::in);
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;
nuclides.back().endecay_fission = q_fission_mev * MEV; // will be overwritten if we have fission product data
printlnlog(" added spontaneous fission nuclide: (Z={}){}{} meanlife {} days", z_in, get_elname(z_in), a_in,
tau_sec / 86400.0);
}
auto ffission_products = fstream_required("fissionproducts_GEF_100keV.txt", std::ios::in);
while (get_noncommentline(ffission_products, line)) {
int z_parent = -1;
int a_parent = -1;
assert_always(std::stringstream(line) >> z_parent >> a_parent);
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) {
nuclides[nucindex].endecay_q[DECAYTYPE_SPONTFISSION] = q_fission_mev * MEV;
nuclides[nucindex].endecay_fission = q_fission_mev * MEV;
nuclides[nucindex].fission_daughters_z_a_prob.clear();
nuclides[nucindex].fission_daughters_z_a_prob.reserve(tablesize);
}
double daughter_prob_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 probability_before_neutron_emission = 0.;
double probability = 0.;
assert_always(std::stringstream(line) >> daughter_a >> daughter_z >> probability_before_neutron_emission >>
probability);
nuclides[nucindex].fission_daughters_z_a_prob.push_back(
{.z = daughter_z, .a = daughter_a, .probability = probability});
daughter_prob_sum += probability;
}
}
if (keep_table) {
nuclides[nucindex].decay_daughters_probsum = daughter_prob_sum;
}
}
}
}
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)