-
Notifications
You must be signed in to change notification settings - Fork 8
/
Copy pathdecay.cc
1425 lines (1216 loc) · 58 KB
/
decay.cc
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 <mpi.h>
#include <algorithm>
#include <array>
#include <cctype>
#include <cmath>
#include <cstddef>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iostream>
#include <numbers>
#include <numeric>
#include <set>
#include <span>
#include <sstream>
#include <string>
#include <string_view>
#include <tuple>
#include <utility>
#include <vector>
#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 "sn3d.h"
namespace decay {
namespace {
constexpr std::array<const std::string, 119> 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 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]
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
};
[[nodiscard]] constexpr auto decay_daughter_z(const int z_parent, const int /*a_parent*/, const int decaytype) -> int
// check if (z_parent, a_parent) is a parent of (z, a)
{
assert_always(decaytype >= 0);
assert_always(decaytype < decaytypes::DECAYTYPE_COUNT);
switch (static_cast<enum decaytypes>(decaytype)) {
case decaytypes::DECAYTYPE_ALPHA: {
return z_parent - 2; // lose two protons and two neutrons
}
case decaytypes::DECAYTYPE_BETAPLUS:
case decaytypes::DECAYTYPE_ELECTRONCAPTURE: {
return z_parent - 1; // lose a proton, gain a neutron
}
case decaytypes::DECAYTYPE_BETAMINUS: {
return z_parent + 1; // lose a neutron, gain a proton
}
case decaytypes::DECAYTYPE_NONE: {
return -1; // no daughter
}
case decaytypes::DECAYTYPE_COUNT: {
assert_always(false);
}
}
return -1; // no daughter
}
[[nodiscard]] constexpr auto decay_daughter_a(const int /*z_parent*/, const int a_parent, const int decaytype) -> int
// check if (z_parent, a_parent) is a parent of (z, a)
{
switch (static_cast<enum decaytypes>(decaytype)) {
case decaytypes::DECAYTYPE_ALPHA: {
return a_parent - 4; // lose two protons and two neutrons
}
case decaytypes::DECAYTYPE_BETAPLUS:
case decaytypes::DECAYTYPE_ELECTRONCAPTURE:
case decaytypes::DECAYTYPE_BETAMINUS: {
return a_parent; // swap a neutron to proton or vice-versa
}
case decaytypes::DECAYTYPE_NONE: {
return -1; // no daughter
}
case decaytypes::DECAYTYPE_COUNT: {
assert_always(false);
}
}
return -1; // no daughter
}
// 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()
[[nodiscard]] auto final_daughter_a() const -> int { return decay_daughter_a(z.back(), a.back(), decaytypes.back()); }
[[nodiscard]] auto final_daughter_z() const -> int { return decay_daughter_z(z.back(), a.back(), decaytypes.back()); }
};
std::vector<Nuclide> nuclides;
std::vector<DecayPath> decaypaths;
// 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
std::span<double> decaypath_energy_per_mass{};
MPI_Win win_decaypath_energy_per_mass{MPI_WIN_NULL};
[[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 get_nuc_decaybranchprob(const int z_parent, const int a_parent, const int decaytype) -> double {
return get_nuc_decaybranchprob(get_nucindex(z_parent, a_parent), decaytype);
}
// check if (z_parent, a_parent) is a parent of (z, a)
[[nodiscard]] auto nuc_is_parent(const int z_parent, const int a_parent, const int z, const int a) -> bool {
assert_testmodeonly(nuc_exists(z_parent, a_parent));
// each radioactive nuclide is limited to one daughter nuclide
return std::ranges::any_of(all_decaytypes, [=](const auto decaytype) {
return decay_daughter_z(z_parent, a_parent, decaytype) == z && decay_daughter_a(z_parent, a_parent, decaytype) == a;
});
}
[[nodiscard]] auto get_nuc_z_a(const int nucindex) -> std::pair<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 int 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) { printout("(Z=%d)%s%d", z, get_elname(z).c_str(), a); }
void printout_nuclidemeanlife(const int z, const int a) {
const int nucindex = get_nucindex_or_neg_one(z, a);
const bool exists = (nucindex >= 0);
if (exists && get_meanlife(nucindex) > 0.) {
printout("[tau %.1es]", get_meanlife(nucindex));
} else if (exists) {
printout("[stable,in_net]");
} else {
printout("[stable,offnet]");
}
}
// 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;
}
default: {
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()); }
[[nodiscard]] auto get_decaypathlength(const DecayPath &dpath) -> int { return static_cast<int>(dpath.z.size()); }
[[nodiscard]] auto get_decaypathlength(const int decaypathindex) -> int {
return get_decaypathlength(decaypaths[decaypathindex]);
}
// return the product of all branching factors in the decay path
[[nodiscard]] auto calculate_decaypath_branchproduct(const DecayPath &decaypath) -> double {
double branchprod = 1.;
const auto decaypathlength = get_decaypathlength(decaypath);
for (int i = 0; i < decaypathlength; i++) {
branchprod = branchprod * get_nuc_decaybranchprob(decaypath.nucindex[i], decaypath.decaytypes[i]);
}
return branchprod;
}
// a decaypath's energy is the decay energy of the last nuclide and decaytype in the chain
[[nodiscard]] auto get_decaypath_lastnucdecayenergy(const DecayPath &dpath) -> double {
const int nucindex_end = dpath.nucindex.back();
const int decaytype_end = dpath.decaytypes.back();
return nucdecayenergy(nucindex_end, decaytype_end);
}
[[nodiscard]] auto get_decaypath_lastnucdecayenergy(const int decaypathindex) -> double {
return get_decaypath_lastnucdecayenergy(decaypaths[decaypathindex]);
}
void printout_decaytype(const int decaytype) {
switch (decaytype) {
case decaytypes::DECAYTYPE_ALPHA: {
printout("alpha");
break;
}
case decaytypes::DECAYTYPE_BETAPLUS: {
printout("beta+");
break;
}
case decaytypes::DECAYTYPE_ELECTRONCAPTURE: {
printout("ec");
break;
}
case decaytypes::DECAYTYPE_BETAMINUS: {
printout("beta-");
break;
}
case decaytypes::DECAYTYPE_NONE: {
printout("none");
break;
}
default:
break;
}
}
void printout_decaypath(const int decaypathindex) {
assert_always(!decaypaths.empty());
const auto &decaypath = decaypaths[decaypathindex];
printout(" decaypath %d: ", decaypathindex);
for (int i = 0; i < get_decaypathlength(decaypathindex); i++) {
printout_nuclidename(decaypath.z[i], decaypath.a[i]);
printout_nuclidemeanlife(decaypath.z[i], decaypath.a[i]);
if (decaypath.decaytypes[i] != DECAYTYPE_NONE) {
printout(" -> ");
printout_decaytype(decaypath.decaytypes[i]);
printout(" -> ");
}
}
// if the last nuclide is unstable, print its daughter nucleus
if (decaypath.decaytypes.back() != DECAYTYPE_NONE) {
printout_nuclidename(decaypath.final_daughter_z(), decaypath.final_daughter_a());
printout_nuclidemeanlife(decaypath.final_daughter_z(), decaypath.final_daughter_a());
}
printout("\n");
}
void extend_lastdecaypath()
// follow decays at the ends of the current list of decaypaths,
// to get decaypaths from all descendants
{
const int startdecaypathindex = static_cast<int>(decaypaths.size() - 1);
const int daughter_z = decaypaths[startdecaypathindex].final_daughter_z();
const int daughter_a = decaypaths[startdecaypathindex].final_daughter_a();
if (nuc_exists(daughter_z, daughter_a)) {
const int daughter_nucindex = get_nucindex(daughter_z, daughter_a);
for (enum decaytypes dectypeindex2 : all_decaytypes) {
if (get_nuc_decaybranchprob(daughter_nucindex, dectypeindex2) == 0.) {
continue;
}
// check for nuclide in existing path, which would indicate a loop
for (int i = 0; i < get_decaypathlength(startdecaypathindex); i++) {
if (decaypaths[startdecaypathindex].z[i] == daughter_z && decaypaths[startdecaypathindex].a[i] == daughter_a) {
printout("\nERROR: Loop found in nuclear decay chain.\n");
std::abort();
}
}
decaypaths.push_back(decaypaths[startdecaypathindex]);
decaypaths.back().z.push_back(daughter_z);
decaypaths.back().a.push_back(daughter_a);
decaypaths.back().nucindex.push_back(daughter_nucindex);
decaypaths.back().decaytypes.push_back(dectypeindex2);
extend_lastdecaypath();
}
}
}
void find_decaypaths(const std::vector<int> &custom_zlist, const std::vector<int> &custom_alist,
const std::vector<Nuclide> &standard_nuclides) {
decaypaths.clear();
for (int startnucindex = 0; startnucindex < get_num_nuclides(); startnucindex++) {
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. || get_meanlife(startnucindex) <= 0.) {
continue;
}
bool is_custom_nuclide = false;
for (ptrdiff_t i = 0; 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
if (!is_custom_nuclide && !std::ranges::any_of(standard_nuclides, [z, a](const auto &stdnuc) {
return (z == stdnuc.z) && (a == stdnuc.a);
})) {
continue;
}
decaypaths.push_back({.z = std::vector<int>(1, z),
.a = std::vector<int>(1, a),
.nucindex = std::vector<int>(1, startnucindex),
.decaytypes = std::vector<int>(1, decaytype),
.lambdas = {},
.branchproduct = 0.});
extend_lastdecaypath(); // take this single step chain and find all descendants
}
}
std::ranges::SORT_OR_STABLE_SORT(decaypaths, [](const DecayPath &d1, const DecayPath &d2) {
// true if d1 < d2
// chains are sorted by mass number, then atomic number, then length
const int d1_length = get_decaypathlength(d1);
const int d2_length = get_decaypathlength(d2);
const int smallestpathlength = std::min(d1_length, d2_length);
for (int i = 0; i < smallestpathlength; i++) {
if (d1.a[i] < d2.a[i]) {
return true;
}
if (d1.a[i] > d2.a[i]) {
return false;
}
if (d1.z[i] < d2.z[i]) {
return true;
}
if (d1.z[i] > d2.z[i]) {
return false;
}
}
// one is an extension of the other, so place the shorter one first
return d1_length < d2_length;
});
for (auto &decaypath : decaypaths) {
// 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.; }));
// 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;
});
// the nuclide one past the end of the path is a used as a sink, so treat it as stable (even if it's not)
decaypath.lambdas.push_back(0.);
decaypath.branchproduct = calculate_decaypath_branchproduct(decaypath);
}
decaypaths.shrink_to_fit();
}
// remove nuclides that are not a standard or custom input-specified nuclide, or connected to these by decays
void filter_unused_nuclides(const std::vector<int> &custom_zlist, const std::vector<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 (ptrdiff_t i = 0; i < std::ssize(custom_zlist); i++) {
if ((nuc.z == custom_zlist[i]) && (nuc.a == custom_alist[i])) {
return false;
}
}
const bool in_any_decaypath = std::ranges::any_of(decaypaths, [&nuc](const auto &decaypath) {
for (ptrdiff_t i = 0; i < std::ssize(decaypath.z); i++) {
if (decaypath.z[i] == nuc.z && decaypath.a[i] == nuc.a) {
// nuc is in the decay path
return true;
}
}
// return true if nuc is the final daughter of a decay path
return (decaypath.final_daughter_z() == nuc.z && decaypath.final_daughter_a() == 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 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 decaypathlength = get_decaypathlength(decaypathindex);
for (int i = 0; i < decaypathlength; i++) {
const int nucindex = decaypaths[decaypathindex].nucindex[i];
const double zrand = rng_uniform_pos();
tdecay += -get_meanlife(nucindex) * log(zrand);
}
}
return tdecay;
}
constexpr auto calculate_decaychain(const double firstinitabund, const std::vector<double> &lambdas,
const int num_nuclides, const double timediff, const bool useexpansionfactor)
-> double {
// 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 since firstinitabund was true [seconds]
// numnuclides: number of items in lambdas to use
// 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 photon energy loss from expansion since the decays occurred
// (This is needed to get the initial temperature)
assert_always(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++) {
double denominator = 1.;
for (int p = 0; p < num_nuclides; p++) {
if (p != j) {
denominator *= (lambdas[p] - lambdas[j]);
}
}
if (!useexpansionfactor) // abundance output
{
sum += exp(-lambdas[j] * timediff) / denominator;
} else {
if (lambdas[j] > 0.) {
const double sumtermtop =
((1 + 1 / lambdas[j] / timediff) * exp(-timediff * lambdas[j])) - (1. / lambdas[j] / timediff);
sum += sumtermtop / denominator;
}
}
}
const double lastabund = firstinitabund * lambdaproduct * sum;
return lastabund;
}
auto get_nuc_massfrac(const int nonemptymgi, const int z, const int a, const double time) -> double
// Get the mass fraction of a nuclide accounting for all decays including those of its parent and grandparent.
// e.g., Co56 abundance may first increase with time due to Ni56 decays, then decease due to Co56 decay
// Can be called for stable nuclides that are one daughters of the radioactive nuclide list e.g., Fe56
// For stable nuclides, abundance returned only comes from other decays (some could be included in init model elem
// frac)
{
const auto modelgridindex = grid::get_mgi_of_nonemptymgi(nonemptymgi);
assert_always(time >= 0.);
const double t_afterinit = time - grid::get_t_model();
const int nucindex = get_nucindex_or_neg_one(z, a);
const bool nuc_exists_z_a = (nucindex >= 0);
// decay chains include all paths from radionuclides to other radionuclides (including trivial size-one chains)
double nuctotal = 0.; // abundance or decay rate, depending on mode parameter
for (const auto &decaypath : decaypaths) {
const int z_end = decaypath.z.back();
const int a_end = decaypath.a.back();
// match 4He abundance to alpha decay of any nucleus (no continue), otherwise check daughter nuclide matches
if (z != 2 || a != 4 || decaypath.decaytypes.back() != decaytypes::DECAYTYPE_ALPHA) {
if (nuc_exists_z_a && (z_end != z || a_end != a)) // requested nuclide is in network, so match last nuc in chain
{
continue;
}
// if requested nuclide is not in network then match daughter of last nucleus in chain
if (!nuc_exists_z_a && !nuc_is_parent(z_end, a_end, z, a)) {
continue;
}
}
const int z_top = decaypath.z[0];
const int a_top = decaypath.a[0];
const int nucindex_top = decaypath.nucindex[0];
const double top_initabund = grid::get_modelinitnucmassfrac(modelgridindex, nucindex_top) / nucmass(z_top, a_top);
assert_always(top_initabund >= 0.);
if (top_initabund <= 0.) {
continue;
}
const int decaypathlength = get_decaypathlength(decaypath);
int fulldecaypathlength = decaypathlength;
// if the nuclide is out of network, it's one past the end of the chain
// or if we're counting alpha particles and the last decaytype is alpha, then the alpha sink is one past the end
if (!nuc_exists_z_a || (z == 2 && a == 4 && decaypath.decaytypes.back() == decaytypes::DECAYTYPE_ALPHA)) {
fulldecaypathlength = decaypathlength + 1;
}
const double massfraccontrib =
(decaypath.branchproduct *
calculate_decaychain(top_initabund, decaypath.lambdas, fulldecaypathlength, t_afterinit, false) *
nucmass(z, a));
// assert_always(massfraccontrib >= 0.);
nuctotal += massfraccontrib;
}
// for stable nuclei in the network, we need to contribute the initial abundance
if (nuc_exists_z_a && get_meanlife(nucindex) <= 0.) {
nuctotal += grid::get_modelinitnucmassfrac(modelgridindex, nucindex);
}
return nuctotal;
}
auto get_endecay_to_tinf_per_ejectamass_at_time(const int modelgridindex, const int decaypathindex, const double time)
-> double
// returns 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 decay 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
assert_testmodeonly(decaypathindex >= 0);
assert_testmodeonly(decaypathindex < get_num_decaypaths());
const int z_top = decaypaths[decaypathindex].z[0];
const int a_top = decaypaths[decaypathindex].a[0];
const int nucindex_top = decaypaths[decaypathindex].nucindex[0];
const double top_initabund = grid::get_modelinitnucmassfrac(modelgridindex, nucindex_top) / nucmass(z_top, a_top);
if (top_initabund <= 0.) {
return 0.;
}
assert_testmodeonly(top_initabund >= 0.);
const int decaypathlength = get_decaypathlength(decaypathindex);
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
const double abund_endplusone =
calculate_decaychain(top_initabund, decaypaths[decaypathindex].lambdas, decaypathlength + 1, t_afterinit, false);
const double ndecays_remaining = decaypaths[decaypathindex].branchproduct * (top_initabund - abund_endplusone);
const double endecay = ndecays_remaining * get_decaypath_lastnucdecayenergy(decaypathindex);
return endecay;
}
// Simple Euler integration as a check on the analytic result from
// get_endecay_per_ejectamass_t0_to_time_withexpansion()
auto get_endecay_per_ejectamass_t0_to_time_withexpansion_chain_numerical(const int nonemptymgi,
const int decaypathindex, const double tstart)
-> double {
const auto modelgridindex = grid::get_mgi_of_nonemptymgi(nonemptymgi);
double min_meanlife = -1;
for (int i = 0; i < get_decaypathlength(decaypathindex); i++) {
const double meanlife = get_meanlife(decaypaths[decaypathindex].nucindex[i]);
if (min_meanlife < 0. || meanlife < min_meanlife) {
min_meanlife = meanlife;
}
}
// 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));
printout(" chain_endecay: %g\n", chain_endecay);
printout(" chain_endecay_noexpansion: %g\n", chain_endecay_noexpansion);
printout(" expansion energy factor: %g\n", chain_endecay / chain_endecay_noexpansion);
return chain_endecay;
}
auto get_endecay_per_ejectamass_between_times(const int mgi, const int decaypathindex, const double tlow,
const double thigh) -> double
// get decay energy per mass [erg/g] released by a decaypath between times tlow [s] and thigh [s]
{
assert_always(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;
}
auto calculate_simtime_endecay_per_ejectamass(const int mgi, const int decaypathindex) -> double
// calculate the decay energy released during the simulation time per unit mass [erg/g]
{
assert_testmodeonly(mgi < grid::get_npts_model());
if constexpr (!INITIAL_PACKETS_ON) {
// get decay energy released from t=tmin to tmax
return get_endecay_per_ejectamass_between_times(mgi, decaypathindex, globals::tmin, globals::tmax);
} else {
// get decay energy released from t=0 to tmax
return get_endecay_per_ejectamass_between_times(mgi, decaypathindex, grid::get_t_model(), globals::tmax);
}
}
auto get_simtime_endecay_per_ejectamass(const int nonemptymgi, const int decaypathindex) -> double
// get the decay energy released during the simulation time per unit mass [erg/g]
{
const double chainendecay = decaypath_energy_per_mass[(nonemptymgi * get_num_decaypaths()) + decaypathindex];
assert_testmodeonly(chainendecay >= 0.);
assert_testmodeonly(std::isfinite(chainendecay));
return chainendecay;
}
auto get_decaypath_power_per_ejectamass(const int decaypathindex, const int nonemptymgi, const double time) -> double
// total decay power per mass [erg/s/g] for a given decaypath
{
// 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 int z_top = decaypaths[decaypathindex].z[0];
const int a_top = decaypaths[decaypathindex].a[0];
const int nucindex_top = decaypaths[decaypathindex].nucindex[0];
const int modelgridindex = grid::get_mgi_of_nonemptymgi(nonemptymgi);
const double top_initabund = grid::get_modelinitnucmassfrac(modelgridindex, nucindex_top);
assert_always(top_initabund >= 0.);
if (top_initabund <= 0.) {
return 0.;
}
const int nucindex_end = decaypaths[decaypathindex].nucindex[get_decaypathlength(decaypathindex) - 1];
const double t_afterinit = time - grid::get_t_model();
const int decaypathlength = get_decaypathlength(decaypathindex);
// 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 endnucabund =
decaypaths[decaypathindex].branchproduct *
calculate_decaychain(top_initabund, decaypaths[decaypathindex].lambdas, decaypathlength, t_afterinit, false);
const double endecay = get_decaypath_lastnucdecayenergy(decaypathindex);
const double decaypower = endecay * endnucabund / get_meanlife(nucindex_end) / nucmass(z_top, a_top);
assert_always(decaypower >= 0.);
assert_always(std::isfinite(decaypower));
return decaypower;
}
} // anonymous namespace
[[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[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;
}
printout("Could not find nuclide Z=%d A=%d\n", 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 {
for (int nucindex = 0; nucindex < get_num_nuclides(); nucindex++) {
if (nuclides[nucindex].z == z && nuclides[nucindex].a == a) {
return true;
}
}
return false;
}
// average energy per decay in the form of gamma rays [erg]
[[nodiscard]] __host__ __device__ auto nucdecayenergygamma(const int nucindex) -> double {
return nuclides[nucindex].endecay_gamma;
}
[[nodiscard]] __host__ __device__ 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;
}
}
printout("Could not get atomic number of '%s' '%s'\n", strnuc.c_str(), elcode.c_str());
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
size_t i = 0;
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::vector<int> &custom_zlist, const std::vector<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 (ptrdiff_t i = 0; 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);
assert_always(fbetaminus.is_open());
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_beta_mev = 0.;
double e_gamma_mev = 0.;
double e_elec_mev = 0.;
double e_neutrino = 0.;
double tau_sec = 0.;
std::stringstream(line) >> a >> z >> q_beta_mev >> e_gamma_mev >> e_elec_mev >> e_neutrino >> tau_sec;
if (q_beta_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_beta_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);
assert_always(falpha.is_open());
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_total_alphadec = 0.;
double Q_total_betadec = 0.;
double e_alpha_mev = 0.;
double e_gamma_mev = 0.;
double e_beta_mev = 0.;
std::stringstream(line) >> a >> z >> branch_alpha >> branch_beta >> halflife >> Q_total_alphadec >>
Q_total_betadec >> 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_total_betadec * MEV;
nuclides[alphanucindex].branchprobs[DECAYTYPE_ALPHA] = branch_alpha;
nuclides[alphanucindex].endecay_q[DECAYTYPE_ALPHA] = Q_total_alphadec * MEV;
}
}
}
// add any extra nuclides that were specified but not in the decay data files
for (int i = 0; i < static_cast<int>(custom_alist.size()); i++) {
const int z = custom_zlist[i];
const int a = custom_alist[i];
if (!nuc_exists(z, a)) {
// printout("Adding Z %d A %d with no decay data (assuming stable)\n", z, a);
nuclides.push_back({.z = z, .a = a, .meanlife = -1});
}
}
printout("Number of nuclides before filtering: %td\n", get_num_nuclides());
find_decaypaths(custom_zlist, custom_alist, standard_nuclides);
filter_unused_nuclides(custom_zlist, custom_alist, standard_nuclides);
printout("Number of nuclides: %td\n", get_num_nuclides());
const int maxdecaypathlength = std::accumulate(
decaypaths.cbegin(), decaypaths.cend(), 0,
[](const int maxlen, const auto &decaypath) { return std::max(maxlen, get_decaypathlength(decaypath)); });