diff --git a/process/models/neutronics/__init__.py b/process/models/neutronics/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/process/models/neutronics/atomic_data.json b/process/models/neutronics/atomic_data.json new file mode 100644 index 000000000..8305d824b --- /dev/null +++ b/process/models/neutronics/atomic_data.json @@ -0,0 +1,3738 @@ +{ + "NATURAL_ABUNDANCE_SOURCE": "Isotopic abundances from Meija J, Coplen T B, et al, \"Isotopic compositions of the elements 2013 (IUPAC Technical Report)\", Pure. Appl. Chem. 88 (3), pp. 293-306 (2013). The \"representative isotopic abundance\" values from column 9 are used except where an interval is given, in which case the \"best measurement\" is used.", + "NATURAL_ABUNDANCE": { + "H1": 0.99984426, + "H2": 0.00015574, + "He3": 2e-6, + "He4": 0.999998, + "Li6": 0.07589, + "Li7": 0.92411, + "Be9": 1.0, + "B10": 0.1982, + "B11": 0.8018, + "C12": 0.988922, + "C13": 0.011078, + "N14": 0.996337, + "N15": 0.003663, + "O16": 0.9976206, + "O17": 0.000379, + "O18": 0.0020004, + "F19": 1.0, + "Ne20": 0.9048, + "Ne21": 0.0027, + "Ne22": 0.0925, + "Na23": 1.0, + "Mg24": 0.78951, + "Mg25": 0.1002, + "Mg26": 0.11029, + "Al27": 1.0, + "Si28": 0.9222968, + "Si29": 0.0468316, + "Si30": 0.0308716, + "P31": 1.0, + "S32": 0.9504074, + "S33": 0.0074869, + "S34": 0.0419599, + "S36": 0.0001458, + "Cl35": 0.757647, + "Cl37": 0.242353, + "Ar36": 0.003336, + "Ar38": 0.000629, + "Ar40": 0.996035, + "K39": 0.932581, + "K40": 0.000117, + "K41": 0.067302, + "Ca40": 0.96941, + "Ca42": 0.00647, + "Ca43": 0.00135, + "Ca44": 0.02086, + "Ca46": 4e-5, + "Ca48": 0.00187, + "Sc45": 1.0, + "Ti46": 0.0825, + "Ti47": 0.0744, + "Ti48": 0.7372, + "Ti49": 0.0541, + "Ti50": 0.0518, + "V50": 0.0025, + "V51": 0.9975, + "Cr50": 0.04345, + "Cr52": 0.83789, + "Cr53": 0.09501, + "Cr54": 0.02365, + "Mn55": 1.0, + "Fe54": 0.05845, + "Fe56": 0.91754, + "Fe57": 0.02119, + "Fe58": 0.00282, + "Co59": 1.0, + "Ni58": 0.680769, + "Ni60": 0.262231, + "Ni61": 0.011399, + "Ni62": 0.036345, + "Ni64": 0.009256, + "Cu63": 0.6915, + "Cu65": 0.3085, + "Zn64": 0.4917, + "Zn66": 0.2773, + "Zn67": 0.0404, + "Zn68": 0.1845, + "Zn70": 0.0061, + "Ga69": 0.60108, + "Ga71": 0.39892, + "Ge70": 0.2052, + "Ge72": 0.2745, + "Ge73": 0.0776, + "Ge74": 0.3652, + "Ge76": 0.0775, + "As75": 1.0, + "Se74": 0.0086, + "Se76": 0.0923, + "Se77": 0.076, + "Se78": 0.2369, + "Se80": 0.498, + "Se82": 0.0882, + "Br79": 0.50686, + "Br81": 0.49314, + "Kr78": 0.00355, + "Kr80": 0.02286, + "Kr82": 0.11593, + "Kr83": 0.115, + "Kr84": 0.56987, + "Kr86": 0.17279, + "Rb85": 0.7217, + "Rb87": 0.2783, + "Sr84": 0.0056, + "Sr86": 0.0986, + "Sr87": 0.07, + "Sr88": 0.8258, + "Y89": 1.0, + "Zr90": 0.5145, + "Zr91": 0.1122, + "Zr92": 0.1715, + "Zr94": 0.1738, + "Zr96": 0.028, + "Nb93": 1.0, + "Mo92": 0.14649, + "Mo94": 0.09187, + "Mo95": 0.15873, + "Mo96": 0.16673, + "Mo97": 0.09582, + "Mo98": 0.24292, + "Mo100": 0.09744, + "Ru96": 0.0554, + "Ru98": 0.0187, + "Ru99": 0.1276, + "Ru100": 0.126, + "Ru101": 0.1706, + "Ru102": 0.3155, + "Ru104": 0.1862, + "Rh103": 1.0, + "Pd102": 0.0102, + "Pd104": 0.1114, + "Pd105": 0.2233, + "Pd106": 0.2733, + "Pd108": 0.2646, + "Pd110": 0.1172, + "Ag107": 0.51839, + "Ag109": 0.48161, + "Cd106": 0.01245, + "Cd108": 0.00888, + "Cd110": 0.1247, + "Cd111": 0.12795, + "Cd112": 0.24109, + "Cd113": 0.12227, + "Cd114": 0.28754, + "Cd116": 0.07512, + "In113": 0.04281, + "In115": 0.95719, + "Sn112": 0.0097, + "Sn114": 0.0066, + "Sn115": 0.0034, + "Sn116": 0.1454, + "Sn117": 0.0768, + "Sn118": 0.2422, + "Sn119": 0.0859, + "Sn120": 0.3258, + "Sn122": 0.0463, + "Sn124": 0.0579, + "Sb121": 0.5721, + "Sb123": 0.4279, + "Te120": 0.0009, + "Te122": 0.0255, + "Te123": 0.0089, + "Te124": 0.0474, + "Te125": 0.0707, + "Te126": 0.1884, + "Te128": 0.3174, + "Te130": 0.3408, + "I127": 1.0, + "Xe124": 0.00095, + "Xe126": 0.00089, + "Xe128": 0.0191, + "Xe129": 0.26401, + "Xe130": 0.04071, + "Xe131": 0.21232, + "Xe132": 0.26909, + "Xe134": 0.10436, + "Xe136": 0.08857, + "Cs133": 1.0, + "Ba130": 0.0011, + "Ba132": 0.001, + "Ba134": 0.0242, + "Ba135": 0.0659, + "Ba136": 0.0785, + "Ba137": 0.1123, + "Ba138": 0.717, + "La138": 0.0008881, + "La139": 0.9991119, + "Ce136": 0.00186, + "Ce138": 0.00251, + "Ce140": 0.88449, + "Ce142": 0.11114, + "Pr141": 1.0, + "Nd142": 0.27153, + "Nd143": 0.12173, + "Nd144": 0.23798, + "Nd145": 0.08293, + "Nd146": 0.17189, + "Nd148": 0.05756, + "Nd150": 0.05638, + "Sm144": 0.0308, + "Sm147": 0.15, + "Sm148": 0.1125, + "Sm149": 0.1382, + "Sm150": 0.0737, + "Sm152": 0.2674, + "Sm154": 0.2274, + "Eu151": 0.4781, + "Eu153": 0.5219, + "Gd152": 0.002, + "Gd154": 0.0218, + "Gd155": 0.148, + "Gd156": 0.2047, + "Gd157": 0.1565, + "Gd158": 0.2484, + "Gd160": 0.2186, + "Tb159": 1.0, + "Dy156": 0.00056, + "Dy158": 0.00095, + "Dy160": 0.02329, + "Dy161": 0.18889, + "Dy162": 0.25475, + "Dy163": 0.24896, + "Dy164": 0.2826, + "Ho165": 1.0, + "Er162": 0.00139, + "Er164": 0.01601, + "Er166": 0.33503, + "Er167": 0.22869, + "Er168": 0.26978, + "Er170": 0.1491, + "Tm169": 1.0, + "Yb168": 0.00123, + "Yb170": 0.02982, + "Yb171": 0.14086, + "Yb172": 0.21686, + "Yb173": 0.16103, + "Yb174": 0.32025, + "Yb176": 0.12995, + "Lu175": 0.97401, + "Lu176": 0.02599, + "Hf174": 0.0016, + "Hf176": 0.0526, + "Hf177": 0.186, + "Hf178": 0.2728, + "Hf179": 0.1362, + "Hf180": 0.3508, + "Ta180": 0.0001201, + "Ta181": 0.9998799, + "W180": 0.0012, + "W182": 0.265, + "W183": 0.1431, + "W184": 0.3064, + "W186": 0.2843, + "Re185": 0.374, + "Re187": 0.626, + "Os184": 0.0002, + "Os186": 0.0159, + "Os187": 0.0196, + "Os188": 0.1324, + "Os189": 0.1615, + "Os190": 0.2626, + "Os192": 0.4078, + "Ir191": 0.373, + "Ir193": 0.627, + "Pt190": 0.00012, + "Pt192": 0.00782, + "Pt194": 0.32864, + "Pt195": 0.33775, + "Pt196": 0.25211, + "Pt198": 0.07356, + "Au197": 1.0, + "Hg196": 0.0015, + "Hg198": 0.1004, + "Hg199": 0.1694, + "Hg200": 0.2314, + "Hg201": 0.1317, + "Hg202": 0.2974, + "Hg204": 0.0682, + "Tl203": 0.29524, + "Tl205": 0.70476, + "Pb204": 0.014, + "Pb206": 0.241, + "Pb207": 0.221, + "Pb208": 0.524, + "Bi209": 1.0, + "Th230": 0.0002, + "Th232": 0.9998, + "Pa231": 1.0, + "U234": 5.4e-5, + "U235": 0.007204, + "U238": 0.992742 + }, + "ATOMIC_MASS_SOURCE": "retrieved from openmc 0.15.3, which in turn obtains the data from mass_1.mas20.txt, which is retrieved from the publication by W.J.Huang, M.Wang, F.G.Kondev, G.Audi and S.Naimi, Chinese Physics C45, 030002, March 2021.", + "ATOMIC_MASS": { + "N1": 1.00866491582, + "H1": 1.00782503224, + "H2": 2.01410177811, + "H3": 3.01604928199, + "He3": 3.01602932265, + "Li3": 3.030775, + "H4": 4.026431868, + "He4": 4.00260325413, + "Li4": 4.027185562, + "H5": 5.035311493, + "He5": 5.012057224, + "Li5": 5.0125378, + "Be5": 5.03987, + "H6": 6.044955437, + "He6": 6.018885891, + "Li6": 6.01512288742, + "Be6": 6.0197264090000004, + "B6": 6.0508, + "H7": 7.052749, + "He7": 7.027990654, + "Li7": 7.01600343666, + "Be7": 7.016928717, + "B7": 7.029712, + "He8": 8.03393439, + "Li8": 8.022486246, + "Be8": 8.005305102, + "B8": 8.024607316, + "C8": 8.037643042, + "He9": 9.043946419, + "Li9": 9.026790191, + "Be9": 9.012183066, + "B9": 9.013329649, + "C9": 9.031037207, + "He10": 10.052815308, + "Li10": 10.035483453, + "Be10": 10.013534695, + "B10": 10.012936862, + "C10": 10.016853218, + "N10": 10.041653543, + "Li11": 11.043723581, + "Be11": 11.021661081, + "B11": 11.009305166, + "C11": 11.011432597, + "N11": 11.026090945, + "Li12": 12.052613941, + "Be12": 12.026922083, + "B12": 12.014352638, + "C12": 12.0, + "N12": 12.018613182, + "O12": 12.034261747, + "Li13": 13.061171503, + "Be13": 13.036134507, + "B13": 13.017779981, + "C13": 13.00335483521, + "N13": 13.005738609, + "O13": 13.024815437, + "Be14": 14.04289292, + "B14": 14.025404012, + "C14": 14.00324198843, + "N14": 14.00307400446, + "O14": 14.008596706, + "F14": 14.034315199, + "Be15": 15.053490215, + "B15": 15.031087953, + "C15": 15.010599256, + "N15": 15.00010889894, + "O15": 15.003065618, + "F15": 15.017785139, + "Ne15": 15.04317298, + "Be16": 16.061672036, + "B16": 16.03984192, + "C16": 16.014701256, + "N16": 16.006101925, + "O16": 15.9949146196, + "F16": 16.011465723, + "Ne16": 16.025750864, + "B17": 17.046931399, + "C17": 17.022578672, + "N17": 17.008448877, + "O17": 16.99913175664, + "F17": 17.002095238, + "Ne17": 17.017713959, + "Na17": 17.03776, + "B18": 18.055601682, + "C18": 18.026751932, + "N18": 18.014077565, + "O18": 17.99915961284, + "F18": 18.000937325, + "Ne18": 18.005708693, + "Na18": 18.026879386, + "B19": 19.064166, + "C19": 19.034797596, + "N19": 19.017022419, + "O19": 19.00357797, + "F19": 18.99840316288, + "Ne19": 19.001880903, + "Na19": 19.013880272, + "Mg19": 19.034169182, + "B20": 20.073484, + "C20": 20.040261732, + "N20": 20.023367295, + "O20": 20.004075358, + "F20": 19.999981252, + "Ne20": 19.99244017619, + "Na20": 20.007354426, + "Mg20": 20.018763075, + "B21": 21.083017, + "C21": 21.049, + "N21": 21.027087573, + "O21": 21.00865495, + "F21": 20.999948894, + "Ne21": 20.993846685, + "Na21": 20.997654702, + "Mg21": 21.011705764, + "Al21": 21.028975, + "C22": 22.05755399, + "N22": 22.034100918, + "O22": 22.009965746, + "F22": 22.002998809, + "Ne22": 21.991385109, + "Na22": 21.994437418, + "Mg22": 21.999570654, + "Al22": 22.01954, + "Si22": 22.03579, + "C23": 23.06889, + "N23": 23.039421, + "O23": 23.015696686, + "F23": 23.003526874, + "Ne23": 22.9944669, + "Na23": 22.98976928199, + "Mg23": 22.994123941, + "Al23": 23.007244351, + "Si23": 23.02544, + "N24": 24.05039, + "O24": 24.019861, + "F24": 24.00809937, + "Ne24": 23.993610645, + "Na24": 23.990963011, + "Mg24": 23.985041697, + "Al24": 23.999947541, + "Si24": 24.011535441, + "P24": 24.03577, + "N25": 25.0601, + "O25": 25.029338919, + "F25": 25.012167727, + "Ne25": 24.997814799, + "Na25": 24.989953973, + "Mg25": 24.985836964, + "Al25": 24.990428306, + "Si25": 25.004108801, + "P25": 25.02119, + "O26": 26.037210155, + "F26": 26.020020392, + "Ne26": 26.000516496, + "Na26": 25.992634649, + "Mg26": 25.982592971, + "Al26": 25.986891863, + "Si26": 25.992333804, + "P26": 26.01178, + "S26": 26.02907, + "O27": 27.047955, + "F27": 27.027322, + "Ne27": 27.007569462, + "Na27": 26.994076408, + "Mg27": 26.984340627999998, + "Al27": 26.981538408, + "Si27": 26.986704688, + "P27": 26.999224409, + "S27": 27.01828, + "O28": 28.05591, + "F28": 28.036223095, + "Ne28": 28.012130767, + "Na28": 27.998939, + "Mg28": 27.983876606, + "Al28": 27.981910087, + "Si28": 27.97692653499, + "P28": 27.992326585, + "S28": 28.004372766, + "Cl28": 28.02954, + "F29": 29.043103, + "Ne29": 29.019753, + "Na29": 29.002877092, + "Mg29": 28.988617393, + "Al29": 28.980453164, + "Si29": 28.97649466525, + "P29": 28.981800368, + "S29": 28.996611448, + "Cl29": 29.014130178, + "F30": 30.05165, + "Ne30": 30.024992235, + "Na30": 30.009097932, + "Mg30": 29.990462825999998, + "Al30": 29.982968388, + "Si30": 29.973770136, + "P30": 29.978313489, + "S30": 29.984906769, + "Cl30": 30.00477, + "Ar30": 30.022470511, + "F31": 31.060272, + "Ne31": 31.033474816, + "Na31": 31.013146656, + "Mg31": 30.996648232, + "Al31": 30.983949756, + "Si31": 30.975363194, + "P31": 30.97376199863, + "S31": 30.979557007, + "Cl31": 30.992448098, + "Ar31": 31.012158, + "Ne32": 32.03972, + "Na32": 32.020011026, + "Mg32": 31.999110139, + "Al32": 31.988084339, + "Si32": 31.974151539, + "P32": 31.973907643, + "S32": 31.97207117443, + "Cl32": 31.985684637, + "Ar32": 31.997637826, + "K32": 32.02265, + "Ne33": 33.04938, + "Na33": 33.025529, + "Mg33": 33.005327245, + "Al33": 32.990877687, + "Si33": 32.977976964, + "P33": 32.971725694, + "S33": 32.97145890985, + "Cl33": 32.977451989, + "Ar33": 32.989925547, + "K33": 33.00756, + "Ne34": 34.056728, + "Na34": 34.03401, + "Mg34": 34.008935481, + "Al34": 33.996779057, + "Si34": 33.978575437, + "P34": 33.973645887, + "S34": 33.967867012, + "Cl34": 33.973762491, + "Ar34": 33.980270093, + "K34": 33.99869, + "Ca34": 34.01487, + "Na35": 35.041043, + "Mg35": 35.01679, + "Al35": 34.999759817, + "Si35": 34.984550134, + "P35": 34.973314053, + "S35": 34.969032322, + "Cl35": 34.968852694, + "Ar35": 34.975257721, + "K35": 34.988005407, + "Ca35": 35.00514, + "Na36": 36.049708, + "Mg36": 36.021879, + "Al36": 36.006388, + "Si36": 35.986649271, + "P36": 35.978259619, + "S36": 35.967080699, + "Cl36": 35.968306822, + "Ar36": 35.967545105, + "K36": 35.98130201, + "Ca36": 35.993074406, + "Sc36": 36.01648, + "Na37": 37.057471, + "Mg37": 37.030286265, + "Al37": 37.010531, + "Si37": 36.992945191, + "P37": 36.979606956, + "S37": 36.971125506999996, + "Cl37": 36.965902584, + "Ar37": 36.966776314, + "K37": 36.973375889, + "Ca37": 36.985897852, + "Sc37": 37.003779, + "Mg38": 38.03658, + "Al38": 38.017402, + "Si38": 37.995523, + "P38": 37.984303105, + "S38": 37.97116331, + "Cl38": 37.968010418, + "Ar38": 37.962732104, + "K38": 37.969081116, + "Ca38": 37.976319226, + "Sc38": 37.995438, + "Ti38": 38.011669, + "Mg39": 39.045384, + "Al39": 39.022169, + "Si39": 39.002491, + "P39": 38.986285865, + "S39": 38.975133852, + "Cl39": 38.968008162, + "Ar39": 38.964313039, + "K39": 38.96370648661, + "Ca39": 38.970710813, + "Sc39": 38.98478497, + "Ti39": 39.002362, + "Mg40": 40.051906, + "Al40": 40.029619, + "Si40": 40.005829, + "P40": 39.991288865, + "S40": 39.975482562, + "Cl40": 39.970415469, + "Ar40": 39.96238312378, + "K40": 39.963998166, + "Ca40": 39.962590865, + "Sc40": 39.977967292, + "Ti40": 39.990498721, + "V40": 40.013065, + "Al41": 41.035878, + "Si41": 41.013011, + "P41": 40.994654, + "S41": 40.979593451, + "Cl41": 40.970684525, + "Ar41": 40.964500571, + "K41": 40.96182525796, + "Ca41": 40.962277921, + "Sc41": 40.969251104, + "Ti41": 40.983148, + "V41": 41.000344, + "Al42": 42.043049, + "Si42": 42.017681, + "P42": 42.001084, + "S42": 41.9810651, + "Cl42": 41.973342, + "Ar42": 41.963045736, + "K42": 41.962402306, + "Ca42": 41.958617828, + "Sc42": 41.965516522, + "Ti42": 41.973049022, + "V42": 41.99182, + "Cr42": 42.007225, + "Al43": 43.050478, + "Si43": 43.0248, + "P43": 43.005024, + "S43": 42.986907635, + "Cl43": 42.9740637, + "Ar43": 42.965636055, + "K43": 42.960734703, + "Ca43": 42.95876643, + "Sc43": 42.961150472, + "Ti43": 42.968522521, + "V43": 42.980766, + "Cr43": 42.997885, + "Si44": 44.03061, + "P44": 44.011219, + "S44": 43.990118848, + "Cl44": 43.978116312, + "Ar44": 43.964923816, + "K44": 43.961586986, + "Ca44": 43.955481543, + "Sc44": 43.959402867, + "Ti44": 43.959689951, + "V44": 43.97411, + "Cr44": 43.985657, + "Mn44": 44.007547, + "Si45": 45.040247, + "P45": 45.016747, + "S45": 44.995717, + "Cl45": 44.980394353, + "Ar45": 44.968039733, + "K45": 44.960691493, + "Ca45": 44.956186326, + "Sc45": 44.955907503, + "Ti45": 44.958121211, + "V45": 44.965768951, + "Cr45": 44.97905, + "Mn45": 44.994364, + "Fe45": 45.014774, + "P46": 46.024659, + "S46": 46.000365, + "Cl46": 45.985121323, + "Ar46": 45.968037446, + "K46": 45.961981586, + "Ca46": 45.953687988, + "Sc46": 45.955167485, + "Ti46": 45.952626856, + "V46": 45.960197971, + "Cr46": 45.96836097, + "Mn46": 45.986506, + "Fe46": 46.000977, + "P47": 47.031895, + "S47": 47.007912, + "Cl47": 46.989501, + "Ar47": 46.972768114, + "K47": 46.961661614, + "Ca47": 46.954541394, + "Sc47": 46.952402704, + "Ti47": 46.951757752, + "V47": 46.954904038, + "Cr47": 46.962895544, + "Mn47": 46.975774, + "Fe47": 46.992625, + "Co47": 47.011133, + "S48": 48.0137, + "Cl48": 47.995405, + "Ar48": 47.97608, + "K48": 47.965341186, + "Ca48": 47.952522904, + "Sc48": 47.952223157, + "Ti48": 47.947940932, + "V48": 47.952251229, + "Cr48": 47.954028667, + "Mn48": 47.968549085, + "Fe48": 47.980676, + "Co48": 48.00161, + "Ni48": 48.018028, + "S49": 49.022644, + "Cl49": 49.001009, + "Ar49": 48.981546, + "K49": 48.968210755, + "Ca49": 48.955662875, + "Sc49": 48.950014423, + "Ti49": 48.947864627, + "V49": 48.948510746, + "Cr49": 48.951332955, + "Mn49": 48.959612585, + "Fe49": 48.973429, + "Co49": 48.989393, + "Ni49": 49.008803, + "Cl50": 50.008309, + "Ar50": 49.98569, + "K50": 49.972380017, + "Ca50": 49.957499217, + "Sc50": 49.952176415, + "Ti50": 49.944785839, + "V50": 49.947155845, + "Cr50": 49.946041443, + "Mn50": 49.954237391, + "Fe50": 49.962988, + "Co50": 49.981073, + "Ni50": 49.995577, + "Cl51": 51.015341, + "Ar51": 50.992818, + "K51": 50.975827867, + "Ca51": 50.960995665, + "Sc51": 50.953592095, + "Ti51": 50.9466096, + "V51": 50.943956867, + "Cr51": 50.944764652, + "Mn51": 50.948208065, + "Fe51": 50.956840779, + "Co51": 50.970647, + "Ni51": 50.987225, + "Ar52": 51.998626, + "K52": 51.981602, + "Ca52": 51.963213648, + "Sc52": 51.956582351, + "Ti52": 51.94689196, + "V52": 51.944772839, + "Cr52": 51.940504992, + "Mn52": 51.945563488, + "Fe52": 51.948115217, + "Co52": 51.963112, + "Ni52": 51.976028, + "Cu52": 51.997552, + "Ar53": 53.00729, + "K53": 52.9868, + "Ca53": 52.968451, + "Sc53": 52.958231821, + "Ti53": 52.949724785, + "V53": 52.944335593, + "Cr53": 52.940646961, + "Mn53": 52.941287742, + "Fe53": 52.945305574, + "Co53": 52.954203217, + "Ni53": 52.96819, + "Cu53": 52.985754, + "K54": 53.99463, + "Ca54": 53.972989, + "Sc54": 53.96361662, + "Ti54": 53.951022786, + "V54": 53.946437472, + "Cr54": 53.938878012, + "Mn54": 53.940356429, + "Fe54": 53.939608306, + "Co54": 53.948459192, + "Ni54": 53.957833, + "Cu54": 53.977015, + "Zn54": 53.993267, + "K55": 55.00076, + "Ca55": 54.9803, + "Sc55": 54.967622601, + "Ti55": 54.955267465, + "V55": 54.947241114, + "Cr55": 54.940837289, + "Mn55": 54.938043172, + "Fe55": 54.938291283, + "Co55": 54.941996531, + "Ni55": 54.951329961, + "Cu55": 54.966038, + "Zn55": 54.984358, + "K56": 56.00851, + "Ca56": 55.985079999999996, + "Sc56": 55.97332, + "Ti56": 55.95778819, + "V56": 55.950450694, + "Cr56": 55.940649107, + "Mn56": 55.938902947, + "Fe56": 55.934935617, + "Co56": 55.93983815, + "Ni56": 55.942127872, + "Cu56": 55.958515, + "Zn56": 55.972743, + "Ga56": 55.996361, + "Ca57": 56.99262, + "Sc57": 56.97746, + "Ti57": 56.963590068, + "V57": 56.952320197, + "Cr57": 56.943612408999996, + "Mn57": 56.938285968, + "Fe57": 56.935392134, + "Co57": 56.936289913, + "Ni57": 56.939791525, + "Cu57": 56.949211819, + "Zn57": 56.965056, + "Ga57": 56.983886, + "Ca58": 57.99794, + "Sc58": 57.98403, + "Ti58": 57.966602, + "V58": 57.956626932, + "Cr58": 57.944184502, + "Mn58": 57.940066646, + "Fe58": 57.933273738, + "Co58": 57.935751429, + "Ni58": 57.93534178, + "Cu58": 57.944532413, + "Zn58": 57.954590428, + "Ga58": 57.974728999999996, + "Ge58": 57.992399, + "Sc59": 58.98894, + "Ti59": 58.972614, + "V59": 58.959385659, + "Cr59": 58.94837781, + "Mn59": 58.940391113, + "Fe59": 58.934873649, + "Co59": 58.933193656, + "Ni59": 58.934345571, + "Cu59": 58.939496844, + "Zn59": 58.949312017, + "Ga59": 58.963757, + "Ge59": 58.982963, + "Sc60": 59.99565, + "Ti60": 59.976028, + "V60": 59.96431329, + "Cr60": 59.949898146, + "Mn60": 59.943136576, + "Fe60": 59.934070411, + "Co60": 59.933815667, + "Ni60": 59.930785256, + "Cu60": 59.937363916, + "Zn60": 59.94184145, + "Ga60": 59.957498, + "Ge60": 59.970918, + "As60": 59.994128, + "Sc61": 61.001, + "Ti61": 60.982448, + "V61": 60.96725, + "Cr61": 60.954400963, + "Mn61": 60.944452544, + "Fe61": 60.936746244, + "Co61": 60.932476145, + "Ni61": 60.931054945, + "Cu61": 60.933457371, + "Zn61": 60.93950696, + "Ga61": 60.949398859, + "Ge61": 60.964187, + "As61": 60.981857, + "Ti62": 61.986581, + "V62": 61.97265, + "Cr62": 61.956097451, + "Mn62": 61.947907386, + "Fe62": 61.936791812, + "Co62": 61.934058317, + "Ni62": 61.928344871, + "Cu62": 61.932594921, + "Zn62": 61.934333477, + "Ga62": 61.944189757, + "Ge62": 61.95519, + "As62": 61.973891, + "Ti63": 62.993827, + "V63": 62.9765, + "Cr63": 62.961344384, + "Mn63": 62.949664675, + "Fe63": 62.9402727, + "Co63": 62.933599744, + "Ni63": 62.929669139, + "Cu63": 62.929597236, + "Zn63": 62.933211167, + "Ga63": 62.939294195, + "Ge63": 62.949628, + "As63": 62.964036, + "Ti64": 63.9989, + "V64": 63.98248, + "Cr64": 63.964058, + "Mn64": 63.95384937, + "Fe64": 63.940987763, + "Co64": 63.935810291, + "Ni64": 63.927966341, + "Cu64": 63.929763857, + "Zn64": 63.929141772, + "Ga64": 63.936840365, + "Ge64": 63.941689913, + "As64": 63.95756, + "Se64": 63.971336, + "V65": 64.987354, + "Cr65": 64.969705, + "Mn65": 64.95601975, + "Fe65": 64.945015324, + "Co65": 64.936462073, + "Ni65": 64.930084697, + "Cu65": 64.927789487, + "Zn65": 64.929240532, + "Ga65": 64.932734395, + "Ge65": 64.939368137, + "As65": 64.949611, + "Se65": 64.964552, + "V66": 65.993977, + "Cr66": 65.973462, + "Mn66": 65.960546834, + "Fe66": 65.94624996, + "Co66": 65.939442945, + "Ni66": 65.929139334, + "Cu66": 65.928868814, + "Zn66": 65.926033704, + "Ga66": 65.931589832, + "Ge66": 65.933862126, + "As66": 65.944148779, + "Se66": 65.955276, + "V67": 66.999302, + "Cr67": 66.979946, + "Mn67": 66.964079, + "Fe67": 66.951035482, + "Co67": 66.940609628, + "Ni67": 66.931569414, + "Cu67": 66.927729526, + "Zn67": 66.927127482, + "Ga67": 66.928202384, + "Ge67": 66.93273362, + "As67": 66.939251111, + "Se67": 66.949994, + "Br67": 66.964798, + "Cr68": 67.984112, + "Mn68": 67.969533, + "Fe68": 67.953314875, + "Co68": 67.944250135, + "Ni68": 67.931868789, + "Cu68": 67.929610889, + "Zn68": 67.924844291, + "Ga68": 67.927980221, + "Ge68": 67.928095308, + "As68": 67.93677413, + "Se68": 67.941825239, + "Br68": 67.958356, + "Cr69": 68.990789, + "Mn69": 68.973408, + "Fe69": 68.9581, + "Co69": 68.946023102, + "Ni69": 68.935610268, + "Cu69": 68.929429268, + "Zn69": 68.926550418, + "Ga69": 68.925573531, + "Ge69": 68.927964471, + "As69": 68.932246294, + "Se69": 68.939414847, + "Br69": 68.950338413, + "Kr69": 68.96518, + "Cr70": 69.995191, + "Mn70": 69.979066, + "Fe70": 69.960805, + "Co70": 69.949941, + "Ni70": 69.936431303, + "Cu70": 69.932392079, + "Zn70": 69.925319181, + "Ga70": 69.926021917, + "Ge70": 69.924248706, + "As70": 69.930926151, + "Se70": 69.933515523, + "Br70": 69.944792323, + "Kr70": 69.955877, + "Mn71": 70.983285, + "Fe71": 70.966259, + "Co71": 70.952366923, + "Ni71": 70.940518964, + "Cu71": 70.932676832, + "Zn71": 70.92771958, + "Ga71": 70.924702536, + "Ge71": 70.924952284, + "As71": 70.927113758, + "Se71": 70.932209432, + "Br71": 70.939342156, + "Kr71": 70.950265696, + "Rb71": 70.965582, + "Mn72": 71.989372, + "Fe72": 71.969479, + "Co72": 71.956844, + "Ni72": 71.941785926, + "Cu72": 71.935820307, + "Zn72": 71.926842807, + "Ga72": 71.926367434, + "Ge72": 71.922075826, + "As72": 71.926752295, + "Se72": 71.927140507, + "Br72": 71.936594607, + "Kr72": 71.942092407, + "Rb72": 71.958851, + "Fe73": 72.975416, + "Co73": 72.95983, + "Ni73": 72.946206683, + "Cu73": 72.936674378, + "Zn73": 72.929582582, + "Ga73": 72.925174682, + "Ge73": 72.923458956, + "As73": 72.923829089, + "Se73": 72.926754883, + "Br73": 72.931671621, + "Kr73": 72.939289195, + "Rb73": 72.950529, + "Sr73": 72.9657, + "Fe74": 73.978969, + "Co74": 73.964766, + "Ni74": 73.94798, + "Cu74": 73.939874862, + "Zn74": 73.929407262, + "Ga74": 73.926945726, + "Ge74": 73.921177762, + "As74": 73.923928598, + "Se74": 73.922475935, + "Br74": 73.929910281, + "Kr74": 73.933084017, + "Rb74": 73.944265868, + "Sr74": 73.95617, + "Fe75": 74.985357, + "Co75": 74.96817, + "Ni75": 74.952732, + "Cu75": 74.941522606, + "Zn75": 74.932840246, + "Ga75": 74.926500246, + "Ge75": 74.922858371, + "As75": 74.921594562, + "Se75": 74.922522871, + "Br75": 74.92581057, + "Kr75": 74.930945746, + "Rb75": 74.938573201, + "Sr75": 74.94995277, + "Y75": 74.96584, + "Co76": 75.973687, + "Ni76": 75.955308, + "Cu76": 75.945275025, + "Zn76": 75.933114957, + "Ga76": 75.928827625, + "Ge76": 75.921402726, + "As76": 75.92239201, + "Se76": 75.919213704, + "Br76": 75.924541577, + "Kr76": 75.925910726, + "Rb76": 75.935073032, + "Sr76": 75.941762761, + "Y76": 75.95869, + "Co77": 76.97744, + "Ni77": 76.960494, + "Cu77": 76.9478, + "Zn77": 76.936887199, + "Ga77": 76.9291543, + "Ge77": 76.923549844, + "As77": 76.920647564, + "Se77": 76.91991415, + "Br77": 76.921379194, + "Kr77": 76.92467, + "Rb77": 76.9304016, + "Sr77": 76.937945455, + "Y77": 76.950146, + "Zr77": 76.965604, + "Ni78": 77.963618, + "Cu78": 77.95223, + "Zn78": 77.938289205, + "Ga78": 77.931608845, + "Ge78": 77.922852912, + "As78": 77.921827795, + "Se78": 77.917309243, + "Br78": 77.921145859, + "Kr78": 77.920366341, + "Rb78": 77.928141868, + "Sr78": 77.93217998, + "Y78": 77.94399, + "Zr78": 77.956146, + "Ni79": 78.970402, + "Cu79": 78.95519, + "Zn79": 78.942638068, + "Ga79": 78.932852301, + "Ge79": 78.925360129, + "As79": 78.920948445, + "Se79": 78.918499251, + "Br79": 78.918337601, + "Kr79": 78.920082945, + "Rb79": 78.923989864, + "Sr79": 78.929707664, + "Y79": 78.93793, + "Zr79": 78.94979, + "Nb79": 78.966022, + "Ni80": 79.975706, + "Cu80": 79.961138, + "Zn80": 79.94455293, + "Ga80": 79.936420774, + "Ge80": 79.925350774, + "As80": 79.922474548, + "Se80": 79.916521785, + "Br80": 79.91852981, + "Kr80": 79.916378048, + "Rb80": 79.922516444, + "Sr80": 79.92451754, + "Y80": 79.934354755, + "Zr80": 79.941642, + "Nb80": 79.958754, + "Cu81": 80.966269, + "Zn81": 80.950402619, + "Ga81": 80.938133842, + "Ge81": 80.928832942, + "As81": 80.92213229, + "Se81": 80.917993044, + "Br81": 80.916288206, + "Kr81": 80.916589714, + "Rb81": 80.918993927, + "Sr81": 80.923211394, + "Y81": 80.929454283, + "Zr81": 80.938314, + "Nb81": 80.95023, + "Mo81": 80.965915, + "Cu82": 81.972818, + "Zn82": 81.954574099, + "Ga82": 81.943176533, + "Ge82": 81.929774033, + "As82": 81.924738733, + "Se82": 81.916699537, + "Br82": 81.91680176, + "Kr82": 81.9134811552, + "Rb82": 81.918209024, + "Sr82": 81.918399847, + "Y82": 81.926930188, + "Zr82": 81.931689, + "Nb82": 81.944079, + "Mo82": 81.956661, + "Zn83": 82.961041, + "Ga83": 82.947120301, + "Ge83": 82.934539101, + "As83": 82.925206901, + "Se83": 82.919118609, + "Br83": 82.915175289, + "Kr83": 82.914126518, + "Rb83": 82.915114182, + "Sr83": 82.917554374, + "Y83": 82.922484025, + "Zr83": 82.929240925, + "Nb83": 82.938211, + "Mo83": 82.950252, + "Tc83": 82.966377, + "Zn84": 83.965722, + "Ga84": 83.95267, + "Ge84": 83.937575091, + "As84": 83.929303291, + "Se84": 83.918466762, + "Br84": 83.916496419, + "Kr84": 83.91149772863, + "Rb84": 83.914375225, + "Sr84": 83.91341912, + "Y84": 83.920671061, + "Zr84": 83.923325662, + "Nb84": 83.934279, + "Mo84": 83.941846, + "Tc84": 83.959527, + "Zn85": 84.972914, + "Ga85": 84.95722, + "Ge85": 84.942969659, + "As85": 84.932163659, + "Se85": 84.922260759, + "Br85": 84.915645759, + "Kr85": 84.912527262, + "Rb85": 84.9117897376, + "Sr85": 84.912932043, + "Y85": 84.916433039, + "Zr85": 84.921443198, + "Nb85": 84.928845837, + "Mo85": 84.938260737, + "Tc85": 84.950778, + "Ru85": 84.966774, + "Ga86": 85.963414, + "Ge86": 85.946967, + "As86": 85.936701533, + "Se86": 85.924311733, + "Br86": 85.918805433, + "Kr86": 85.91061062627, + "Rb86": 85.911167443, + "Sr86": 85.90926072631, + "Y86": 85.914886098, + "Zr86": 85.916296815, + "Nb86": 85.925781535, + "Mo86": 85.931174817, + "Tc86": 85.944637, + "Ru86": 85.957305, + "Ga87": 86.968599, + "Ge87": 86.95268, + "As87": 86.940291718, + "Se87": 86.928688618, + "Br87": 86.920674018, + "Kr87": 86.913354759, + "Rb87": 86.909180531, + "Sr87": 86.90887749615, + "Y87": 86.910876102, + "Zr87": 86.914817339, + "Nb87": 86.920692472, + "Mo87": 86.928196201, + "Tc87": 86.938067187, + "Ru87": 86.951132, + "Ge88": 87.95691, + "As88": 87.94555, + "Se88": 87.931417491, + "Br88": 87.924083291, + "Kr88": 87.914447881, + "Rb88": 87.911315591, + "Sr88": 87.90561225561, + "Y88": 87.909501276, + "Zr88": 87.910220709, + "Nb88": 87.918224287, + "Mo88": 87.921967781, + "Tc88": 87.933782381, + "Ru88": 87.941664, + "Rh88": 87.960429, + "Ge89": 88.96379, + "As89": 88.94976, + "Se89": 88.936669059, + "Br89": 88.926704559, + "Kr89": 88.91783545, + "Rb89": 88.912278137, + "Sr89": 88.907450808, + "Y89": 88.905841205, + "Zr89": 88.908882332, + "Nb89": 88.913445272, + "Mo89": 88.91946815, + "Tc89": 88.92764865, + "Ru89": 88.937455, + "Rh89": 88.950767, + "Ge90": 89.96863, + "As90": 89.95563, + "Se90": 89.940096, + "Br90": 89.93129285, + "Kr90": 89.91952793, + "Rb90": 89.914798803, + "Sr90": 89.907730885, + "Y90": 89.9071448, + "Zr90": 89.904698758, + "Nb90": 89.911259204, + "Mo90": 89.913931272, + "Tc90": 89.924073921, + "Ru90": 89.930344379, + "Rh90": 89.944498, + "Pd90": 89.95737, + "As91": 90.96039, + "Se91": 90.9457, + "Br91": 90.934398618, + "Kr91": 90.92380631, + "Rb91": 90.916537265, + "Sr91": 90.910195958, + "Y91": 90.907298066, + "Zr91": 90.905640223, + "Nb91": 90.906990274, + "Mo91": 90.911745195, + "Tc91": 90.918424975, + "Ru91": 90.926741532, + "Rh91": 90.937123, + "Pd91": 90.950692, + "As92": 91.96674, + "Se92": 91.94984, + "Br92": 91.939631597, + "Kr92": 91.926173094, + "Rb92": 91.919728481, + "Sr92": 91.911038224, + "Y92": 91.908945745, + "Zr92": 91.905035322, + "Nb92": 91.907188568, + "Mo92": 91.906807155, + "Tc92": 91.915269779, + "Ru92": 91.920234375, + "Rh92": 91.932367694, + "Pd92": 91.941406, + "Ag92": 91.960139, + "Se93": 92.95629, + "Br93": 92.94322, + "Kr93": 92.931147174, + "Rb93": 92.922039325, + "Sr93": 92.914024311, + "Y93": 92.909578422, + "Zr93": 92.906470646, + "Nb93": 92.906373161, + "Mo93": 92.906808773, + "Tc93": 92.910245149, + "Ru93": 92.917104444, + "Rh93": 92.925912781, + "Pd93": 92.93666, + "Ag93": 92.95033, + "Se94": 93.96049, + "Br94": 93.949114, + "Kr94": 93.934140454, + "Rb94": 93.926394818, + "Sr94": 93.915355643, + "Y94": 93.911592063, + "Zr94": 93.906312524, + "Nb94": 93.907278992, + "Mo94": 93.905083592, + "Tc94": 93.909652325, + "Ru94": 93.911342863, + "Rh94": 93.921730453, + "Pd94": 93.929036292, + "Ag94": 93.943736, + "Cd94": 93.956908, + "Se95": 94.9673, + "Br95": 94.95301, + "Kr95": 94.939710923, + "Rb95": 94.929262568, + "Sr95": 94.91935584, + "Y95": 94.912818711, + "Zr95": 94.908040267, + "Nb95": 94.906831115, + "Mo95": 94.905837442, + "Tc95": 94.907652287, + "Ru95": 94.91040442, + "Rh95": 94.915897895, + "Pd95": 94.924888512, + "Ag95": 94.93602, + "Cd95": 94.94994, + "Br96": 95.95903, + "Kr96": 95.943016618, + "Rb96": 95.934133393, + "Sr96": 95.921712692, + "Y96": 95.915902953, + "Zr96": 95.908277621, + "Nb96": 95.908101591, + "Mo96": 95.904674774, + "Tc96": 95.907866681, + "Ru96": 95.907588914, + "Rh96": 95.91445171, + "Pd96": 95.918213744, + "Ag96": 95.930743906, + "Cd96": 95.94034, + "In96": 95.959323, + "Br97": 96.96344, + "Kr97": 96.949088784, + "Rb97": 96.937177118, + "Sr97": 96.926374776, + "Y97": 96.918280286, + "Zr97": 96.910957386, + "Nb97": 96.908098414, + "Mo97": 96.906016903, + "Tc97": 96.906360723, + "Ru97": 96.907545779, + "Rh97": 96.911327876, + "Pd97": 96.916471987, + "Ag97": 96.923965326, + "Cd97": 96.9351, + "In97": 96.94934, + "Br98": 97.969672, + "Kr98": 97.95243, + "Rb98": 97.941632317, + "Sr98": 97.92869186, + "Y98": 97.92238836, + "Zr98": 97.912735124, + "Nb98": 97.91033265, + "Mo98": 97.905403608, + "Tc98": 97.907211205, + "Ru98": 97.905286713, + "Rh98": 97.91070774, + "Pd98": 97.912698337, + "Ag98": 97.921559972, + "Cd98": 97.927389317, + "In98": 97.94214, + "Kr99": 98.95839, + "Rb99": 98.94511919199999, + "Sr99": 98.932880511, + "Y99": 98.924154288, + "Zr99": 98.916670835, + "Nb99": 98.911609371, + "Mo99": 98.907707298, + "Tc99": 98.906249678, + "Ru99": 98.905930278, + "Rh99": 98.90812469, + "Pd99": 98.91177329, + "Ag99": 98.917645768, + "Cd99": 98.924925847, + "In99": 98.93411, + "Sn99": 98.94853, + "Kr100": 99.96236999999999, + "Rb100": 99.950351731, + "Sr100": 99.935779615, + "Y100": 99.927721063, + "Zr100": 99.918005444, + "Nb100": 99.914333963, + "Mo100": 99.907467976, + "Tc100": 99.907652711, + "Ru100": 99.904210452, + "Rh100": 99.908114141, + "Pd100": 99.908520315, + "Ag100": 99.916115445, + "Cd100": 99.92034882, + "In100": 99.93095718000001, + "Sn100": 99.938504196, + "Kr101": 100.96873, + "Rb101": 100.954004, + "Sr101": 100.940606266, + "Y101": 100.930154138, + "Zr101": 100.92145311, + "Nb101": 100.915306496, + "Mo101": 100.910337641, + "Tc101": 100.90730526, + "Ru101": 100.905573075, + "Rh101": 100.906158905, + "Pd101": 100.908284828, + "Ag101": 100.912683953, + "Cd101": 100.918586211, + "In101": 100.92634, + "Sn101": 100.935259244, + "Rb102": 101.95952, + "Sr102": 101.94400468, + "Y102": 101.934327889, + "Zr102": 101.923147431, + "Nb102": 101.918083697, + "Mo102": 101.910288138, + "Tc102": 101.909207275, + "Ru102": 101.9043403, + "Rh102": 101.90683427, + "Pd102": 101.905632058, + "Ag102": 101.91170454, + "Cd102": 101.914481799, + "In102": 101.924105916, + "Sn102": 101.93028953, + "Rb103": 102.96392, + "Sr103": 102.94909, + "Y103": 102.937243208, + "Zr103": 102.92719724, + "Nb103": 102.919453403, + "Mo103": 102.91308514, + "Tc103": 102.909174008, + "Ru103": 102.906314833, + "Rh103": 102.905494068, + "Pd103": 102.90611084, + "Ag103": 102.90896056, + "Cd103": 102.913416923, + "In103": 102.919878613, + "Sn103": 102.928101962, + "Sb103": 102.93969, + "Sr104": 103.95265, + "Y104": 103.94196, + "Zr104": 103.929442315, + "Nb104": 103.922899115, + "Mo104": 103.913740756, + "Tc104": 103.911428905, + "Ru104": 103.90542536, + "Rh104": 103.906645295, + "Pd104": 103.904030401, + "Ag104": 103.908623725, + "Cd104": 103.90985623, + "In104": 103.91821454, + "Sn104": 103.923105197, + "Sb104": 103.936474502, + "Sr105": 104.95855, + "Y105": 104.944959, + "Zr105": 104.93401489, + "Nb105": 104.924942564, + "Mo105": 104.916975159, + "Tc105": 104.911657952, + "Ru105": 104.907745525, + "Rh105": 104.905687806, + "Pd105": 104.905079487, + "Ag105": 104.906525607, + "Cd105": 104.909463895, + "In105": 104.914502324, + "Sn105": 104.921268423, + "Sb105": 104.931276549, + "Te105": 104.943304508, + "Sr106": 105.962651, + "Y106": 105.95056, + "Zr106": 105.937144, + "Nb106": 105.928927768, + "Mo106": 105.918266218, + "Tc106": 105.914356697, + "Ru106": 105.907328203, + "Rh106": 105.907285901, + "Pd106": 105.903480293, + "Ag106": 105.906663507, + "Cd106": 105.906459797, + "In106": 105.913463603, + "Sn106": 105.916957396, + "Sb106": 105.928637982, + "Te106": 105.937498526, + "Sr107": 106.968975, + "Y107": 106.95452, + "Zr107": 106.941621, + "Nb107": 106.931589672, + "Mo107": 106.922112692, + "Tc107": 106.915458485, + "Ru107": 106.909969885, + "Rh107": 106.906747974, + "Pd107": 106.905128064, + "Ag107": 106.905091531, + "Cd107": 106.906612108, + "In107": 106.910290071, + "Sn107": 106.915713651, + "Sb107": 106.924150624, + "Te107": 106.935008356, + "I107": 106.946935, + "Y108": 107.95996, + "Zr108": 107.94487, + "Nb108": 107.936074988, + "Mo108": 107.924040367, + "Tc108": 107.918493541, + "Ru108": 107.910185841, + "Rh108": 107.908714688, + "Pd108": 107.903891805, + "Ag108": 107.905950266, + "Cd108": 107.904183587, + "In108": 107.909693655, + "Sn108": 107.911894292, + "Sb108": 107.922226734, + "Te108": 107.929380471, + "I108": 107.943478321, + "Y109": 108.964358, + "Zr109": 108.95041, + "Nb109": 108.939141, + "Mo109": 108.928431106, + "Tc109": 108.920254156, + "Ru109": 108.913323756, + "Rh109": 108.908749326, + "Pd109": 108.905950574, + "Ag109": 108.904755773, + "Cd109": 108.904986698, + "In109": 108.907149685, + "Sn109": 108.911292843, + "Sb109": 108.918141204, + "Te109": 108.927304534, + "I109": 108.938086025, + "Xe109": 108.950434948, + "Zr110": 109.95396, + "Nb110": 109.943843, + "Mo110": 109.93071068, + "Tc110": 109.923741312, + "Ru110": 109.914038548, + "Rh110": 109.911079742, + "Pd110": 109.905172868, + "Ag110": 109.906110719, + "Cd110": 109.90300746, + "In110": 109.907170665, + "Sn110": 109.907844835, + "Sb110": 109.916854286, + "Te110": 109.922458104, + "I110": 109.935089033, + "Xe110": 109.944258765, + "Zr111": 110.959678, + "Nb111": 110.94753, + "Mo111": 110.935652016, + "Tc111": 110.925899016, + "Ru111": 110.917567616, + "Rh111": 110.911642531, + "Pd111": 110.907690347, + "Ag111": 110.905296816, + "Cd111": 110.904183766, + "In111": 110.905107233, + "Sn111": 110.907741126, + "Sb111": 110.913218189, + "Te111": 110.921000589, + "I111": 110.930269239, + "Xe111": 110.941603989, + "Cs111": 110.95403, + "Zr112": 111.963703, + "Nb112": 111.95247, + "Mo112": 111.93831, + "Tc112": 111.929941644, + "Ru112": 111.918806972, + "Rh112": 111.914404705, + "Pd112": 111.907329986, + "Ag112": 111.90704855, + "Cd112": 111.902763883, + "In112": 111.905538704, + "Sn112": 111.904824877, + "Sb112": 111.912399903, + "Te112": 111.91672785, + "I112": 111.92800455, + "Xe112": 111.935559071, + "Cs112": 111.950305341, + "Nb113": 112.95651, + "Mo113": 112.94365, + "Tc113": 112.932569033, + "Ru113": 112.922846396, + "Rh113": 112.91543956699999, + "Pd113": 112.910261267, + "Ag113": 112.906572858, + "Cd113": 112.904408097, + "In113": 112.904060448, + "Sn113": 112.905175845, + "Sb113": 112.909374652, + "Te113": 112.915891, + "I113": 112.923650064, + "Xe113": 112.933221666, + "Cs113": 112.944428488, + "Ba113": 112.95729, + "Nb114": 113.96201, + "Mo114": 113.94653, + "Tc114": 113.93709, + "Ru114": 113.92461378, + "Rh114": 113.918721296, + "Pd114": 113.91036878, + "Ag114": 113.908823031, + "Cd114": 113.90336499, + "In114": 113.904916402, + "Sn114": 113.902780132, + "Sb114": 113.909289191, + "Te114": 113.912089, + "I114": 113.92185, + "Xe114": 113.927980331, + "Cs114": 113.941296175, + "Ba114": 113.950718495, + "Nb115": 114.96634, + "Mo115": 114.95196, + "Tc115": 114.939538, + "Ru115": 114.928942393, + "Rh115": 114.920310993, + "Pd115": 114.913658718, + "Ag115": 114.908767363, + "Cd115": 114.905437417, + "In115": 114.903878773, + "Sn115": 114.903344697, + "Sb115": 114.906598, + "Te115": 114.911902, + "I115": 114.918048, + "Xe115": 114.926293945, + "Cs115": 114.93591, + "Ba115": 114.947375, + "Mo116": 115.955448, + "Tc116": 115.94476, + "Ru116": 115.931219193, + "Rh116": 115.924061645, + "Pd116": 115.91429721, + "Ag116": 115.911386812, + "Cd116": 115.90476323, + "In116": 115.905259992, + "Sn116": 115.901742824, + "Sb116": 115.906792583, + "Te116": 115.90846, + "I116": 115.916808658, + "Xe116": 115.921581112, + "Cs116": 115.933395, + "Ba116": 115.941406, + "La116": 115.956365, + "Mo117": 116.96117, + "Tc117": 116.94806, + "Ru117": 116.936135, + "Rh117": 116.926035623, + "Pd117": 116.917954944, + "Ag117": 116.911773974, + "Cd117": 116.907226038, + "In117": 116.904515712, + "Sn117": 116.902954017, + "Sb117": 116.904841535, + "Te117": 116.908646313, + "I117": 116.913648314, + "Xe117": 116.92035876, + "Cs117": 116.928616726, + "Ba117": 116.938316561, + "La117": 116.950111, + "Mo118": 117.96497, + "Tc118": 117.95299, + "Ru118": 117.938529, + "Rh118": 117.930340443, + "Pd118": 117.919066847, + "Ag118": 117.914595487, + "Cd118": 117.906921955, + "In118": 117.906356659, + "Sn118": 117.901606609, + "Sb118": 117.905532174, + "Te118": 117.905853839, + "I118": 117.913074, + "Xe118": 117.91617868, + "Cs118": 117.926559519, + "Ba118": 117.93306, + "La118": 117.946795, + "Tc119": 118.95666, + "Ru119": 118.94357, + "Rh119": 118.932556952, + "Pd119": 118.923340459, + "Ag119": 118.915570293, + "Cd119": 118.909846903, + "In119": 118.905850944, + "Sn119": 118.903311216, + "Sb119": 118.903945512, + "Te119": 118.906407148, + "I119": 118.910074, + "Xe119": 118.915410713, + "Cs119": 118.92237733, + "Ba119": 118.930659686, + "La119": 118.941181, + "Ce119": 118.952828, + "Tc120": 119.96187, + "Ru120": 119.94631, + "Rh120": 119.93686, + "Pd120": 119.924551258, + "Ag120": 119.918784767, + "Cd120": 119.909868067, + "In120": 119.907966805, + "Sn120": 119.902201873, + "Sb120": 119.905079624, + "Te120": 119.904059514, + "I120": 119.910087465, + "Xe120": 119.91178427, + "Cs120": 119.920677279, + "Ba120": 119.926045, + "La120": 119.938196, + "Ce120": 119.946752, + "Tc121": 120.965883, + "Ru121": 120.95164, + "Rh121": 120.939613, + "Pd121": 120.928950343, + "Ag121": 120.920125282, + "Cd121": 120.912963663, + "In121": 120.907851286, + "Sn121": 120.904242792, + "Sb121": 120.903810093, + "Te121": 120.904942488, + "I121": 120.907405255, + "Xe121": 120.911453014, + "Cs121": 120.917227238, + "Ba121": 120.924052289, + "La121": 120.933236, + "Ce121": 120.943435, + "Pr121": 120.955532, + "Ru122": 121.95475, + "Rh122": 121.94409, + "Pd122": 121.930631694, + "Ag122": 121.923664448, + "Cd122": 121.913459052, + "In122": 121.910280966, + "Sn122": 121.903444001, + "Sb122": 121.905168074, + "Te122": 121.903043434, + "I122": 121.90758882, + "Xe122": 121.908367658, + "Cs122": 121.916108145, + "Ba122": 121.919904, + "La122": 121.93071, + "Ce122": 121.93787, + "Pr122": 121.951927, + "Ru123": 122.960193, + "Rh123": 122.94701, + "Pd123": 122.935126, + "Ag123": 122.925337062, + "Cd123": 122.916892453, + "In123": 122.910433826, + "Sn123": 122.905725446, + "Sb123": 122.904214016, + "Te123": 122.904269747, + "I123": 122.90558852, + "Xe123": 122.90848174999999, + "Cs123": 122.912996062, + "Ba123": 122.918781062, + "La123": 122.9263, + "Ce123": 122.93528, + "Pr123": 122.946076, + "Ru124": 123.963542, + "Rh124": 123.951809, + "Pd124": 123.937316, + "Ag124": 123.928931229, + "Cd124": 123.917657363, + "In124": 123.913182263, + "Sn124": 123.905276692, + "Sb124": 123.905935789, + "Te124": 123.902817064, + "I124": 123.906209021, + "Xe124": 123.905891588, + "Cs124": 123.912257798, + "Ba124": 123.915093629, + "La124": 123.924574275, + "Ce124": 123.93031, + "Pr124": 123.94294, + "Nd124": 123.9522, + "Rh125": 124.954911, + "Pd125": 124.9419, + "Ag125": 124.930735, + "Cd125": 124.921257577, + "In125": 124.913604591, + "Sn125": 124.907786442, + "Sb125": 124.905252987, + "Te125": 124.9044299, + "I125": 124.904629333, + "Xe125": 124.90639405, + "Cs125": 124.909727867, + "Ba125": 124.914471843, + "La125": 124.920815932, + "Ce125": 124.92844, + "Pr125": 124.937799, + "Nd125": 124.9489, + "Rh126": 125.959957, + "Pd126": 125.944326, + "Ag126": 125.934857, + "Cd126": 125.922429127, + "In126": 125.916507344, + "Sn126": 125.907658836, + "Sb126": 125.907253036, + "Te126": 125.903310866, + "I126": 125.905623313, + "Xe126": 125.904296794, + "Cs126": 125.909445655, + "Ba126": 125.911250204, + "La126": 125.919512667, + "Ce126": 125.923971, + "Pr126": 125.93524, + "Nd126": 125.94311, + "Pm126": 125.957756, + "Rh127": 126.963467, + "Pd127": 126.94935, + "Ag127": 126.937262, + "Cd127": 126.926196624, + "In127": 126.917448546, + "Sn127": 126.910390401, + "Sb127": 126.906924277, + "Te127": 126.905225714, + "I127": 126.904471838, + "Xe127": 126.905182899, + "Cs127": 126.907417381, + "Ba127": 126.911091275, + "La127": 126.916375084, + "Ce127": 126.922727, + "Pr127": 126.93071, + "Nd127": 126.94038, + "Pm127": 126.95192, + "Pd128": 127.952238, + "Ag128": 127.941363, + "Cd128": 127.927812857, + "In128": 127.920401053, + "Sn128": 127.910507197, + "Sb128": 127.909145645, + "Te128": 127.904461311, + "I128": 127.9058086, + "Xe128": 127.903530996, + "Cs128": 127.907748648, + "Ba128": 127.908342408, + "La128": 127.915592123, + "Ce128": 127.918911, + "Pr128": 127.928791, + "Nd128": 127.93525, + "Pm128": 127.9487, + "Sm128": 127.958486, + "Pd129": 128.959624, + "Ag129": 128.944197, + "Cd129": 128.932304399, + "In129": 128.921805486, + "Sn129": 128.913482102, + "Sb129": 128.909146696, + "Te129": 128.906596492, + "I129": 128.904983687, + "Xe129": 128.90478085892, + "Cs129": 128.90606569, + "Ba129": 128.908680896, + "La129": 128.912694475, + "Ce129": 128.918102, + "Pr129": 128.925095, + "Nd129": 128.933102, + "Pm129": 128.94323, + "Sm129": 128.954911, + "Ag130": 129.950942, + "Cd130": 129.934387566, + "In130": 129.924977288, + "Sn130": 129.913974533, + "Sb130": 129.911662688, + "Te130": 129.906222747, + "I130": 129.906670211, + "Xe130": 129.903509349, + "Cs130": 129.906709283, + "Ba130": 129.906320874, + "La130": 129.912369413, + "Ce130": 129.914736, + "Pr130": 129.92359, + "Nd130": 129.928506, + "Pm130": 129.94053, + "Sm130": 129.949, + "Eu130": 129.96384, + "Ag131": 130.95665, + "Cd131": 130.94072, + "In131": 130.926972122, + "Sn131": 130.917053066, + "Sb131": 130.911989341, + "Te131": 130.908522211, + "I131": 130.906126384, + "Xe131": 130.905084136, + "Cs131": 130.905464999, + "Ba131": 130.906941181, + "La131": 130.91007, + "Ce131": 130.914429465, + "Pr131": 130.92023496, + "Nd131": 130.92724802, + "Pm131": 130.935952, + "Sm131": 130.94618, + "Eu131": 130.957842, + "Ag132": 131.963725, + "Cd132": 131.94604, + "In132": 131.932998449, + "Sn132": 131.917823902, + "Sb132": 131.914508015, + "Te132": 131.908546716, + "I132": 131.907993514, + "Xe132": 131.90415508697, + "Cs132": 131.906437743, + "Ba132": 131.905061098, + "La132": 131.910118959, + "Ce132": 131.911463846, + "Pr132": 131.91924, + "Nd132": 131.923321237, + "Pm132": 131.93384, + "Sm132": 131.94087, + "Eu132": 131.954696, + "Cd133": 132.95285, + "In133": 132.93831, + "Sn133": 132.923913756, + "Sb133": 132.91527213, + "Te133": 132.910963332, + "I133": 132.907827361, + "Xe133": 132.90591075, + "Cs133": 132.905451961, + "Ba133": 132.906007325, + "La133": 132.908218, + "Ce133": 132.911520402, + "Pr133": 132.916330561, + "Nd133": 132.922348, + "Pm133": 132.929782, + "Sm133": 132.93856, + "Eu133": 132.94929, + "Gd133": 132.961503, + "Cd134": 133.958218, + "In134": 133.94454, + "Sn134": 133.928680433, + "Sb134": 133.920535675, + "Te134": 133.911396379, + "I134": 133.909775663, + "Xe134": 133.905393033, + "Cs134": 133.906718503, + "Ba134": 133.904508399, + "La134": 133.908514011, + "Ce134": 133.908928142, + "Pr134": 133.915696729, + "Nd134": 133.91879021, + "Pm134": 133.928353, + "Sm134": 133.93411, + "Eu134": 133.9464, + "Gd134": 133.95566, + "In135": 134.95005, + "Sn135": 134.934908605, + "Sb135": 134.925184357, + "Te135": 134.916554718, + "I135": 134.910059382, + "Xe135": 134.907231661, + "Cs135": 134.905977234, + "Ba135": 134.905688606, + "La135": 134.906984568, + "Ce135": 134.909160799, + "Pr135": 134.913111774, + "Nd135": 134.91818132, + "Pm135": 134.924796, + "Sm135": 134.93252, + "Eu135": 134.94187, + "Gd135": 134.952345, + "Tb135": 134.96476, + "In136": 135.956511, + "Sn136": 135.93999, + "Sb136": 135.930749011, + "Te136": 135.920101182, + "I136": 135.914604695, + "Xe136": 135.907214476, + "Cs136": 135.90731159, + "Ba136": 135.904575959, + "La136": 135.907634962, + "Ce136": 135.907129438, + "Pr136": 135.912677532, + "Nd136": 135.914976064, + "Pm136": 135.923595949, + "Sm136": 135.928275555, + "Eu136": 135.93962, + "Gd136": 135.9473, + "Tb136": 135.961213, + "In137": 136.962383, + "Sn137": 136.94655, + "Sb137": 136.935522522, + "Te137": 136.925599357, + "I137": 136.91802818, + "Xe137": 136.911557773, + "Cs137": 136.907089464, + "Ba137": 136.905827375, + "La137": 136.906450618, + "Ce137": 136.907762596, + "Pr137": 136.910679304, + "Nd137": 136.914562448, + "Pm137": 136.920479522, + "Sm137": 136.926970517, + "Eu137": 136.935430722, + "Gd137": 136.94502, + "Tb137": 136.95602, + "Sn138": 137.95184, + "Sb138": 137.941792, + "Te138": 137.929472454, + "I138": 137.922726394, + "Xe138": 137.914146271, + "Cs138": 137.911017207, + "Ba138": 137.905247229, + "La138": 137.907117834, + "Ce138": 137.905988743, + "Pr138": 137.910752059, + "Nd138": 137.911949717, + "Pm138": 137.919548077, + "Sm138": 137.92324399, + "Eu138": 137.933709, + "Gd138": 137.940096, + "Tb138": 137.95312, + "Dy138": 137.9625, + "Sn139": 138.958733, + "Sb139": 138.94655, + "Te139": 138.935367193, + "I139": 138.926493403, + "Xe139": 138.918792203, + "Cs139": 138.913363992, + "Ba139": 138.908841334, + "La139": 138.906358804, + "Ce139": 138.906657625, + "Pr139": 138.90894327, + "Nd139": 138.911954407, + "Pm139": 138.916799806, + "Sm139": 138.922296634, + "Eu139": 138.92979231, + "Gd139": 138.93813, + "Tb139": 138.94833, + "Dy139": 138.95959, + "Sb140": 139.95283, + "Te140": 139.939262917, + "I140": 139.931715917, + "Xe140": 139.921645817, + "Cs140": 139.917283305, + "Ba140": 139.910606666, + "La140": 139.909483184, + "Ce140": 139.905446424, + "Pr140": 139.909083592, + "Nd140": 139.909544332, + "Pm140": 139.916034122, + "Sm140": 139.918994717, + "Eu140": 139.928087637, + "Gd140": 139.933674, + "Tb140": 139.945805049, + "Dy140": 139.95402, + "Ho140": 139.968589, + "Sb141": 140.958014, + "Te141": 140.9458, + "I141": 140.935666084, + "Xe141": 140.926787184, + "Cs141": 140.920045086, + "Ba141": 140.9144035, + "La141": 140.910969222, + "Ce141": 140.908283987, + "Pr141": 140.907658403, + "Nd141": 140.909615488, + "Pm141": 140.913555084, + "Sm141": 140.918481591, + "Eu141": 140.924931745, + "Gd141": 140.932126, + "Tb141": 140.941448, + "Dy141": 140.95128, + "Ho141": 140.963108, + "Te142": 141.95022, + "I142": 141.941202, + "Xe142": 141.929973098, + "Cs142": 141.924299512, + "Ba142": 141.916432888, + "La142": 141.914090454, + "Ce142": 141.909249884, + "Pr142": 141.91005044, + "Nd142": 141.907728895, + "Pm142": 141.912890428, + "Sm142": 141.915204532, + "Eu142": 141.923441836, + "Gd142": 141.928116, + "Tb142": 141.939280859, + "Dy142": 141.946194, + "Ho142": 141.96001, + "Er142": 141.969909, + "Te143": 142.95676, + "I143": 142.945646, + "Xe143": 142.935369553, + "Cs143": 142.927347348, + "Ba143": 142.92062515, + "La143": 142.916079422, + "Ce143": 142.91239163, + "Pr143": 142.910822564, + "Nd143": 142.909819887, + "Pm143": 142.910938073, + "Sm143": 142.914634821, + "Eu143": 142.920298681, + "Gd143": 142.926750682, + "Tb143": 142.935137335, + "Dy143": 142.943994335, + "Ho143": 142.95486, + "Er143": 142.966441, + "I144": 143.95139, + "Xe144": 143.938945079, + "Cs144": 143.932075404, + "Ba144": 143.922954821, + "La144": 143.919645589, + "Ce144": 143.91365283, + "Pr144": 143.91331075, + "Nd144": 143.910092865, + "Pm144": 143.912596224, + "Sm144": 143.912006373, + "Eu144": 143.918819517, + "Gd144": 143.922963, + "Tb144": 143.933045, + "Dy144": 143.939269514, + "Ho144": 143.952109714, + "Er144": 143.9607, + "Tm144": 143.976104, + "I145": 144.95605, + "Xe145": 144.944719634, + "Cs145": 144.93552893, + "Ba145": 144.9275184, + "La145": 144.921808066, + "Ce145": 144.917265144, + "Pr145": 144.914518033, + "Nd145": 144.912579199, + "Pm145": 144.912755773, + "Sm145": 144.913417244, + "Eu145": 144.916272668, + "Gd145": 144.92171037, + "Tb145": 144.928729105, + "Dy145": 144.937473994, + "Ho145": 144.947267394, + "Er145": 144.957874, + "Tm145": 144.970389, + "Xe146": 145.948518248, + "Cs146": 145.94062187, + "Ba146": 145.930276431, + "La146": 145.925871468, + "Ce146": 145.918802065, + "Pr146": 145.917679549, + "Nd146": 145.913122503, + "Pm146": 145.914702286, + "Sm146": 145.913046881, + "Eu146": 145.917210909, + "Gd146": 145.918318548, + "Tb146": 145.927252768, + "Dy146": 145.932844529, + "Ho146": 145.944993506, + "Er146": 145.952418359, + "Tm146": 145.966661, + "Xe147": 146.954525, + "Cs147": 146.944261515, + "Ba147": 146.9353039, + "La147": 146.9284178, + "Ce147": 146.922689903, + "Pr147": 146.919007458, + "Nd147": 146.91610601, + "Pm147": 146.915144638, + "Sm147": 146.914904064, + "Eu147": 146.916752276, + "Gd147": 146.919100987, + "Tb147": 146.92405462, + "Dy147": 146.931082715, + "Ho147": 146.940142295, + "Er147": 146.949964458, + "Tm147": 146.96137989, + "Xe148": 147.958561, + "Cs148": 147.949639029, + "Ba148": 147.938170578, + "La148": 147.9326794, + "Ce148": 147.924424196, + "Pr148": 147.922130015, + "Nd148": 147.916899093, + "Pm148": 147.917481255, + "Sm148": 147.914829012, + "Eu148": 147.918089294, + "Gd148": 147.918121503, + "Tb148": 147.924275323, + "Dy148": 147.927149772, + "Ho148": 147.937743928, + "Er148": 147.944735029, + "Tm148": 147.958384029, + "Yb148": 147.967439, + "Cs149": 148.953569, + "Ba149": 148.942973, + "La149": 148.93535126, + "Ce149": 148.9284269, + "Pr149": 148.9237361, + "Nd149": 148.920154648, + "Pm149": 148.918341658, + "Sm149": 148.917191375, + "Eu149": 148.917937086, + "Gd149": 148.919347831, + "Tb149": 148.923253753, + "Dy149": 148.927325448, + "Ho149": 148.933819672, + "Er149": 148.942306, + "Tm149": 148.95289, + "Yb149": 148.96436, + "Cs150": 149.959023, + "Ba150": 149.94643, + "La150": 149.939742, + "Ce150": 149.930384035, + "Pr150": 149.926676415, + "Nd150": 149.920901525, + "Pm150": 149.920990217, + "Sm150": 149.917282195, + "Eu150": 149.919707229, + "Gd150": 149.918664066, + "Tb150": 149.923664864, + "Dy150": 149.92559308, + "Ho150": 149.933498358, + "Er150": 149.937915528, + "Tm150": 149.95009, + "Yb150": 149.95852, + "Lu150": 149.973548, + "Cs151": 150.963253, + "Ba151": 150.951755, + "La151": 150.942769, + "Ce151": 150.9342722, + "Pr151": 150.928309114, + "Nd151": 150.923839565, + "Pm151": 150.921216817, + "Sm151": 150.919939066, + "Eu151": 150.91985686, + "Gd151": 150.920355109, + "Tb151": 150.923109001, + "Dy151": 150.926191253, + "Ho151": 150.931698177, + "Er151": 150.937448567, + "Tm151": 150.945493201, + "Yb151": 150.955402458, + "Lu151": 150.967677, + "Cs152": 151.968942, + "Ba152": 151.955222, + "La152": 151.947085, + "Ce152": 151.936682, + "Pr152": 151.9315529, + "Nd152": 151.924691509, + "Pm152": 151.923505481, + "Sm152": 151.91973904, + "Eu152": 151.921751235, + "Gd152": 151.919798822, + "Tb152": 151.924082263, + "Dy152": 151.924725363, + "Ho152": 151.931717465, + "Er152": 151.935050169, + "Tm152": 151.944476, + "Yb152": 151.9503267, + "Lu152": 151.96412, + "Ba153": 152.960848, + "La153": 152.950553, + "Ce153": 152.941052, + "Pr153": 152.933903532, + "Nd153": 152.927717949, + "Pm153": 152.924156436, + "Sm153": 152.922103969, + "Eu153": 152.921237043, + "Gd153": 152.921757359, + "Tb153": 152.923441978, + "Dy153": 152.925771992, + "Ho153": 152.930206632, + "Er153": 152.935084279, + "Tm153": 152.942057244, + "Yb153": 152.94932, + "Lu153": 152.958805054, + "Hf153": 152.970692, + "Ba154": 153.964766, + "La154": 153.955416, + "Ce154": 153.94394, + "Pr154": 153.937621738, + "Nd154": 153.929333977, + "Pm154": 153.926449364, + "Sm154": 153.922216164, + "Eu154": 153.922985955, + "Gd154": 153.920873398, + "Tb154": 153.924684106, + "Dy154": 153.924429028, + "Ho154": 153.930606841, + "Er154": 153.932790743, + "Tm154": 153.941570067, + "Yb154": 153.946395701, + "Lu154": 153.957364, + "Hf154": 153.964927, + "La155": 154.95928, + "Ce155": 154.948706, + "Pr155": 154.940509259, + "Nd155": 154.933135668, + "Pm155": 154.928137024, + "Sm155": 154.924647051, + "Eu155": 154.922900102, + "Gd155": 154.922629796, + "Tb155": 154.923509921, + "Dy155": 154.925758459, + "Ho155": 154.929103634, + "Er155": 154.933215684, + "Tm155": 154.939209578, + "Yb155": 154.945783217, + "Lu155": 154.954326011, + "Hf155": 154.963317, + "Ta155": 154.974312, + "La156": 155.964519, + "Ce156": 155.951884, + "Pr156": 155.94464, + "Nd156": 155.935078868, + "Pm156": 155.93111749, + "Sm156": 155.925538511, + "Eu156": 155.924763285, + "Gd156": 155.922130562, + "Tb156": 155.92475443, + "Dy156": 155.924284038, + "Ho156": 155.929705436, + "Er156": 155.93106589, + "Tm156": 155.938985597, + "Yb156": 155.942816893, + "Lu156": 155.953086606, + "Hf156": 155.959401889, + "Ta156": 155.972237, + "Ce157": 156.957133, + "Pr157": 156.94789, + "Nd157": 156.939386037, + "Pm157": 156.93312137, + "Sm157": 156.928418673, + "Eu157": 156.925432791, + "Gd157": 156.92396787, + "Tb157": 156.924032328, + "Dy157": 156.925469667, + "Ho157": 156.928251999, + "Er157": 156.931922655, + "Tm157": 156.936973, + "Yb157": 156.94264923, + "Lu157": 156.950144045, + "Hf157": 156.958236, + "Ta157": 156.968230251, + "W157": 156.979098, + "Ce158": 157.960644, + "Pr158": 157.95241, + "Nd158": 157.94197, + "Pm158": 157.936565121, + "Sm158": 157.929950979, + "Eu158": 157.927798581, + "Gd158": 157.924111646, + "Tb158": 157.925420166, + "Dy158": 157.924414597, + "Ho158": 157.928944692, + "Er158": 157.929893474, + "Tm158": 157.936979525, + "Yb158": 157.939870534, + "Lu158": 157.949315626, + "Hf158": 157.954801222, + "Ta158": 157.966541, + "W158": 157.974629, + "Pr159": 158.95589, + "Nd159": 158.94653, + "Pm159": 158.939286479, + "Sm159": 158.933217202, + "Eu159": 158.929099612, + "Gd159": 158.926396267, + "Tb159": 158.925353933, + "Dy159": 158.925746023, + "Ho159": 158.927718768, + "Er159": 158.930690875, + "Tm159": 158.934975, + "Yb159": 158.940054787, + "Lu159": 158.946635615, + "Hf159": 158.953995838, + "Ta159": 158.963028052, + "W159": 158.972845, + "Re159": 158.984171, + "Pr160": 159.960794, + "Nd160": 159.9494, + "Pm160": 159.9431, + "Sm160": 159.935335286, + "Eu160": 159.931850916, + "Gd160": 159.927061537, + "Tb160": 159.927174778, + "Dy160": 159.925203244, + "Ho160": 159.928735204, + "Er160": 159.92907713, + "Tm160": 159.935263106, + "Yb160": 159.937559763, + "Lu160": 159.946033, + "Hf160": 159.950682513, + "Ta160": 159.961541679, + "W160": 159.968516753, + "Re160": 159.98203, + "Nd161": 160.95428, + "Pm161": 160.94607, + "Sm161": 160.939160143, + "Eu161": 160.933664066, + "Gd161": 160.929676602, + "Tb161": 160.927577001, + "Dy161": 160.926939088, + "Ho161": 160.927860759, + "Er161": 160.930003191, + "Tm161": 160.933549, + "Yb161": 160.937906846, + "Lu161": 160.943572, + "Hf161": 160.950279151, + "Ta161": 160.958369031, + "W161": 160.967197, + "Re161": 160.977627121, + "Os161": 160.989287, + "Nd162": 161.957541, + "Pm162": 161.95022, + "Sm162": 161.94146, + "Eu162": 161.936979303, + "Gd162": 161.930992146, + "Tb162": 161.929493955, + "Dy162": 161.926804168, + "Ho162": 161.929101485, + "Er162": 161.92878696, + "Tm162": 161.934000872, + "Yb162": 161.935773771, + "Lu162": 161.943282776, + "Hf162": 161.947214896, + "Ta162": 161.957294202, + "W162": 161.963500347, + "Re162": 161.975844, + "Os162": 161.984498, + "Pm163": 162.95357, + "Sm163": 162.94555, + "Eu163": 162.939360977, + "Gd163": 162.934176832, + "Tb163": 162.930653261, + "Dy163": 162.928736879, + "Ho163": 162.928739921, + "Er163": 162.930039567, + "Tm163": 162.932657941, + "Yb163": 162.9363398, + "Lu163": 162.941179, + "Hf163": 162.947113258, + "Ta163": 162.954337195, + "W163": 162.962524511, + "Re163": 162.972085441, + "Os163": 162.982617, + "Pm164": 163.958271, + "Sm164": 163.94836, + "Eu164": 163.942693, + "Gd164": 163.93583, + "Tb164": 163.933356559, + "Dy164": 163.929180472, + "Ho164": 163.930239483, + "Er164": 163.929207392, + "Tm164": 163.933543281, + "Yb164": 163.934495103, + "Lu164": 163.941339, + "Hf164": 163.944370544, + "Ta164": 163.953534, + "W164": 163.958952222, + "Re164": 163.970507124, + "Os164": 163.978075966, + "Ir164": 163.992116, + "Sm165": 164.95297, + "Eu165": 164.945546, + "Gd165": 164.939395, + "Tb165": 164.93498, + "Dy165": 164.931709054, + "Ho165": 164.930328047, + "Er165": 164.930733198, + "Tm165": 164.932442269, + "Yb165": 164.935270241, + "Lu165": 164.939406758, + "Hf165": 164.944567, + "Ta165": 164.950780303, + "W165": 164.958280974, + "Re165": 164.967085375, + "Os165": 164.976602, + "Ir165": 164.987555, + "Sm166": 165.956275, + "Eu166": 165.94932, + "Gd166": 165.94146, + "Tb166": 165.937858119, + "Dy166": 165.932812461, + "Ho166": 165.932290139, + "Er166": 165.930299023, + "Tm166": 165.933560092, + "Yb166": 165.933874249, + "Lu166": 165.939859, + "Hf166": 165.94218, + "Ta166": 165.950512, + "W166": 165.955031346, + "Re166": 165.96576094, + "Os166": 165.972698141, + "Ir166": 165.985664, + "Pt166": 165.994923, + "Eu167": 166.952753, + "Gd167": 166.94545, + "Tb167": 166.93996, + "Dy167": 166.935661823, + "Ho167": 166.933138994, + "Er167": 166.932054119, + "Tm167": 166.932856635, + "Yb167": 166.934953337, + "Lu167": 166.93827, + "Hf167": 166.9426, + "Ta167": 166.948093, + "W167": 166.954805873, + "Re167": 166.962607, + "Os167": 166.971548938, + "Ir167": 166.981671981, + "Pt167": 166.992901, + "Eu168": 167.957337, + "Gd168": 167.94808, + "Tb168": 167.9434, + "Dy168": 167.937133716, + "Ho168": 167.935521676, + "Er168": 167.932376192, + "Tm168": 167.934177868, + "Yb168": 167.933889106, + "Lu168": 167.938735139, + "Hf168": 167.940568, + "Ta168": 167.948047, + "W168": 167.951805262, + "Re168": 167.961572608, + "Os168": 167.967798812, + "Ir168": 167.979960981, + "Pt168": 167.988183004, + "Gd169": 168.9526, + "Tb169": 168.94597, + "Dy169": 168.940313971, + "Ho169": 168.93687863, + "Er169": 168.934596353, + "Tm169": 168.93421835, + "Yb169": 168.935182016, + "Lu169": 168.937643653, + "Hf169": 168.941259, + "Ta169": 168.946011, + "W169": 168.951778677, + "Re169": 168.958765991, + "Os169": 168.967017833, + "Ir169": 168.976281287, + "Pt169": 168.986567, + "Au169": 168.99808, + "Gd170": 169.955577, + "Tb170": 169.94984, + "Dy170": 169.94239, + "Ho170": 169.939625289, + "Er170": 169.935470673, + "Tm170": 169.935806507, + "Yb170": 169.934767245, + "Lu170": 169.938479234, + "Hf170": 169.939609, + "Ta170": 169.946175, + "W170": 169.9492312, + "Re170": 169.958224966, + "Os170": 169.963578673, + "Ir170": 169.974922, + "Pt170": 169.982502095, + "Au170": 169.995972, + "Tb171": 170.95273, + "Dy171": 170.94612, + "Ho171": 170.94147149, + "Er171": 170.938036148, + "Tm171": 170.936435126, + "Yb171": 170.936331517, + "Lu171": 170.93791866, + "Hf171": 170.940492, + "Ta171": 170.944476, + "W171": 170.949451, + "Re171": 170.955716, + "Os171": 170.963175348, + "Ir171": 170.971645522, + "Pt171": 170.981245502, + "Au171": 170.991881542, + "Hg171": 171.003736, + "Tb172": 171.957219, + "Dy172": 171.94846, + "Ho172": 171.94473, + "Er172": 171.939362344, + "Tm172": 171.938406067, + "Yb172": 171.936386658, + "Lu172": 171.939091417, + "Hf172": 171.939449716, + "Ta172": 171.944895, + "W172": 171.947292, + "Re172": 171.955408079, + "Os172": 171.960017088, + "Ir172": 171.970607036, + "Pt172": 171.977340788, + "Au172": 171.989996708, + "Hg172": 171.998863391, + "Dy173": 172.95283, + "Ho173": 172.94702, + "Er173": 172.9424, + "Tm173": 172.939606632, + "Yb173": 172.938216215, + "Lu173": 172.938935822, + "Hf173": 172.940513, + "Ta173": 172.94375, + "W173": 172.947689, + "Re173": 172.953243, + "Os173": 172.959808375, + "Ir173": 172.967505496, + "Pt173": 172.976443315, + "Au173": 172.986223808, + "Hg173": 172.997091, + "Dy174": 173.955587, + "Ho174": 173.95095, + "Er174": 173.94423, + "Tm174": 173.942174064, + "Yb174": 173.938867548, + "Lu174": 173.940342938, + "Hf174": 173.94004848, + "Ta174": 173.944454, + "W174": 173.946079, + "Re174": 173.953115, + "Os174": 173.957063152, + "Ir174": 173.966866676, + "Pt174": 173.972819832, + "Au174": 173.984718, + "Hg174": 173.992870583, + "Ho175": 174.95362, + "Er175": 174.94777, + "Tm175": 174.943842313, + "Yb175": 174.94128191, + "Lu175": 174.940777308, + "Hf175": 174.941511527, + "Ta175": 174.943737, + "W175": 174.946717, + "Re175": 174.951381, + "Os175": 174.956945105, + "Ir175": 174.964149521, + "Pt175": 174.972395457, + "Au175": 174.981316085, + "Hg175": 174.991441086, + "Ho176": 175.95782, + "Er176": 175.94994, + "Tm176": 175.946997711, + "Yb176": 175.942574708, + "Lu176": 175.942691809, + "Hf176": 175.941409905, + "Ta176": 175.944857, + "W176": 175.945634, + "Re176": 175.951623, + "Os176": 175.954806, + "Ir176": 175.963630119, + "Pt176": 175.968938214, + "Au176": 175.980116927, + "Hg176": 175.987348335, + "Tl176": 176.000624367, + "Er177": 176.95399, + "Tm177": 176.94904, + "Yb177": 176.945263848, + "Lu177": 176.943763668, + "Hf177": 176.94323032, + "Ta177": 176.944482073, + "W177": 176.946643, + "Re177": 176.950328, + "Os177": 176.954957882, + "Ir177": 176.9613015, + "Pt177": 176.968469529, + "Au177": 176.976870379, + "Hg177": 176.986277376, + "Tl177": 176.996413797, + "Er178": 177.956779, + "Tm178": 177.95264, + "Yb178": 177.94664971, + "Lu178": 177.945960162, + "Hf178": 177.943708456, + "Ta178": 177.945681, + "W178": 177.945885925, + "Re178": 177.950989, + "Os178": 177.9532533, + "Ir178": 177.961082, + "Pt178": 177.965649248, + "Au178": 177.976055945, + "Hg178": 177.982484158, + "Tl178": 177.994857, + "Pb178": 178.003837163, + "Tm179": 178.95534, + "Yb179": 178.95004, + "Lu179": 178.947333082, + "Hf179": 178.945825838, + "Ta179": 178.945939187, + "W179": 178.947079501, + "Re179": 178.949989715, + "Os179": 178.953816669, + "Ir179": 178.959117596, + "Pt179": 178.965358719, + "Au179": 178.973173668, + "Hg179": 178.981826899, + "Tl179": 178.991123405, + "Pb179": 179.002201452, + "Tm180": 179.959291, + "Yb180": 179.95212, + "Lu180": 179.949890876, + "Hf180": 179.946559669, + "Ta180": 179.947468392, + "W180": 179.946713435, + "Re180": 179.950791568, + "Os180": 179.95237993, + "Ir180": 179.959229446, + "Pt180": 179.963031563, + "Au180": 179.972489883, + "Hg180": 179.978260249, + "Tl180": 179.989923019, + "Pb180": 179.997915842, + "Tm181": 180.962243, + "Yb181": 180.95589, + "Lu181": 180.951908, + "Hf181": 180.949110965, + "Ta181": 180.947999331, + "W181": 180.948218863, + "Re181": 180.950061523, + "Os181": 180.953247188, + "Ir181": 180.957634694, + "Pt181": 180.963089927, + "Au181": 180.970079103, + "Hg181": 180.977819357, + "Tl181": 180.986259992, + "Pb181": 180.996653386, + "Yb182": 181.958325, + "Lu182": 181.95504, + "Hf182": 181.950563816, + "Ta182": 181.950155413, + "W182": 181.948205721, + "Re182": 181.951211645, + "Os182": 181.952110153, + "Ir182": 181.958076296, + "Pt182": 181.961171571, + "Au182": 181.969617874, + "Hg182": 181.974689132, + "Tl182": 181.98569188, + "Pb182": 181.99267294, + "Yb183": 182.962319, + "Lu183": 182.957363, + "Hf183": 182.953534004, + "Ta183": 182.95137618, + "W183": 182.9502245, + "Re183": 182.95082139, + "Os183": 182.953124719, + "Ir183": 182.956839968, + "Pt183": 182.961596653, + "Au183": 182.967588108, + "Hg183": 182.974444629, + "Tl183": 182.982192846, + "Pb183": 182.991867668, + "Yb184": 183.965067, + "Lu184": 183.96091, + "Hf184": 183.955448587, + "Ta184": 183.954010038, + "W184": 183.95093326, + "Re184": 183.952528267, + "Os184": 183.952492949, + "Ir184": 183.957476, + "Pt184": 183.959920039, + "Au184": 183.967451524, + "Hg184": 183.971713221, + "Tl184": 183.981875093, + "Pb184": 183.988135702, + "Bi184": 184.00114125, + "Yb185": 184.969404, + "Lu185": 184.96362, + "Hf185": 184.958862, + "Ta185": 184.955561396, + "W185": 184.953421286, + "Re185": 184.952958337, + "Os185": 184.954045995, + "Ir185": 184.956698, + "Pt185": 184.960613659, + "Au185": 184.965798874, + "Hg185": 184.971890676, + "Tl185": 184.978789191, + "Pb185": 184.987609989, + "Bi185": 184.9976, + "Lu186": 185.967568, + "Hf186": 185.960897, + "Ta186": 185.958553111, + "W186": 185.954365215, + "Re186": 185.954989419, + "Os186": 185.95383766, + "Ir186": 185.957946754, + "Pt186": 185.959350846, + "Au186": 185.965952703, + "Hg186": 185.969362017, + "Tl186": 185.978650841, + "Pb186": 185.984238196, + "Bi186": 185.996622402, + "Po186": 186.004402577, + "Lu187": 186.970392, + "Hf187": 186.96477, + "Ta187": 186.960391, + "W187": 186.957161323, + "Re187": 186.955752288, + "Os187": 186.95574964, + "Ir187": 186.957542, + "Pt187": 186.960616976, + "Au187": 186.964543155, + "Hg187": 186.969814158, + "Tl187": 186.975904743, + "Pb187": 186.983910836, + "Bi187": 186.993147276, + "Po187": 187.003036624, + "Lu188": 187.97446, + "Hf188": 187.96685, + "Ta188": 187.963916, + "W188": 187.958488395, + "Re188": 187.958113728, + "Os188": 187.955837361, + "Ir188": 187.958835046, + "Pt188": 187.95939756, + "Au188": 187.965247969, + "Hg188": 187.96757691, + "Tl188": 187.976020886, + "Pb188": 187.980874592, + "Bi188": 187.992276184, + "Po188": 187.999415655, + "Hf189": 188.97084, + "Ta189": 188.96583, + "W189": 188.961763, + "Re189": 188.959227817, + "Os189": 188.958146005, + "Ir189": 188.958722669, + "Pt189": 188.960848542, + "Au189": 188.963948286, + "Hg189": 188.968194748, + "Tl189": 188.973573527, + "Pb189": 188.980843639, + "Bi189": 188.989195141, + "Po189": 188.998473415, + "Hf190": 189.973129, + "Ta190": 189.96939, + "W190": 189.963089066, + "Re190": 189.96174336, + "Os190": 189.958445496, + "Ir190": 189.960543445, + "Pt190": 189.959949876, + "Au190": 189.96475175, + "Hg190": 189.966322169, + "Tl190": 189.973835551, + "Pb190": 189.978081828, + "Bi190": 189.988620883, + "Po190": 189.995100519, + "Ta191": 190.97156, + "W191": 190.966531, + "Re191": 190.963123437, + "Os191": 190.960928159, + "Ir191": 190.960591527, + "Pt191": 190.961676363, + "Au191": 190.963716455, + "Hg191": 190.967158247, + "Tl191": 190.971784096, + "Pb191": 190.978281, + "Bi191": 190.985786975, + "Po191": 190.994558488, + "At191": 191.004148086, + "Ta192": 191.97524, + "W192": 191.96817, + "Re192": 191.966088, + "Os192": 191.961478881, + "Ir192": 191.962602485, + "Pt192": 191.961042736, + "Au192": 191.964817684, + "Hg192": 191.965634182, + "Tl192": 191.972225, + "Pb192": 191.975785115, + "Bi192": 191.985470078, + "Po192": 191.991335788, + "At192": 192.003141034, + "Ta193": 192.977595, + "W193": 192.97178, + "Re193": 192.967545, + "Os193": 192.964149753, + "Ir193": 192.962923824, + "Pt193": 192.962984616, + "Au193": 192.964138447, + "Hg193": 192.966653377, + "Tl193": 192.970501997, + "Pb193": 192.976173234, + "Bi193": 192.982947223, + "Po193": 192.991062403, + "At193": 192.999927728, + "Rn193": 193.009707964, + "Ta194": 193.981428, + "W194": 193.97367, + "Re194": 193.97076, + "Os194": 193.965179477, + "Ir194": 193.965075773, + "Pt194": 193.962683527, + "Au194": 193.965419062, + "Hg194": 193.965449111, + "Tl194": 193.971081411, + "Pb194": 193.974011706, + "Bi194": 193.982792362, + "Po194": 193.988186015, + "At194": 193.999226872, + "Rn194": 194.006144424, + "W195": 194.977445, + "Re195": 194.97254, + "Os195": 194.968318, + "Ir195": 194.965976967, + "Pt195": 194.964794353, + "Au195": 194.965037851, + "Hg195": 194.966705751, + "Tl195": 194.969774096, + "Pb195": 194.974548743, + "Bi195": 194.980648762, + "Po195": 194.988130617, + "At195": 194.996274485, + "Rn195": 195.005421699, + "W196": 195.979731, + "Re196": 195.9758, + "Os196": 195.969643277, + "Ir196": 195.968399696, + "Pt196": 195.964954675, + "Au196": 195.966571221, + "Hg196": 195.965833444, + "Tl196": 195.970481192, + "Pb196": 195.972787466, + "Bi196": 195.980666509, + "Po196": 195.985536094, + "At196": 195.995797421, + "Rn196": 196.002115945, + "W197": 196.983747, + "Re197": 196.97799, + "Os197": 196.97283, + "Ir197": 196.969657233, + "Pt197": 196.967343053, + "Au197": 196.966570114, + "Hg197": 196.967213713, + "Tl197": 196.969573986, + "Pb197": 196.973434717, + "Bi197": 196.978864929, + "Po197": 196.985659607, + "At197": 196.993177357, + "Rn197": 197.00162143, + "Fr197": 197.01100809, + "Re198": 197.9816, + "Os198": 197.97441, + "Ir198": 197.97228, + "Pt198": 197.967896734, + "Au198": 197.968243724, + "Hg198": 197.966769179, + "Tl198": 197.970446673, + "Pb198": 197.972015397, + "Bi198": 197.979206, + "Po198": 197.983388672, + "At198": 197.992791673, + "Rn198": 197.998679156, + "Fr198": 198.010278138, + "Re199": 198.984047, + "Os199": 198.97801, + "Ir199": 198.973807115, + "Pt199": 198.970597038, + "Au199": 198.968766582, + "Hg199": 198.968280989, + "Tl199": 198.969877, + "Pb199": 198.972912542, + "Bi199": 198.977672893, + "Po199": 198.983673021, + "At199": 198.990527719, + "Rn199": 198.998390273, + "Fr199": 199.007269389, + "Os200": 199.97984, + "Ir200": 199.9768, + "Pt200": 199.971444625, + "Au200": 199.970756556, + "Hg200": 199.968326934, + "Tl200": 199.970963602, + "Pb200": 199.971818332, + "Bi200": 199.978131093, + "Po200": 199.98181227, + "At200": 199.9903511, + "Rn200": 199.995700707, + "Fr200": 200.006583507, + "Os201": 200.98364, + "Ir201": 200.97864, + "Pt201": 200.974513293, + "Au201": 200.971657665, + "Hg201": 200.970303038, + "Tl201": 200.970820168, + "Pb201": 200.972870425, + "Bi201": 200.977008512, + "Po201": 200.98226377700001, + "At201": 200.988417061, + "Rn201": 200.995628179, + "Fr201": 201.003852496, + "Ra201": 201.012814683, + "Os202": 201.98595, + "Ir202": 201.98199, + "Pt202": 201.975639, + "Au202": 201.973856, + "Hg202": 201.970643585, + "Tl202": 201.972109089, + "Pb202": 201.972151604, + "Bi202": 201.9777331, + "Po202": 201.980738881, + "At202": 201.98863038, + "Rn202": 201.993263902, + "Fr202": 202.003323946, + "Ra202": 202.009742264, + "Os203": 202.991798, + "Ir203": 202.98423, + "Pt203": 202.97893, + "Au203": 202.975154498, + "Hg203": 202.972872326, + "Tl203": 202.972344022, + "Pb203": 202.973390535, + "Bi203": 202.976892145, + "Po203": 202.981415995, + "At203": 202.986942957, + "Rn203": 202.993393732, + "Fr203": 203.000940872, + "Ra203": 203.009298745, + "Ir204": 203.9896, + "Pt204": 203.98076, + "Au204": 203.977831, + "Hg204": 203.973494037, + "Tl204": 203.973863337, + "Pb204": 203.97304342, + "Bi204": 203.977835717, + "Po204": 203.980309863, + "At204": 203.987251197, + "Rn204": 203.991443644, + "Fr204": 204.000651974, + "Ra204": 204.006502228, + "Ir205": 204.993602, + "Pt205": 204.98608, + "Au205": 204.97985, + "Hg205": 204.976073125, + "Tl205": 204.974427237, + "Pb205": 204.974481597, + "Bi205": 204.977386323, + "Po205": 204.981190004, + "At205": 204.986074041, + "Rn205": 204.991723204, + "Fr205": 204.998593858, + "Ra205": 205.006268415, + "Ac205": 205.015144158, + "Pt206": 205.98966, + "Au206": 205.98474, + "Hg206": 205.977513756, + "Tl206": 205.976110026, + "Pb206": 205.974465124, + "Bi206": 205.978498757, + "Po206": 205.980473654, + "At206": 205.986656148, + "Rn206": 205.990195358, + "Fr206": 205.998666211, + "Ra206": 206.003827763, + "Ac206": 206.014470787, + "Pt207": 206.995126, + "Au207": 206.988395, + "Hg207": 206.9823, + "Tl207": 206.977418586, + "Pb207": 206.975896735, + "Bi207": 206.978470471, + "Po207": 206.981593252, + "At207": 206.985799783, + "Rn207": 206.9907302, + "Fr207": 206.996946474, + "Ra207": 207.003805161, + "Ac207": 207.011965973, + "Pt208": 207.998937, + "Au208": 207.99345, + "Hg208": 207.985759, + "Tl208": 207.982017992, + "Pb208": 207.976651918, + "Bi208": 207.979741981, + "Po208": 207.981245616, + "At208": 207.986613042, + "Rn208": 207.989634295, + "Fr208": 207.997138018, + "Ra208": 208.001854929, + "Ac208": 208.011544073, + "Th208": 208.017910722, + "Au209": 208.997273, + "Hg209": 208.99072, + "Tl209": 208.98535175, + "Pb209": 208.981089898, + "Bi209": 208.980398519, + "Po209": 208.982430276, + "At209": 208.986169944, + "Rn209": 208.990401388, + "Fr209": 208.995953197, + "Ra209": 209.001994879, + "Ac209": 209.00949422, + "Th209": 209.017571, + "Au210": 210.0025, + "Hg210": 209.99424, + "Tl210": 209.99007297, + "Pb210": 209.984188301, + "Bi210": 209.984120156, + "Po210": 209.982873601, + "At210": 209.987147338, + "Rn210": 209.989688854, + "Fr210": 209.996421657, + "Ra210": 210.000475356, + "Ac210": 210.00943613, + "Th210": 210.015093437, + "Hg211": 210.99933, + "Tl211": 210.993475, + "Pb211": 210.988735356, + "Bi211": 210.987268698, + "Po211": 210.986653085, + "At211": 210.987496147, + "Rn211": 210.990600686, + "Fr211": 210.995555259, + "Ra211": 211.000893213, + "Ac211": 211.007731894, + "Th211": 211.014933183, + "Pa211": 211.023704, + "Hg212": 212.00296, + "Tl212": 211.998335, + "Pb212": 211.991895975, + "Bi212": 211.991285016, + "Po212": 211.988867896, + "At212": 211.990737223, + "Rn212": 211.990703528, + "Fr212": 211.996225453, + "Ra212": 211.999786399, + "Ac212": 212.007812501, + "Th212": 212.013001487, + "Pa212": 212.023181425, + "Hg213": 213.00823, + "Tl213": 213.001915, + "Pb213": 212.996560867, + "Bi213": 212.994383608, + "Po213": 212.992857083, + "At213": 212.992936514, + "Rn213": 212.993885064, + "Fr213": 212.996185861, + "Ra213": 213.00037097, + "Ac213": 213.006607333, + "Th213": 213.013011447, + "Pa213": 213.021108697, + "Hg214": 214.012, + "Tl214": 214.00694, + "Pb214": 213.999803788, + "Bi214": 213.998710938, + "Po214": 213.995201208, + "At214": 213.996371601, + "Rn214": 213.995362566, + "Fr214": 213.998970785, + "Ra214": 214.000099554, + "Ac214": 214.006917762, + "Th214": 214.011481431, + "Pa214": 214.020918561, + "Hg215": 215.0174, + "Tl215": 215.01064, + "Pb215": 215.00466159, + "Bi215": 215.001749149, + "Po215": 214.999418454, + "At215": 214.99865189, + "Rn215": 214.998745498, + "Fr215": 215.000341456, + "Ra215": 215.00272008, + "Ac215": 215.006474132, + "Th215": 215.011724805, + "Pa215": 215.019177728, + "U215": 215.026756035, + "Hg216": 216.02132, + "Tl216": 216.0158, + "Pb216": 216.00803, + "Bi216": 216.006305989, + "Po216": 216.001913506, + "At216": 216.002422631, + "Rn216": 216.000271464, + "Fr216": 216.003189445, + "Ra216": 216.003533117, + "Ac216": 216.008743367, + "Th216": 216.011055714, + "Pa216": 216.019108242, + "U216": 216.024762747, + "Tl217": 217.01966, + "Pb217": 217.01314, + "Bi217": 217.009372, + "Po217": 217.006316216, + "At217": 217.004717835, + "Rn217": 217.003927562, + "Fr217": 217.004631902, + "Ra217": 217.006322806, + "Ac217": 217.009343777, + "Th217": 217.013103444, + "Pa217": 217.018323692, + "U217": 217.024663, + "Tl218": 218.024885, + "Pb218": 218.01659, + "Bi218": 218.014188, + "Po218": 218.008971502, + "At218": 218.008693735, + "Rn218": 218.005601052, + "Fr218": 218.007578274, + "Ra218": 218.007140325, + "Ac218": 218.011641093, + "Th218": 218.013276242, + "Pa218": 218.020057853, + "U218": 218.023504829, + "Pb219": 219.02177, + "Bi219": 219.01748, + "Po219": 219.013614, + "At219": 219.011160647, + "Rn219": 219.009478753, + "Fr219": 219.009251553, + "Ra219": 219.010085176, + "Ac219": 219.012420348, + "Th219": 219.015535677, + "Pa219": 219.01990365, + "U219": 219.024999161, + "Np219": 219.031623021, + "Pb220": 220.02541, + "Bi220": 220.02235, + "Po220": 220.016386, + "At220": 220.015433, + "Rn220": 220.011392534, + "Fr220": 220.012326778, + "Ra220": 220.011025562, + "Ac220": 220.01475445, + "Th220": 220.015747926, + "Pa220": 220.021705, + "U220": 220.02462, + "Np220": 220.03254, + "Bi221": 221.02587, + "Po221": 221.021228, + "At221": 221.018017, + "Rn221": 221.015535709, + "Fr221": 221.014253757, + "Ra221": 221.013917224, + "Ac221": 221.015591199, + "Th221": 221.018186236, + "Pa221": 221.021874846, + "U221": 221.026323299, + "Np221": 221.032045, + "Bi222": 222.030842, + "Po222": 222.02414, + "At222": 222.022494, + "Rn222": 222.017576286, + "Fr222": 222.01758262, + "Ra222": 222.015373355, + "Ac222": 222.017843887, + "Th222": 222.0184683, + "Pa222": 222.023784, + "U222": 222.026057953, + "Np222": 222.0333, + "Bi223": 223.0345, + "Po223": 223.02907, + "At223": 223.025151, + "Rn223": 223.021889285, + "Fr223": 223.019734313, + "Ra223": 223.018500719, + "Ac223": 223.019136872, + "Th223": 223.020811546, + "Pa223": 223.023962232, + "U223": 223.027737168, + "Np223": 223.03285, + "Bi224": 224.039539, + "Po224": 224.03211, + "At224": 224.029749, + "Rn224": 224.024095804, + "Fr224": 224.0233481, + "Ra224": 224.020210453, + "Ac224": 224.021722239, + "Th224": 224.021464157, + "Pa224": 224.02561721, + "U224": 224.027613974, + "Np224": 224.03422, + "Po225": 225.03707, + "At225": 225.03263, + "Rn225": 225.028485574, + "Fr225": 225.025572478, + "Ra225": 225.023610574, + "Ac225": 225.023228647, + "Th225": 225.023950907, + "Pa225": 225.026130844, + "U225": 225.029393555, + "Np225": 225.033910797, + "Po226": 226.04031, + "At226": 226.03716, + "Rn226": 226.030861382, + "Fr226": 226.029544515, + "Ra226": 226.025408455, + "Ac226": 226.026097069, + "Th226": 226.024903686, + "Pa226": 226.027947872, + "U226": 226.029338749, + "Np226": 226.035188, + "Po227": 227.04539, + "At227": 227.04024, + "Rn227": 227.035304396, + "Fr227": 227.031865417, + "Ra227": 227.029176474, + "Ac227": 227.027750666, + "Th227": 227.027702618, + "Pa227": 227.028804477, + "U227": 227.031181587, + "Np227": 227.034956832, + "Pu227": 227.039474, + "At228": 228.04475, + "Rn228": 228.037835418, + "Fr228": 228.035839437, + "Ra228": 228.031068657, + "Ac228": 228.031019767, + "Th228": 228.028739835, + "Pa228": 228.031050748, + "U228": 228.031371351, + "Np228": 228.036066462, + "Pu228": 228.038741387, + "At229": 229.04812, + "Rn229": 229.042257276, + "Fr229": 229.038291455, + "Ra229": 229.034956707, + "Ac229": 229.032947, + "Th229": 229.031761431, + "Pa229": 229.032095652, + "U229": 229.033505909, + "Np229": 229.036263974, + "Pu229": 229.040145819, + "Am229": 229.045249909, + "Rn230": 230.04514, + "Fr230": 230.042390791, + "Ra230": 230.03705478, + "Ac230": 230.036327, + "Th230": 230.033132358, + "Pa230": 230.034539789, + "U230": 230.033940102, + "Np230": 230.037827716, + "Pu230": 230.039650703, + "Am230": 230.046089, + "Rn231": 231.04987, + "Fr231": 231.045175357, + "Ra231": 231.041027086, + "Ac231": 231.038393, + "Th231": 231.036302853, + "Pa231": 231.035882575, + "U231": 231.036292252, + "Np231": 231.03824449, + "Pu231": 231.04112641, + "Am231": 231.045529, + "Cm231": 231.050746, + "Fr232": 232.049461224, + "Ra232": 232.04347527, + "Ac232": 232.042034, + "Th232": 232.038053689, + "Pa232": 232.0385903, + "U232": 232.03715486, + "Np232": 232.040107, + "Pu232": 232.041184526, + "Am232": 232.046527, + "Cm232": 232.049718, + "Fr233": 233.052517838, + "Ra233": 233.047594573, + "Ac233": 233.044346, + "Th233": 233.041580208, + "Pa233": 233.040246605, + "U233": 233.039634367, + "Np233": 233.040739489, + "Pu233": 233.042997345, + "Am233": 233.046445, + "Cm233": 233.050772206, + "Bk233": 233.056748, + "Ra234": 234.050382104, + "Ac234": 234.048139, + "Th234": 234.04359986, + "Pa234": 234.043305615, + "U234": 234.04095037, + "Np234": 234.04289332, + "Pu234": 234.043317478, + "Am234": 234.047731, + "Cm234": 234.050160959, + "Bk234": 234.057387, + "Ra235": 235.05489, + "Ac235": 235.05084, + "Th235": 235.047255, + "Pa235": 235.045399, + "U235": 235.04392819, + "Np235": 235.044061591, + "Pu235": 235.045284682, + "Am235": 235.047907371, + "Cm235": 235.051567, + "Bk235": 235.05658, + "Ac236": 236.054988, + "Th236": 236.049657, + "Pa236": 236.048668, + "U236": 236.045566201, + "Np236": 236.046568392, + "Pu236": 236.046056756, + "Am236": 236.049427, + "Cm236": 236.051374506, + "Bk236": 236.05748, + "Ac237": 237.057993, + "Th237": 237.053629, + "Pa237": 237.051023, + "U237": 237.04872838, + "Np237": 237.04817171, + "Pu237": 237.048407957, + "Am237": 237.049995, + "Cm237": 237.052868923, + "Bk237": 237.0571, + "Cf237": 237.062199993, + "Th238": 238.056388, + "Pa238": 238.054637, + "U238": 238.050786996, + "Np238": 238.050944671, + "Pu238": 238.04955825, + "Am238": 238.051982607, + "Cm238": 238.053081595, + "Bk238": 238.058203, + "Cf238": 238.06149, + "Th239": 239.060602, + "Pa239": 239.05726, + "U239": 239.054292048, + "Np239": 239.052937599, + "Pu239": 239.052161669, + "Am239": 239.053022803, + "Cm239": 239.054908593, + "Bk239": 239.05824, + "Cf239": 239.062554, + "Es239": 239.06823, + "Pa240": 240.061095, + "U240": 240.056592425, + "Np240": 240.05616383, + "Pu240": 240.053811812, + "Am240": 240.055298444, + "Cm240": 240.055528329, + "Bk240": 240.059758, + "Cf240": 240.062255842, + "Es240": 240.06892, + "Pa241": 241.064026, + "U241": 241.06033, + "Np241": 241.058250697, + "Pu241": 241.056849722, + "Am241": 241.056827413, + "Cm241": 241.057651288, + "Bk241": 241.060153, + "Cf241": 241.06369, + "Es241": 241.06856, + "Fm241": 241.07421, + "U242": 242.062931, + "Np242": 242.061639615, + "Pu242": 242.058741045, + "Am242": 242.059547428, + "Cm242": 242.058834263, + "Bk242": 242.06198, + "Cf242": 242.063754533, + "Es242": 242.069567, + "Fm242": 242.07343, + "U243": 243.066946, + "Np243": 243.064279, + "Pu243": 243.062002119, + "Am243": 243.06137994, + "Cm243": 243.061387403, + "Bk243": 243.06300598, + "Cf243": 243.065475, + "Es243": 243.069509, + "Fm243": 243.07449, + "Np244": 244.06785, + "Pu244": 244.064204415, + "Am244": 244.064282964, + "Cm244": 244.062750694, + "Bk244": 244.065179039, + "Cf244": 244.065999543, + "Es244": 244.070881, + "Fm244": 244.074038, + "Np245": 245.070736, + "Pu245": 245.067824568, + "Am245": 245.06645289, + "Cm245": 245.065491113, + "Bk245": 245.066359885, + "Cf245": 245.068046825, + "Es245": 245.071247, + "Fm245": 245.075349, + "Md245": 245.080808, + "Pu246": 246.070204209, + "Am246": 246.069774, + "Cm246": 246.067222082, + "Bk246": 246.068671367, + "Cf246": 246.068803762, + "Es246": 246.072894, + "Fm246": 246.075350815, + "Md246": 246.081713, + "Pu247": 247.07419, + "Am247": 247.072092, + "Cm247": 247.070352726, + "Bk247": 247.07030594, + "Cf247": 247.070965462, + "Es247": 247.073621932, + "Fm247": 247.076944, + "Md247": 247.081521, + "Am248": 248.075752, + "Cm248": 248.072349101, + "Bk248": 248.073087, + "Cf248": 248.072182978, + "Es248": 248.075469, + "Fm248": 248.077185528, + "Md248": 248.082822, + "No248": 248.08655, + "Am249": 249.07848, + "Cm249": 249.075954006, + "Bk249": 249.074983182, + "Cf249": 249.074850491, + "Es249": 249.076409, + "Fm249": 249.078926098, + "Md249": 249.082912, + "No249": 249.087797, + "Cm250": 250.078357556, + "Bk250": 250.078315027, + "Cf250": 250.076404561, + "Es250": 250.078611, + "Fm250": 250.079519828, + "Md250": 250.084413, + "No250": 250.087562, + "Cm251": 251.082285036, + "Bk251": 251.080760603, + "Cf251": 251.079587219, + "Es251": 251.079992224, + "Fm251": 251.081539889, + "Md251": 251.084774291, + "No251": 251.088942, + "Lr251": 251.09418, + "Cm252": 252.08487, + "Bk252": 252.08431, + "Cf252": 252.081626523, + "Es252": 252.082979189, + "Fm252": 252.082464972, + "Md252": 252.086432, + "No252": 252.088966141, + "Lr252": 252.095263, + "Bk253": 253.08688, + "Cf253": 253.085133738, + "Es253": 253.084821305, + "Fm253": 253.08518116, + "Md253": 253.087143, + "No253": 253.090562831, + "Lr253": 253.095089, + "Rf253": 253.100438, + "Bk254": 254.0906, + "Cf254": 254.08732359, + "Es254": 254.088020527, + "Fm254": 254.086852726, + "Md254": 254.08959, + "No254": 254.090954259, + "Lr254": 254.096481, + "Rf254": 254.100053, + "Cf255": 255.091047, + "Es255": 255.090273553, + "Fm255": 255.089962633, + "Md255": 255.091082787, + "No255": 255.093191404, + "Lr255": 255.096562404, + "Rf255": 255.101267, + "Db255": 255.106918, + "Cf256": 256.093442, + "Es256": 256.093599, + "Fm256": 256.091773878, + "Md256": 256.093888, + "No256": 256.094280866, + "Lr256": 256.098494029, + "Rf256": 256.101151535, + "Db256": 256.107889, + "Es257": 257.095979, + "Fm257": 257.095105317, + "Md257": 257.095537977, + "No257": 257.096884419, + "Lr257": 257.09948, + "Rf257": 257.102916848, + "Db257": 257.107576, + "Es258": 258.09952, + "Fm258": 258.097077, + "Md258": 258.098429825, + "No258": 258.098205, + "Lr258": 258.101753, + "Rf258": 258.103426362, + "Db258": 258.109284, + "Sg258": 258.112984, + "Fm259": 259.100596, + "Md259": 259.10051, + "No259": 259.100997503, + "Lr259": 259.102901, + "Rf259": 259.105596, + "Db259": 259.109491865, + "Sg259": 259.114353, + "Fm260": 260.102809, + "Md260": 260.103653, + "No260": 260.102643, + "Lr260": 260.105504, + "Rf260": 260.106439, + "Db260": 260.111297, + "Sg260": 260.114383508, + "Bh260": 260.121658, + "Md261": 261.105828, + "No261": 261.105696, + "Lr261": 261.10688, + "Rf261": 261.10876999, + "Db261": 261.11198, + "Sg261": 261.115948188, + "Bh261": 261.121454, + "Md262": 262.109101, + "No262": 262.107463, + "Lr262": 262.109611, + "Rf262": 262.109923, + "Db262": 262.114068, + "Sg262": 262.116335446, + "Bh262": 262.122965, + "No263": 263.110714, + "Lr263": 263.111358, + "Rf263": 263.11246, + "Db263": 263.114988, + "Sg263": 263.118294, + "Bh263": 263.122916, + "Hs263": 263.12848, + "No264": 264.112734, + "Lr264": 264.1142, + "Rf264": 264.113878, + "Db264": 264.117405, + "Sg264": 264.118929, + "Bh264": 264.124593, + "Hs264": 264.128356405, + "Lr265": 265.116193, + "Rf265": 265.116683, + "Db265": 265.118608, + "Sg265": 265.12109, + "Bh265": 265.124977, + "Hs265": 265.129791799, + "Mt265": 265.135995, + "Lr266": 266.119831, + "Rf266": 266.118172, + "Db266": 266.121028, + "Sg266": 266.121973, + "Bh266": 266.12679, + "Hs266": 266.130045252, + "Mt266": 266.137373, + "Rf267": 267.121787, + "Db267": 267.122464, + "Sg267": 267.124322, + "Bh267": 267.1275, + "Hs267": 267.131673, + "Mt267": 267.137189, + "Ds267": 267.143726, + "Rf268": 268.123968, + "Db268": 268.125671, + "Sg268": 268.125392, + "Bh268": 268.129691, + "Hs268": 268.131863, + "Mt268": 268.138649, + "Ds268": 268.143477, + "Db269": 269.127911, + "Sg269": 269.12857, + "Bh269": 269.130412, + "Hs269": 269.133725, + "Mt269": 269.138884, + "Ds269": 269.144751021, + "Db270": 270.131302, + "Sg270": 270.130426, + "Bh270": 270.133362, + "Hs270": 270.134314, + "Mt270": 270.140323, + "Ds270": 270.14458309, + "Sg271": 271.133932, + "Bh271": 271.135182, + "Hs271": 271.137135, + "Mt271": 271.140742, + "Ds271": 271.145946, + "Sg272": 272.13589, + "Bh272": 272.138261, + "Hs272": 272.138494, + "Mt272": 272.143406, + "Ds272": 272.146018, + "Rg272": 272.153273, + "Sg273": 273.13958, + "Bh273": 273.14024, + "Hs273": 273.14159, + "Mt273": 273.14462, + "Ds273": 273.148531, + "Rg273": 273.153189, + "Bh274": 274.143513, + "Hs274": 274.143303, + "Mt274": 274.147339, + "Ds274": 274.149434, + "Rg274": 274.155249, + "Bh275": 275.14567, + "Hs275": 275.146667, + "Mt275": 275.149039, + "Ds275": 275.151976, + "Rg275": 275.155981, + "Hs276": 276.148455, + "Mt276": 276.151708, + "Ds276": 276.153024, + "Rg276": 276.158333, + "Cn276": 276.16141, + "Hs277": 277.151899, + "Mt277": 277.153483, + "Ds277": 277.155815, + "Rg277": 277.159247, + "Cn277": 277.163611, + "Mt278": 278.156454, + "Ds278": 278.157146, + "Rg278": 278.161587, + "Cn278": 278.164179, + "Ed278": 278.170574, + "Mt279": 279.158343, + "Ds279": 279.160093, + "Rg279": 279.162937, + "Cn279": 279.166432, + "Ed279": 279.17095, + "Ds280": 280.16159, + "Rg280": 280.165203, + "Cn280": 280.167147, + "Ed280": 280.172991, + "Ds281": 281.164715, + "Rg281": 281.166718, + "Cn281": 281.169641, + "Ed281": 281.17371, + "Rg282": 282.169405, + "Cn282": 282.170668, + "Ed282": 282.175766, + "Rg283": 283.170995, + "Cn283": 283.173362, + "Ed283": 283.17682, + "Cn284": 284.174499, + "Ed284": 284.178843, + "Fl284": 284.181344, + "Cn285": 285.177321, + "Ed285": 285.180066, + "Fl285": 285.183579, + "Ed286": 286.182518, + "Fl286": 286.184406, + "Ed287": 287.18384, + "Fl287": 287.186875, + "Ef287": 287.190978, + "Fl288": 288.187916, + "Ef288": 288.192992, + "Fl289": 289.190623, + "Ef289": 289.193953, + "Lv289": 289.198099, + "Ef290": 290.196345, + "Lv290": 290.198818, + "Ef291": 291.197522, + "Lv291": 291.201169, + "Eh291": 291.205906, + "Lv292": 292.202086, + "Eh292": 292.207812, + "Lv293": 293.204691, + "Eh293": 293.20868, + "Ei293": 293.213498, + "Eh294": 294.210974, + "Ei294": 294.214132, + "Ei295": 295.216332, + "C0": 12.011115164864455, + "Zn0": 65.377782324378, + "Pt0": 195.08442936208695, + "Os0": 190.2248615316531, + "Tl0": 204.3833321886034 + } +} diff --git a/process/models/neutronics/base.py b/process/models/neutronics/base.py new file mode 100644 index 000000000..f8e9e79a9 --- /dev/null +++ b/process/models/neutronics/base.py @@ -0,0 +1,1599 @@ +""" +Most of derivation is basd on the book Reactor Analysis, Duderstadt and Hamilton, 1976, +ISBN:9780471223634. + +The rest of the derivation are done in a paper, the conversion is as follows: +| quantity |paper|this program | +--------------------------------------------- +|indexing from | 1 | 0 | +|group index (1) | i | n | +|group index (2) | j | g | +|group index (3) | | k | +|group index (4) | g | i | +|layer index (1) | m | num_layer | +|layer index (2) |l=m+1| num_layer+1 | +|layer index (3) | k | k | +|total number of groups(1)| N |self.n_groups| +|total number of layers(2)| M |self.n_layers| +The reason for this deviation in variable names is because of code style, +e.g. n and g are more descriptive (*n*umber and *g*roup) indices, and hence +is the most frequently used letter for group index; while capital letters are +frowned upon as standalone python variables, so aren't used in the program. +This retains maintainability without having to refer to the paper. +(Meanwhile, it is customary to use i,j,k,l,m as mathematical indices in +academic publications) +""" + +import functools +import inspect +import warnings +from collections.abc import Callable, Iterable +from dataclasses import asdict, dataclass +from itertools import pairwise + +import numpy as np +from matplotlib import pyplot as plt +from numpy import typing as npt + +from process.core.exceptions import ProcessValidationError, ProcessValueError +from process.models.neutronics.data import DT_NEUTRON_E, N_A, MaterialMacroInfo + + +def summarize_values(func): + """ + Keep groupwise_func unchanged, but create a new method under a similar name + (but with the prefix "groupwise_" removed) which outputs the sum of every + groupwise value. + """ + summary_method_name = func.__name__[10:] + # confirm this is a groupwise method + func_params = inspect.signature(func).parameters + if not (func.__name__.startswith("groupwise_") and "n" in func_params): + raise ValueError( + "The decorated method is designed to turn groupwise methods into " + "flux/integrated flux/current methods." + ) + + @functools.wraps(func) + def wrapper(self, *args, **kwargs): + groupwise_func = getattr(self, func.__name__) + return np.sum( + [groupwise_func(n, *args, **kwargs) for n in range(self.n_groups)], + axis=0, + ) + + def wrapper_setattr(cls): + """ + Attach the method that outputs the summed values of all of the + groupwise values to the same parent class. + """ + setattr(cls, func.__name__, func) + setattr(cls, summary_method_name, wrapper) + return cls + + # Instead of returning a function, we return a descriptor that registers itself later + return RegisterLater(wrapper_setattr) + + +class RegisterLater: + """Descriptor class""" + + def __init__(self, installer): + """Modifies the class AFTER it has been created.""" + self.installer = installer + + def __set_name__(self, owner, name): + """Re-write method name""" + self.installer(owner) + + +def extrapolation_length(diffusion_constficient: float) -> float: + """Get the extrapolation length of the final medium :math:`\\delta`. + + Notes + ----- + Diffusion theory breaks down at the vacuum boundary, where once the neutron exits, + it will travel indefinitely into free space, never to return. To counteract this + problem, we can approximate the neutron profile quite closely by assuming that the + flux goes to 0 at an extended boundary, rather than at the vacuum boundary. + THis yields a very close approximation. All of this equation is provided by + Duderstadt and Hamilton. + """ + return 0.7104 * 3 * diffusion_constficient + + +@dataclass +class Coefficients: + """ + Inside each material, there are two new hyperbolic trig funcs per group, i.e. + group n=0 has cosh(x/L[0]) and sinh(x/L[0]), + group n=1 has cosh(x/L[0]), sinh(x/L[0]), cosh(x/L[1]) and sinh(x/L[1]), + etc. + To get the neutron flux, each trig func has to be scaled by a coefficient. + E.g. Let's say for the first wall, which is num_layer=0, + group n=0: NeutronFluxProfile.coefficients[0, 0] = Coefficients(...) + fw_grp0 = NeutronFluxProfile.coefficients[0, 0] + flux = fw_grp0.c[0] * cosh(x/L[0]) + fw_grp0.s[0] * sinh(x/L[0]) + + group n=1: NeutronFluxProfile.coefficients[0, 1] = Coefficients(...) + fw_grp1 = NeutronFluxProfile.coefficients[0, 1] + flux = fw_grp1.c[0] * cosh(x/L[0]) + fw_grp1.c[1] * cosh(x/L[1]) + + fw_grp1.s[0] * sinh(x/L[0]) + fw_grp1.s[1] * sinh(x/L[1]) + + """ + + c: Iterable[float] + s: Iterable[float] + + def validate_length(self, exp_len: int, parent_name: str): + """Validate that all fields has the correct length.""" + for const_name, const_value in asdict(self).items(): + if len(const_value) != exp_len: + raise ProcessValueError( + f"{parent_name}'s [{exp_len - 1}]-th item is expected to " + f"have .{const_name} of length={exp_len}, but instead got " + f"{const_value}." + ) + self._len = exp_len + + def __len__(self): + """Return number of coefficients pairs that has been populated.""" + return self._len + + +class AutoPopulatingDict: + """ + Class that behaves like a dictionary, but if the required key does not + exist in the dictionary, it will call the populating_method to populate + that specific key. + """ + + def __init__(self, populating_method: Callable[[int], None], name: str): + """ + Attributes + ---------- + _dict: + A dictionary indexed by integers, so that we can populate its + values out of sequence. + populating_method: + The method to be called if the requested index n is currently + unpopulated in the dictionary. This method should populate the + dictionary. + """ + self._dict = {} + self.name = name + self._attempting_to_access = set() + self.populating_method = populating_method + + def __getitem__(self, i: int): + """Check if index i is in the dictionary or not. If not, populate it.""" + if i in self._attempting_to_access: + raise RecursionError( + f"retrieving the value of {self.name}[{i}] requires the " + f"value of {self.name}[{i}]." + ) + if i not in self._dict: + self._attempting_to_access.add(i) + self.populating_method(i) + if i not in self._dict: + raise RuntimeError( + f"{self.populating_method}({i}) failed to populate key {i} " + "in the dictionary!" + ) + self._attempting_to_access.discard(i) + return self._dict[i] + + def __contains__(self, i: int): + """Check if key 'i' is in the dictionary or not.""" + return i in self._dict + + def __len__(self): + """Return the number of items in the dict""" + return len(self._dict) + + def __setitem__(self, i: int, value: float): + """Check if dict i is in the index or not.""" + if hasattr(value, "validate_length"): + value.validate_length(len(self) + 1, parent_name=self.name) + self._dict[i] = value + self._attempting_to_access.discard(i) + + def __delitem__(self, i: int): + """Delete item from underlying dict.""" + del self._dict[i] + + def values(self): + return self._dict.values() + + def items(self): + return self._dict.items() + + def __repr__(self): + return f"" + + +class LayerSpecificGroupwiseConstants: + """An object containing multiple AutoPopulatingDict""" + + def __init__( + self, + populating_method: Callable[[int], None], + layer_names: list[str], + quantity_description: str, + ): + """ + Create an object that contains as many AutoPopulatingDict as there are + items in layer_names. + + Parameters + ---------- + populating_method: + The method to be called if the requested index n in any one of the + AutoPopulatingDict is currently unpopulated. This method should + populate that dictionary. + layer_names: + A list of strings, each of which is the descriptive name for that + layer. While the actual content in each string could be empty, + the length of this list MUST be equal to the total number + of dictionaries required. + quantity_description: + A name to be given to this specific instance of the class, to help + label what quantity is being stored. + """ + self._name = quantity_description + layer_dicts = [] + for num_layer, _layer_name in enumerate(layer_names): + name = f"{self._name} for layer {num_layer}" + if _layer_name: + name += f":{_layer_name}" + layer_dicts.append(AutoPopulatingDict(populating_method, name)) + self._dicts = tuple(layer_dicts) + self.n_layers = len(self._dicts) + + def __iter__(self): + return self._dicts.__iter__() + + def __len__(self) -> int: + return len(self._dicts) + + def __setitem__(self, index: int | tuple[int, int], value): + """ + Act as if this is a 2D array, where the first-axis is the layer and the + second axis is the group. support slice of the thing, + """ + if isinstance(index, tuple) and len(index) >= 2: + if len(index) > 2: + raise IndexError("2D array indexed with more than 2 indices!") + layer_index, group_index = index + self._dicts[layer_index][group_index] = value + else: + super().__setitem__(index, value) + + def __getitem__(self, index: int | tuple[int, int]): + """ + Act as if this is a 2D array, where the first-axis is the layer and the + second axis is the group. Handle slices as well. + """ + if isinstance(index, tuple) and len(index) >= 2: + if len(index) > 2: + raise IndexError("2D array indexed with more than 2 indices!") + layer_index, group_index = index + if isinstance(layer_index, slice): + return tuple(_dict[group_index] for _dict in self._dicts[layer_index]) + return self._dicts[layer_index][group_index] + return self._dicts[index] + + def __delitem__(self, index: int | tuple[int, int]): + if isinstance(index, tuple) and len(index) >= 2: + if len(index) > 2: + raise IndexError("2D array indexed with more than 2 indices!") + layer_index, group_index = index + if isinstance(layer_index, slice): + raise NotImplementedError("Cannot delete slice!") + del self._dicts[layer_index][group_index] + else: + raise ValueError( + f"Deletion of a single dictionary in {self.__class__} is not permitted." + ) + + def has_populated(self, n: int) -> bool: + """ + Check if group n's constants are populated for every layer's dict. + + Parameter + --------- + n: + group number to check the population status of every layer's dict. + + + Returns + ------- + : + Whether group n's constants are poplated across all dictionaries + stored by the current instance. + """ + return all(n in layer_dict for layer_dict in self._dicts) + + def __repr__(self): + return f"" + + +UNIT_LOOKUP = { + "linear_heating_density": "J m^-1", + "integrated_flux": "m^-1 s^-1", + "integrated_heating": "W m^-2", + "integrated_tritium_production": "mole m^-2", + "flux": "m^-2 s^-1", + "current": "m^-2 s^-1", + "heating": "W m^-3", + "tritium_production": "mole m^-3", +} + + +class NeutronFluxProfile: + """ + Calculate the neutron flux, neutron current, and neutron heating in the + mirrored infinite-slab model, where each layer extend infinitely in y- and + z-directions, but has finite width in the x-direction. Each layer's + thickness is defined along the positive x-axis starting at 0, and then + reflected along x=0 to fill out the negative x-axis. + """ + + def __init__( + self, + flux: float, + layer_x: npt.NDArray[np.float64], + materials: Iterable[MaterialMacroInfo], + init_neutron_energy: float = DT_NEUTRON_E, + ): + """Initialize a particular FW-BZ geometry and neutron flux. + + Parameters + ---------- + flux: + Neutron flux directly emitted by the plasma, incident on the first + wall. unit: m^-2 s^-1 + init_neutron_energy: + Neutron's initial energy when it first exit the plasma, before any + downscattering or reactions. unit: J. + layer_x: + The x-coordinates of the right side of every layers. By definition, + the plasma is situated at x=0, so all values in layer_x must be >0. + E.g. layer_x[0] is the thickness of the first wall, + layer_x[1] is the thickness of the first wall + breeding zone, + etc. + materials: + Every layer's material information. + + Attributes + ---------- + interface_x: + The x-coordinates of every plasma-layer interfaces/ layer-layer + interface/ layer-void interface. For n_layers, + there will be the interface between the first layer and the plasma, + plus (n_layers - 1) interfaces between layers, plus the interface + between the final layer and the void into which neutrons are lost. + E.g. interface_x[0] = 0.0 = the plasma-fw interface. + n_layers: + Number of layers + n_groups: + Number of groups in the group structure + group_structure: + Energy bin edges, 1D array of len = n_groups+1 + group_energy: + The average neutron energy of each group. + + coefficients: + Coefficients that determine the flux shape (and therefore reaction + rates, neutron current, etc.) of each group. Each coefficient has + unit: [m^-2 s^-1] + l2: + Square of the characteristic diffusion length of each layer as + given by Reactor Analysis, Duderstadt and Hamilton. unit: [m^2] + diffusion_const: + Diffusion coefficient of each layer. unit: [m] + extended_boundary: + Extended boundary for each group. These values should be larger + than layer_x[-1]. + num_iteration: + How many times the method has been called to solve all of the + neutron groups collectively. + downscatter_only: + A boolean to denote if all materials here only allow for + downscattering. + """ + # flux incident on the first wall at the highest energy. + self.init_neutron_energy = init_neutron_energy + + # layers + self.layer_x = np.array(layer_x).ravel() + if not (np.diff(self.layer_x) > 0).all(): + raise ValueError("Model cannot have non-positive layer thicknesses.") + + self.layer_x.flags.writeable = False + self.interface_x = np.array([0.0, *self.layer_x]) + self.interface_x.flags.writeable = False + + self.materials = tuple(materials) + if len(self.layer_x) != len(self.materials): + raise ProcessValidationError( + "The number of layers specified by self.materials must match " + "the number of x-positions specified by layer_x." + ) + self.n_layers = len(self.materials) + + # groups + fw_mat = self.materials[0] + for mat in self.materials[1:]: + if not np.allclose( + fw_mat.group_structure, + mat.group_structure, + atol=0, + ): + raise ProcessValidationError( + "All material info must have the same group structure!" + ) + self.n_groups = fw_mat.n_groups + self.group_structure = fw_mat.group_structure + self.group_energy, incident_neutron_group = ( + self._calculate_mean_energy_and_incident_bin( + self.group_structure, self.init_neutron_energy + ) + ) + self.fluxes = np.array([ + flux if n == incident_neutron_group else 0.0 for n in range(self.n_groups) + ]) + + mat_name_list = [mat.name for mat in self.materials] + self.coefficients = LayerSpecificGroupwiseConstants( + self.solve_group_n, mat_name_list, "Coefficients" + ) + self.extended_boundary = AutoPopulatingDict( + self.solve_group_n, "extended_boundary" + ) + self.num_iteration = [0 for n in range(self.n_groups)] + self.contains_upscatter = any(not mat.downscatter_only for mat in self.materials) + + @staticmethod + def _calculate_mean_energy_and_incident_bin( + group_structure: npt.NDArray[np.float64], + init_neutron_e: float, + ) -> npt.NDArray[np.float64]: + """ + Calculate the average energy of each neutron group in Joule. + When implementing this method, we can choose either a weighted mean or + an unweighted mean. The weighted mean (where neutron flux is assumed + constant w.r.t. neutron lethargy within the bin) is more accurate, but + may end up being incorrect if the bins in the group structure are too + wide, as it heavily biases towards lower energy. In contrast, the + simple unweighted mean does not have such problem, but is inconsistent + with the rest of the program's assumption (const. flux w.r.t. lethargy), + therefore the former is chosen. + + Parameters + ---------- + group_structure: + The neutron energy bin's edges, in descending order, with len= + n_groups + 1. + init_neutron_e: + The neutrons entering from the plasma to the FW is assumed to be + monoenergetic, with this energy. + + Returns + ------- + avg_neutron_E: + Mean energy of neutrons. The bin containing init_neutron_e + (likely the highest energy bin, i.e. bin[0]) is assumed to be + dominated by the unscattered neutrons entering from the plasma, + therefore it is + incident_neutron_group: + The group index (n) of the neutron group whose energy range + includes the incident (monoenergetic) plasma neutron's energy. + """ + high, low = group_structure[:-1], group_structure[1:] + weighted_mean = (high - low) / (np.log(high) - np.log(low)) + # unweighted_mean = np.mean([high, low], axis=0) + first_bin = np.logical_and(low <= init_neutron_e, init_neutron_e < high) + incident_neutron_group = np.where(first_bin)[0][0] + if first_bin.sum() < 1: + raise ValueError( + "The energy of neutrons incident from the plasma is not " + "captured by the group structure!" + ) + if first_bin.sum() > 1: + raise ValueError( + "The energy of neutrons incident from the plasma is captured " + "multiple times, by more than one bin in the group structure!" + ) + if incident_neutron_group != 0: + raise NotImplementedError( + "If init_neutron_e does not sit inside the lowest lethargy " + "group, then solve_lowest_group would have to be re-written." + ) + weighted_mean[first_bin] = init_neutron_e + return weighted_mean, incident_neutron_group + + def _groupwise_cs_values_in_layer( + self, n: int, num_layer: int, x: float | npt.NDArray + ) -> npt.NDArray: + """ + Calculate the num_layer-th layer n-th basis function at the specified + x position(s). + """ + abs_x = abs(x) + if self.materials[num_layer].l2[n] > 0: + l = np.sqrt(self.materials[num_layer].l2[n]) # noqa: E741 + c, s = np.cosh, np.sinh + else: + l = np.sqrt(-self.materials[num_layer].l2[n]) # noqa: E741 + c, s = np.cos, np.sin + return np.array([c(abs_x / l), s(abs_x / l)]) + + def _groupwise_cs_differential_in_layer( + self, n: int, num_layer: int, x: float | npt.NDArray + ) -> npt.NDArray: + """ + Differentiate the num_layer-th layer n-th basis function, and evaluate + it at position(s) x. + """ + abs_x = abs(x) + if self.materials[num_layer].l2[n] > 0: + l = np.sqrt(self.materials[num_layer].l2[n]) # noqa: E741 + return np.array([np.sinh(abs_x / l) / l, np.cosh(abs_x / l) / l]) + l = np.sqrt(-self.materials[num_layer].l2[n]) # noqa: E741 + return np.array([-np.sin(abs_x / l) / l, np.cos(abs_x / l) / l]) + + def _groupwise_cs_definite_integral_in_layer( + self, n: int, num_layer: int, x_lower: float | npt.NDArray, x_upper + ) -> npt.NDArray: + """ + Integrate the num_layer-th layer n-th basis function + from x_lower to x_upper. + """ + if self.materials[num_layer].l2[n] > 0: + l = np.sqrt(self.materials[num_layer].l2[n]) # noqa: E741 + return np.array([ + l * (np.sinh(x_upper / l) - np.sinh(x_lower / l)), + l * (np.cosh(x_upper / l) - np.cosh(x_lower / l)), + ]) + l = np.sqrt(-self.materials[num_layer].l2[n]) # noqa: E741 + return np.array([ + l * (np.sin(x_upper / l) - np.sin(x_lower / l)), + l * (np.cos(x_lower / l) - np.cos(x_upper / l)), # reverse sign + ]) + + def _groupwise_flux_curvature_in_layer( + self, n: int, num_layer: int, x: float | npt.NDArray + ): + """Second derivative of the group n flux in num_layer at location x.""" + abs_x = abs(x) + trig_funcs = [] + for g, cs_coefs in enumerate( + zip( + self.coefficients[num_layer, n].c, + self.coefficients[num_layer, n].s, + strict=True, + ) + ): + l2 = self.materials[num_layer].l2[g] + l = np.sqrt(abs(l2)) # noqa: E741 + c, s = (np.cosh, np.sinh) if l2 > 0 else (np.cos, np.sin) + trig_funcs.append( + cs_coefs @ np.array([c(abs_x / l) / l2, s(abs_x / l) / l2]) + ) + return np.sum(trig_funcs, axis=0) + + def _summation_shorthand( + self, n: int, num_layer: int, func: Callable, x: float, max_group: int + ) -> float: + """ + A repeating pattern of summation found in the code, so we extract it + to reduce duplication and increase robustness. + + Parameters + ---------- + n: + The group of interest, where neutrons are scattered into. + num_layer: + The material layer index. + g: + The groups from which neutrons are scattered. + func: + The function that takes in g, num_layer, and x and evaluates to + a scalar as the output. + It should have the signature of func(g, num_layer, x) since it + should be one of the _groupwise..._in_layer() function that takes + in group index before the layer. + x: + The x-coordinate at which the function func has to be evaluated at. + (likely denotes an interface or the extended boundary.) + + Returns + ------- + np.sum: + A scalar. + """ + + def coef_pair(g: int) -> npt.NDArray[float]: + """ + A quick function to get the coefficient pair at the specified + neutron group n, material layer num_layer, and in-scattering + neutron group g. For paramters: see parent function. + """ + return np.array([ + self.coefficients[num_layer, n].c[g], + self.coefficients[num_layer, n].s[g], + ]) + + summation_sequence = [ + coef_pair(g) @ func(g, num_layer, x) for g in range(max_group) if g != n + ] + return np.sum(summation_sequence, axis=-1) + + def _propagate_coefs_to_next_layer( + self, n: int, num_layer: int, include_upscatter: bool + ) -> tuple[npt.NDArray, npt.NDArray[float]]: + """ + Infer this layer's main basis functions' coefficients (.c[n] and .s[n]) + using using the previous layer's basis functions. + Can only be used when self.coefficients[num_layer, n].c[n] and + self.coefficients[num_layer, n].s[n] are both 0.0. + + Parameters + ---------- + + Returns + ------- + m: + The matrix m that forms part of the vec2 = (m*vec1 + v) equation. + v: + The vector v that forms part of the vec2 = (m*vec1 + v) equation. + """ + xm = self.layer_x[num_layer] + + a_mmn = np.array([ + self._groupwise_cs_values_in_layer(n, num_layer, xm), + self.materials[num_layer].diffusion_const[n] + * self._groupwise_cs_differential_in_layer(n, num_layer, xm), + ]) + a_lmn = np.array([ + self._groupwise_cs_values_in_layer(n, num_layer + 1, xm), + self.materials[num_layer + 1].diffusion_const[n] + * self._groupwise_cs_differential_in_layer(n, num_layer + 1, xm), + ]) + + in_scatter_max_group = self.n_groups if include_upscatter else n + b_mmn = np.array([ + self._summation_shorthand( + n, + num_layer, + self._groupwise_cs_values_in_layer, + xm, + in_scatter_max_group, + ), + self.materials[num_layer].diffusion_const[n] + * self._summation_shorthand( + n, + num_layer, + self._groupwise_cs_differential_in_layer, + xm, + in_scatter_max_group, + ), + ]) + b_lmn = np.array([ + self._summation_shorthand( + n, + num_layer + 1, + self._groupwise_cs_values_in_layer, + xm, + in_scatter_max_group, + ), + self.materials[num_layer + 1].diffusion_const[n] + * self._summation_shorthand( + n, + num_layer + 1, + self._groupwise_cs_differential_in_layer, + xm, + in_scatter_max_group, + ), + ]) + + inv_a_lmn = np.linalg.inv(a_lmn) + m = inv_a_lmn @ a_mmn + v = inv_a_lmn @ (b_mmn - b_lmn) + return m, v + + def _get_all_propagation_operator( + self, n: int, include_upscatter: bool + ) -> tuple[list[npt.NDArray], list[npt.NDArray[float]]]: + """Get all of the m matrix and v vector, as two lists. + + Parameters + ---------- + n: + The neutron group index whose propagation operators that we want. + include_upscatter: + Whether we include the upscatter part of the propagation operator + or not. Warning: can't be called in iteration=0. + + Returns + ------- + m_list: + A list of m matrices, starting from subscript 1 to subscript + self.n_layer-1, each with shape (2, 2) + v_list: + A list of v vectors, starting from subscript 1 to subscript + self.n_layer-1, each with shape (2,) + """ + m_list, v_list = [], [] + for num_layer in range(self.n_layers - 1): + m, v = self._propagate_coefs_to_next_layer(n, num_layer, include_upscatter) + m_list.append(m) + v_list.append(v) + return m_list, v_list + + def solve(self): + """Alias for solving the highest lethargy group.""" + return self.solve_group_n(self.n_groups - 1) + + def solve_group_n(self, n: int) -> None: + """ + Solve the n-th group of neutron's diffusion equation, where n <= + n_groups-1. Store the solved constants in self.extended_boundary[n], + and self.coefficients[:, n]. + + Parameters + ---------- + n: + The index of the neutron group whose constants are being solved. + The allowed range of values = [0, self.n_groups-1]. Therefore, + n=0 shows the reaction rate for group 1, n=1 for group 2, etc. + """ + if n not in range(self.n_groups): + raise ValueError( + f"n must be a positive integer between 0 and {self.n_groups - 1}!" + ) + if n > 0 and not self.coefficients.has_populated(n - 1): + self.solve_group_n(n - 1) + if self.contains_upscatter and self.num_iteration[n] > 1: + raise NotImplementedError( + "Will implement solve_group_n in a loop later." + ) # Sum over the addition in neutron flux due to the 2nd, 3rd, 4th + # etc. generation of neutrons, which should eventually converge. + if self.coefficients.has_populated(n): + return # skip if it has already been solved. + # Parameter to be changed later, to allow solving non-down-scatter + # only systems by iterating. + for num_layer, mat in enumerate(self.materials): + if mat.diffusion_const[n] > self.layer_x[num_layer]: + warnings.warn( + f"Calculation of flux in group {n} may be inaccurate as " + f"layer {num_layer}: {mat} is thinner than " + r"λ_{tr} " + f"of group {n}.", + stacklevel=2, + ) + self.extended_boundary[n] = self.layer_x[-1] + extrapolation_length( + self.materials[-1].diffusion_const[n] + ) + + include_upscatter = self.contains_upscatter and self.num_iteration[n] != 0 + in_scatter_max_group = self.n_groups if include_upscatter else n + 1 + + try: + for num_layer in range(self.n_layers): + # Setting up aliases for shorter code + coefs_num_layer = Coefficients([], []) + mat = self.materials[num_layer] + src_matrix = mat.sigma_s + mat.sigma_in + diffusion_const_n = self.materials[num_layer].diffusion_const[n] + l2n = mat.l2[n] + for g in range(in_scatter_max_group): + if g == n: + # placeholder zeros, to be properly calculated later. + coefs_num_layer.c.append(0.0) + coefs_num_layer.s.append(0.0) + continue + l2g = mat.l2[g] + l2_diff = l2g - l2n + if np.isclose(l2_diff, 0): + # if the characteristic length of group [g] coincides with + # the characteristic length of group [n], then that particular + # cosh/sinh would be indistinguishable from group [n]'s + # cosh/sinh, causing issues. Currently we simply set the coefficient to 0. + # The correct way multiplier of the sin(h) and cos(h) (x/L_g) function + # should include a factor of x^k where k += 1. + coefs_num_layer.c.append(0.0) + coefs_num_layer.s.append(0.0) + warnings.warn( + f"Group {g} and group {n} has the same neutron " + "diffusion lengths, which may lead to an error " + "in the neutron flux profile.", + stacklevel=2, + ) + continue + scale_factor = (l2n * l2g) / l2_diff / diffusion_const_n + in_scatter_min_group = 0 if include_upscatter else g + + coefs_num_layer.c.append( + np.sum([ + (src_matrix[i, n] * self.coefficients[num_layer, i].c[g]) + for i in range(in_scatter_min_group, in_scatter_max_group) + if i != n + ]) + * scale_factor + ) + coefs_num_layer.s.append( + np.sum([ + (src_matrix[i, n] * self.coefficients[num_layer, i].s[g]) + for i in range(in_scatter_min_group, in_scatter_max_group) + if i != n + ]) + * scale_factor + ) + + self.coefficients[num_layer, n] = coefs_num_layer + + self.coefficients[0, n].s[n] = np.sqrt(abs(self.materials[0].l2[n])) * ( + -(self.fluxes[n] / self.materials[0].diffusion_const[n]) + - np.sum([ + self.coefficients[0, n].s[g] / np.sqrt(abs(self.materials[0].l2[g])) + for g in range(in_scatter_max_group) + if g != n + ]) + ) + + m_list, v_list = self._get_all_propagation_operator(n, include_upscatter) + affine_transform_matrix_stack = multiply_2_2_matrices(*m_list[::-1]) + affine_transformed_column_vector = np.sum( + [ + multiply_2_2_matrices(*m_list[:k:-1]) @ v_list[k] + for k in range(self.n_layers - 1) + ] + or [[0, 0]], + axis=0, + ) + boundary_cs_vector = self._groupwise_cs_values_in_layer( + n, self.n_layers - 1, self.extended_boundary[n] + ) + final_left_vector = boundary_cs_vector @ affine_transform_matrix_stack + final_const = ( + -self._summation_shorthand( + n, + self.n_layers - 1, + self._groupwise_cs_values_in_layer, + self.extended_boundary[n], + in_scatter_max_group, + ) + - boundary_cs_vector @ affine_transformed_column_vector + ) + self.coefficients[0, n].c[n] = ( + final_const - final_left_vector[1] * self.coefficients[0, n].s[n] + ) / final_left_vector[0] + # nonnegativity check for layer 0 + if (self.groupwise_neutron_flux_in_layer(n, 0, self.interface_x[0]) < 0) or ( + self.groupwise_neutron_flux_in_layer(n, 0, self.interface_x[1]) < 0 + ): + warnings.warn( + f"Negative flux found when solving for group {n}" + " in layer 0! Likely due to an unphysical " + "cross-section value or a previous warning about" + r"layer thickness < 3 λ_{tr}.", + stacklevel=2, + ) + + for num_layer in range(self.n_layers - 1): + [ + self.coefficients[num_layer + 1, n].c[n], + self.coefficients[num_layer + 1, n].s[n], + ] = ( + m_list[num_layer] + @ np.array([ + self.coefficients[num_layer, n].c[n], + self.coefficients[num_layer, n].s[n], + ]) + + v_list[num_layer] + ) + # non-negativity check for group 1: + if ( + self.groupwise_neutron_flux_in_layer( + n, num_layer, self.layer_x[num_layer] + ) + < 0 + ): + warnings.warn( + "Negative flux found when solving for " + f"group {n} in layer {num_layer}! Likely due to " + "an unphysical cross-section value.", + stacklevel=2, + ) + self.num_iteration[n] += 1 + except Exception as e: + for num_layer in range(self.n_layers): + if n in self.coefficients[num_layer]: + del self.coefficients[num_layer, n] + raise e + return + + def _check_if_in_layer( + self, x: npt.NDArray[np.float64], num_layer: int + ) -> npt.NDArray[bool]: + abs_x = abs(x) + if num_layer == (self.n_layers - 1): + return np.logical_and( + self.interface_x[num_layer] <= abs_x, + abs_x <= self.interface_x[num_layer + 1], + ) + if num_layer == self.n_layers: + return abs_x > self.interface_x[-1] + return np.logical_and( + self.interface_x[num_layer] <= abs_x, + abs_x <= self.interface_x[num_layer + 1], + ) + + @summarize_values + def groupwise_neutron_flux_at( + self, n: int, x: float | npt.NDArray + ) -> float | npt.NDArray: + """ + Neutron flux [m^-2 s^-1] anywhere. Neutron flux is assumed to be + unperturbed once it leaves the final layer. + + Parameters + ---------- + n: + Neutron group index. n <= n_groups - 1. + Therefore n=0 shows the heating for group 1, n=1 for group 2, etc. + x: + The depth where we want the neutron flux [m]. Neutron flux at + infinity is assumed to be the same as the neutron flux at the + nearest layer-void interface. This is achieved by clipping all out- + of-bounds x back to the the nearest interface_x. + """ + if np.isscalar(x): + return self.groupwise_neutron_flux_at(n, [x])[0] + x = np.asarray(x) + + out_flux = np.zeros_like(x, dtype=float) + for num_layer in range(self.n_layers + 1): + in_layer = self._check_if_in_layer(x, num_layer) + if in_layer.any(): + out_flux[in_layer] = self.groupwise_neutron_flux_in_layer( + n, num_layer, x[in_layer] + ) + return out_flux + + @summarize_values + def groupwise_neutron_current_at( + self, n: int, x: float | npt.NDArray + ) -> float | npt.NDArray: + """ + Neutron current [m^-2 s^-1]. Neutron current is assumed to be + unperturbed once it leaves the final layer. + + Parameters + ---------- + n: + Neutron group index. n <= n_groups - 1. + Therefore n=0 shows the neutron current for group 1, n=1 for group 2, etc. + x: + The depth where we want the neutron current [m]. Neutron current at + infinity is assumed to be the same as the neutron flux at the + nearest layer-void interface; this is achieved by clipping all out- + of-bounds x back to the the nearest interface_x. + """ + if np.isscalar(x): + return self.groupwise_neutron_current_at(n, [x])[0] + x = np.asarray(x) + + current = np.zeros_like(x, dtype=float) + for num_layer in range(self.n_layers + 1): + in_layer = self._check_if_in_layer(x, num_layer) + if in_layer.any(): + current[in_layer] = self.groupwise_neutron_current_in_layer( + n, num_layer, x[in_layer] + ) + return current + + @summarize_values + def groupwise_neutron_heating_at( + self, n: int, x: float | npt.NDArray + ) -> float | npt.NDArray: + """ + Neutron heating [W m^-3] of the n-th group at location x [m]. + + Parameters + ---------- + n: + The index of the neutron group whose heating is being evaluated. + n <= n_groups - 1. + Therefore n=0 shows the heating for group 1, n=1 for group 2, etc. + num_layer: + The index of the layer that we want to get the neutron heating for. + x: + The position where the neutron heating has to be evaluated. + + Returns + ------- + heating: + Volumetric neutron heating due to group n's neutrons at x. + unit: [W m^-3] + """ + if np.isscalar(x): + return self.groupwise_neutron_heating_at(n, [x])[0] + + out_heat = np.zeros_like(x, dtype=float) + for num_layer in range(self.n_layers + 1): + in_layer = self._check_if_in_layer(x, num_layer) + if in_layer.any(): + out_heat[in_layer] = self.groupwise_neutron_heating_in_layer( + n, num_layer, x[in_layer] + ) + return out_heat + + @summarize_values + def groupwise_tritium_production_at( + self, n: int, x: float | npt.NDArray + ) -> float | npt.NDArray: + """ + Volumetric tritium production rate [mole m^-3] of the n-th group in the + specified layer, at location x [m]. + + Parameters + ---------- + n: + The index of the neutron group whose tritium production rate is + being evaluated. n <= n_groups - 1. Therefore n=0 shows the tritium + production rate for group 1, n=1 for group 2, etc. + num_layer: + The index of the layer that we want to get the volumetric tritium + production rate for. + x: + The position where the volumetric tritium production rate has to + be evaluated. + + Returns + ------- + tritium_production: + Volumetric tritium production rate due to group n's neutrons at x. + unit: [mole m^-3] + """ + if np.isscalar(x): + return self.groupwise_tritium_production_at(n, [x])[0] + + tritium_out = np.zeros_like(x, dtype=float) + for num_layer in range(self.n_layers + 1): + in_layer = self._check_if_in_layer(x, num_layer) + if in_layer.any(): + tritium_out[in_layer] = self.groupwise_tritium_production_in_layer( + n, num_layer, x[in_layer] + ) + return tritium_out + + @summarize_values + def groupwise_neutron_flux_in_layer( + self, n: int, num_layer: int, x: float | npt.NDArray + ) -> float | npt.NDArray: + """ + Neutron flux[m^-2 s^-1] of the n-th group in the specified layer, + at location x [m]. + + Parameters + ---------- + n: + The index of the neutron group whose flux is being evaluated. + n <= n_groups - 1. + Therefore n=0 shows the heating for group 1, n=1 for group 2, etc. + num_layer: + The index of the layer that we want to get the neutron flux for. + x: + The position where the neutron flux has to be evaluated. + + Returns + ------- + flux: + Neutron flux at x meter from the first wall. + """ + if num_layer == self.n_layers: + return self.groupwise_neutron_flux_in_layer( + n, self.n_layers - 1, np.sign(x) * self.layer_x[-1] + ) + trig_funcs = [] + for g in range(len(self.coefficients[num_layer, n])): + c_val, s_val = self._groupwise_cs_values_in_layer(g, num_layer, x) + trig_funcs.extend([ + self.coefficients[num_layer, n].c[g] * c_val, + self.coefficients[num_layer, n].s[g] * s_val, + ]) + return np.sum(trig_funcs, axis=0) + + @summarize_values + def groupwise_neutron_current_in_layer( + self, n: int, num_layer: int, x: float | npt.NDArray + ) -> float | npt.NDArray: + """ + Get the neutron current (right=positive, left=negative) in any layer. + + Parameters + ---------- + n: + The index of the neutron group that needs to be solved. n <= n_groups - 1. + Therefore n=0 shows the integrated flux for group 1, n=1 for group 2, etc. + num_layer: + The index of the layer that we want to get the neutron current for. + x: + The depth where we want the neutron current [m]. + """ + if num_layer == self.n_layers: + return self.groupwise_neutron_current_in_layer( + n, self.n_layers - 1, np.sign(x) * self.layer_x[-1] + ) + differentials = [] + for g in range(len(self.coefficients[num_layer, n])): + c_diff, s_diff = self._groupwise_cs_differential_in_layer(g, num_layer, x) + differentials.extend([ + self.coefficients[num_layer, n].c[g] * c_diff, + self.coefficients[num_layer, n].s[g] * s_diff, + ]) + + return ( + -self.materials[num_layer].diffusion_const[n] + * np.sum(differentials, axis=0) + * _get_sign_of(x) + ) + + @summarize_values + def groupwise_neutron_heating_in_layer( + self, n: int, num_layer: int, x: float | npt.NDArray + ) -> float | npt.NDArray: + """ + Calculate volumetric heating (unit: [W m^-3]) in the specified group + and layer. + + We do not recommend manually integrating this curve by sampling points + in [self.interface_x[n], self.interface_x[n+1]] to get the total amount + of heating across this entire layer, per unit area. Instead, use + groupwise_integrated_heating_in_layer/ integrated_heating_in_layer, + which is faster and more accurate. + + Parameters + ---------- + n: + Neutron group index. n <= n_groups - 1. + Therefore n=0 shows the heating for group 1, n=1 for group 2, etc. + num_layer: + The index of the layer that we want to get the neutron heating for. + x: + The depth where we want the neutron heating [m]. + + Returns + ------- + : + The neutron heating in that specific layer at position x, due to + group n's neutrons. + """ + return self.groupwise_linear_heating_density_in_layer( + n, num_layer + ) * self.groupwise_neutron_flux_in_layer(n, num_layer, x) + + @summarize_values + def groupwise_tritium_production_in_layer( + self, n: int, num_layer: int, x: float | npt.NDArray + ) -> float | npt.NDArray: + """ + Calculate volumetric tritium production rate (unit: [mole m^-3]) in + the specified group and layer. + + We do not recommend manually integrating this curve by sampling points + in [self.interface_x[n], self.interface_x[n+1]] to get the total amount + of tritium production rate across this entire layer, per unit area. + Instead, use groupwise_integrated_tritium_production_in_layer/ + integrated_tritium_production_in_layer, which is faster and + more accurate. + + Parameters + ---------- + n: + Neutron group index. n <= n_groups - 1. Therefore n=0 shows the + tritium production rate for group 1, n=1 for group 2, etc. + num_layer: + The index of the layer that we want to get the tritium production + rate for. + x: + The depth where we want the tritium production rate [m]. + + Returns + ------- + : + The tritium production rate in that specific layer at position x, + due to group n's neutrons. unit: [mole ^-3] + """ + if num_layer == self.n_layers: + tritium_production_macro_xs_as_mole = 0.0 + else: + tritium_production_macro_xs_as_mole = ( + self.materials[num_layer].sigma_triton[n] / N_A + ) + return ( + tritium_production_macro_xs_as_mole + * self.groupwise_neutron_flux_in_layer(n, num_layer, x) + ) + + # scalar values (one such float per neutron group, and per layer.) + @summarize_values + def groupwise_integrated_flux_in_layer(self, n: int, num_layer: int) -> float: + """ + Calculate the integrated flux[m^-1 s^-1], which can be mulitplied to any + macroscopic cross-section [m^-1] to get the reaction rate [s^-1] in + any layer specified. + + Parameters + ---------- + n: + The index of the neutron group that needs to be solved. n <= n_groups - 1. + Therefore n=0 shows the integrated flux for group 1, n=1 for group 2, etc. + num_layer: + The index of the layer that we want to get the integrated flux for. + """ + if num_layer == self.n_layers: + return np.nan + integrals = [] + # set integration limits + x_start = self.layer_x[num_layer - 1] + if num_layer == 0: + x_start = 0.0 + x_end = self.layer_x[num_layer] + + for g in range(len(self.coefficients[num_layer, n])): + c_int, s_int = self._groupwise_cs_definite_integral_in_layer( + g, num_layer, x_start, x_end + ) + integrals.extend([ + self.coefficients[num_layer, n].c[g] * c_int, + self.coefficients[num_layer, n].s[g] * s_int, + ]) + return np.sum(integrals, axis=0) + + @summarize_values + def groupwise_neutron_current_through_interface( + self, n: int, n_interface: int, *, default_to_inner_layer: bool = True + ) -> float: + """ + Net current from left to right on the positive side of the model, at + the specified interface number. + + Parameters + ---------- + n: + The index of the neutron group that we want the current for. n <= n_groups - 1. + Therefore n=0 shows the neutron current for group 1, n=1 for group 2, etc. + n_interface: + The index of the interface that we want the net neutron current + through for. + E.g. for n_interface=0, that would be getting the current at the + plasma-fw interface. For n_interface=n_layers, that would be + getting the neutron current leaking from the final layer into the void. + + Returns + ------- + : + current in m^-2 + """ + x = self.interface_x[n_interface] + + if default_to_inner_layer: + if n_interface == 0: + return self.groupwise_neutron_current_in_layer(n, 0, x) + return self.groupwise_neutron_current_in_layer(n, n_interface - 1, x) + if n_interface == self.n_layers: + return self.groupwise_neutron_current_in_layer(n, self.n_layers - 1, x) + return self.groupwise_neutron_current_in_layer(n, n_interface, x) + + @summarize_values + def groupwise_neutron_current_escaped(self, n: int) -> float: + """ + Neutron current escaped from the breeding zone to outside the reactor. + Parameters + ---------- + n: + The index of the neutron group that we want the current for. n <= n_groups - 1. + Therefore n=0 shows the neutron current for group 1, n=1 for group 2, etc. + + Returns + ------- + : + current in m^-2 + """ + return self.groupwise_neutron_current_through_interface(n, self.n_layers) + + @summarize_values + def groupwise_integrated_heating_in_layer( + self, + n: int, + num_layer: int, + ) -> float: + """ + The total amount of heat produced (per unit area) due to neutron + heating across the entire num_layer-th layer. unit: [W m^-2]. It should + yield the same result as integrating the curve neutron_heating_in_layer + from self.interface_x[n] to self.interface_x[n+1]. + """ + if num_layer == self.n_layers: + return 0.0 + return self.groupwise_linear_heating_density_in_layer( + n, num_layer + ) * self.groupwise_integrated_flux_in_layer(n, num_layer) + + @summarize_values + def groupwise_integrated_tritium_production_in_layer( + self, + n: int, + num_layer: int, + ) -> float: + """ + The total amount of tritium produced (per unit area) due to (n,t*) + reactions across the entire num_layer-th layer. unit: [mole m^-2]. It should + yield the same result as integrating the curve tritium_production_in_layer + from self.interface_x[n] to self.interface_x[n+1]. + + Returns + ------- + : + tritium production rate integrated across the entire layer. + unit: [mole m^-2] + """ + if num_layer == self.n_layers: + tritium_production_macro_xs_as_mole = 0.0 + else: + tritium_production_macro_xs_as_mole = ( + self.materials[num_layer].sigma_triton[n] / N_A + ) + return ( + tritium_production_macro_xs_as_mole + * self.groupwise_integrated_flux_in_layer(n, num_layer) + ) + + # Do NOT add a summarize_values decorator, as you can't add cross-sections + # from different groups together without first multiplying by flux to get reaction rate. + def groupwise_linear_heating_density_in_layer( + self, + n: int, + num_layer: int, + ) -> float: + """ + unit: [J m^-1] + All reactions that does not lead to scattering are assumed to have + the full energy of the neutron deposited into the material. + Obviously this contradicts the assumption of neutrons retaining some of + its energy in the n,2n reaction, but we hope this is a small enough + error that we can overlook it. + """ + if num_layer == self.n_layers: + return 0.0 + mat = self.materials[num_layer] + non_scatter_xs = mat.sigma_t[n] - mat.sigma_s[n, :].sum() + lost_energy = ( + (self.group_energy[n] - self.group_energy[n:]) * mat.sigma_s[n, n:] + ).sum() + return self.group_energy[n] * non_scatter_xs + lost_energy + + @classmethod + def get_output_unit(cls, method: Callable) -> str | None: + """ + Check a method's outputted quantity's unit + Parameters + ---------- + method: + A method whose name we shall be inspecting and comparing against + UNIT_LOOKUP. + Returns + ------- + : + If a match is found, return the unit as a string. Otherwise, return + None. + """ + for quantity, unit in UNIT_LOOKUP.items(): + if quantity in method.__name__: + return unit + return None + + def plot( + self, + quantity: str = "flux", + ax: plt.Axes | None = None, + *, + plot_groups: bool = True, + symmetric: bool = True, + extend_plot_beyond_boundary: bool = True, + n_points: int = 100, + ): + """ + Make a rough plot of the neutron flux. + + Parameters + ---------- + quantity: + Options of plotting which quantity: {"flux", "current", "heating"}. + ax: + A plt.Axes object to plot on. + n_points: + Number of points to be used for plotting. + symmetric: + Whether to plot from -x to x (symmetric), or from 0 to x + (right side only.) + plot_groups: + Whether to plot each individual group's neutron flux. + If True, a legend will be added to help label the groups. + """ + self.solve() + ax = ax or plt.axes() + method_name = f"neutron_{quantity}_in_layer" + if quantity == "tritium_production": + method_name = "tritium_production_in_layer" + total_function = getattr(self, method_name) + unit = self.get_output_unit(total_function) + ylabel = f"{quantity}({unit})" + + if plot_groups: + groupwise_function = getattr(self, f"groupwise_{method_name}") + x_ranges = _generate_x_range( + self.interface_x.copy(), + max(self.extended_boundary.values()) + if extend_plot_beyond_boundary + else None, + min_total_num_points=n_points, + symmetric=symmetric, + ) + for num_layer in range(self.n_layers + bool(extend_plot_beyond_boundary)): + if symmetric: + neg_x = next(x_ranges) + ax.plot(neg_x, total_function(num_layer, neg_x), color="black") + pos_x = next(x_ranges) + plot_dict = {"label": "total"} if num_layer == 0 else {} + ax.plot( + pos_x, + total_function(num_layer, pos_x), + color="black", + **plot_dict, + ) + if plot_groups: + for n in range(self.n_groups): + if symmetric: + ax.plot( + neg_x, + groupwise_function(n, num_layer, neg_x), + color=f"C{n}", + ) + plot_dict = {"label": f"group {n}"} if num_layer == 0 else {} + ax.plot( + pos_x, + groupwise_function(n, num_layer, pos_x), + color=f"C{n}", + **plot_dict, + ) + ax.legend() + if quantity == "tritium_production": + ax.set_title("Tritium production profile") + else: + ax.set_title(f"Neutron {quantity} profile") + ax.set_xlabel("Distance from the plasma-fw interface [m]") + ax.set_ylabel(ylabel) + + # plotting the interfaces for ease of comprehension. + ylims = ax.get_ylim() + for (xmin, xmax), mat in zip( + pairwise(self.interface_x), self.materials, strict=False + ): + _plot_vertical_dotted_line(ax, xmin, ylims, symmetric=symmetric) + ax.text(np.mean([xmin, xmax]), 0, mat.name, ha="center", va="center") + if symmetric: + ax.text( + -np.mean([xmin, xmax]), + 0, + mat.name, + ha="center", + va="center", + ) + _plot_vertical_dotted_line(ax, xmax, ylims, symmetric=symmetric) + return ax + + +def _get_sign_of(x_values): + """ + Get sign of any real number, but also forces 0.0 to be +ve and -0.0 to be -ve. + The neutron current for the first group (in a non-breeding/weakly breeding + scenario) is strongest at x=0, but have different signs when limit x-> 0^+ + and limit x-> 0^-. This function allows the input x to behave like 0^+ when + it's =0.0 and like 0^- when it's =-0.0, giving the correct neutron current + at those locations, rather than setting the neutron current to zer0. + """ + negatives = np.signbit(x_values) + return np.array(negatives, dtype=float) * -2 + 1 + + +def _plot_vertical_dotted_line(ax, x, ylims, *, symmetric: bool = True): + if symmetric: + ax.plot([-x, -x], ylims, color="black", ls="--", zorder=-1) + ax.plot([x, x], ylims, color="black", ls="--", zorder=-1) + return + + +def _generate_x_range( + interface_x: npt.NDArray[np.float64], + extension_to_be_plotted: float | None = None, + *, + min_total_num_points: int = 100, + symmetric: bool = True, +): + """Helper generator for finding the range of x-values to be plotted. + + Parameters + ---------- + interface_x: + The x-coordinates of each interface. It should be all-positive, and + ascending only. + extended_boundary: + extended boundary + min_total_num_points: + Approximate number of points to be plotted. Increase this number to increase the resolution + symmetric: + Whether to return two copies (one negative, one positive) of x-ranges + per layer. + + Yields + ------- + : + A generator of x-coordinates used for plotting, each of which falls + within the limit of the xmin and xmax of that layer, forming a total + of a minimum of min_total_num_points. + If symmetric=True, then the number of numpy arrays in the list + = 2*n_layers, where out_x_range[0] and out_x_range[1] are for the + negative and positive sides of the first layer respectively; + out_x_range[2] and out_x_range[3] are for the negative and positive + sides of the second layer respectively. + Otherwise, the number of numpy arrays in the list = n_layers. + """ + full_x_range = np.linspace( + interface_x.min(), interface_x.max(), min_total_num_points + ) + for xmin, xmax in pairwise(interface_x): + num_points = np.logical_and(xmin < full_x_range, full_x_range < xmax).sum() + 2 + layer_x_range = np.linspace(xmin, xmax, num_points) + if symmetric: + yield -layer_x_range[::-1] + yield layer_x_range.copy() + + if extension_to_be_plotted: + if (extension_to_be_plotted <= interface_x).any(): + raise ValueError( + "The extension_to_be_plotted must extend beyond all of the " + "layers' x-coordinates!" + ) + layer_x_range = np.array([ + np.nextafter(xmax, np.inf), + extension_to_be_plotted, + ]) + if symmetric: + yield -layer_x_range[::-1] + yield layer_x_range + + +def multiply_2_2_matrices(*matrices): + """ + Multiply a chain of 2x2 matrices from the left (smallest index) to the + right. + + Parameters + ---------- + Matrices: + An iterable of matrices. + + Returns + ------- + matrix: + A 2x2 matrix + """ + seed_matrix = np.identity(2) + for matrix in matrices: + seed_matrix = seed_matrix @ matrix + return seed_matrix diff --git a/process/models/neutronics/data.py b/process/models/neutronics/data.py new file mode 100644 index 000000000..e53d2b32d --- /dev/null +++ b/process/models/neutronics/data.py @@ -0,0 +1,993 @@ +""" +Some simple constants used by the neutronics module, as well as functions used +to load in nuclear data. + +A mock class ENDFRecord with the method get_xs is used. This allows process to +extract nuclear data from any other external module, as long as that module can +be used to produce an object with the get_xs method. +""" + +import json +import warnings +from abc import ABC, abstractmethod +from collections.abc import Callable, Iterable +from dataclasses import dataclass +from itertools import pairwise +from pathlib import Path +from typing import ClassVar + +import numpy as np +from numpy import typing as npt +from scipy.constants import Avogadro + +from process.core.exceptions import ProcessValidationError + +BARNS_TO_M2 = 1e-28 +N_A = Avogadro +EV_TO_J = 1.602e-19 +DT_NEUTRON_E = 14.06e6 * EV_TO_J + +with open(Path(__file__).parent / "atomic_data.json") as j: + _data = json.load(j) + _ATOMIC_MASS_SOURCE = _data["ATOMIC_MASS_SOURCE"] + ATOMIC_MASS = _data["ATOMIC_MASS"] + _NATURAL_ABUNDANCE_SOURCE = _data["NATURAL_ABUNDANCE_SOURCE"] + NATURAL_ABUNDANCE = _data["NATURAL_ABUNDANCE"] + + +def get_isotopic_composition(element: str): + """Get the isotopic composition of a naturally occurring element.""" + return { + iso: frac + for iso, frac in NATURAL_ABUNDANCE.items() + if read_elem_and_mass(iso)[0] == element + } + + +def elem_to_isotopic_comp(composition: dict[str, float]) -> dict[str, float]: + """ + Convert an element composition dictionary into an isotope composition + dictionary using NATURAL_ABUNDANCE. + Parameters + ---------- + composition: + A dictionary showing the atomic fraction that each relevant element + makes up. + + Returns + ------- + new_comp_dict: + A dictionary showing the atomic fraction that each relevant isotope + makes up. + """ + new_comp_dict = {} + for elem, overall_fraction in composition.items(): + for isotope, fraction in get_isotopic_composition(elem).items(): + new_comp_dict[isotope] = overall_fraction * fraction + return new_comp_dict + + +def get_avg_atomic_mass(composition: dict[str, float]) -> float: + """Calculate the average atomic mass number. + Parameters + ---------- + composition: + A dictionary showing the atomic fraction that each species makes up. + """ + total_fraction = sum(composition.values()) + return sum( + ATOMIC_MASS[species] * (fraction / total_fraction) + for species, fraction in composition.items() + ) + + +class ENDFRecord(ABC): + @abstractmethod + def get_xs( + self, mt: int, group_structure: npt.NDArray[np.float64] + ) -> npt.NDArray[np.float64]: + """ + A method that returns the formatted (integrated, weighted by lethargy) + cross-section. + """ + + +@dataclass +class MTTreeNode: + reaction_name: str + main_number: int + constituents: tuple[int] + auxillary: dict[int, str] + + def resolve_xs( + self, endf_record: ENDFRecord, group_structure: npt.NDArray[np.float64] + ) -> npt.NDArray[np.float64]: + """ + Check if the endf record is sesnsible by following the correct summation rules, + and then extract the correct cross-section. + + Parameters + ---------- + endf_record: + an instance of a class with the method with a signature of (self, group_structure) + group_structure: + A 1D array of numbers representing the group boundaries, from high + energy to low energy (low lethargy to high lethargy). + + Returns + ------- + xs: + formatted to the correct group structure. + """ + main_xs = endf_record.get_xs(self.main_number, group_structure) + constituent_xs = np.sum( + [endf_record.get_xs(mt, group_structure) for mt in self.constituents], + axis=0, + ) + if ( + np.sum(main_xs) < np.sum(constituent_xs) + and not np.isclose( + np.sum(main_xs), np.sum(constituent_xs), atol=0, rtol=0.001 + ).all() + ): + raise ValueError(f"MT={self.main_number} does not match the constituents") + if len(main_xs) != (len(group_structure) - 1): + raise ValueError(f"{self.mt_tuple} resolves to zero!") + auxillary = np.sum( + [endf_record.get_xs(mt, group_structure) for mt in self.auxillary], + axis=0, + ) + return main_xs + auxillary + + +class MTResolutionRule: + def __init__( + self, + mt_tuple: tuple[MTTreeNode], + redundant_reaction: str, + redundant_mt: int, + ): + self.redundant_reaction = str(redundant_reaction) + self.redundant_mt = int(redundant_mt) + self.mt_tuple = mt_tuple + + def resolve_xs( + self, endf_record: ENDFRecord, group_structure: npt.NDArray[np.float64] + ) -> npt.NDArray[np.float64]: + """ + Check if the endf record is sesnsible by following the correct summation rules, + and then extract the correct cross-section. + + Parameters + ---------- + endf_record: + an instance of a class with the method with a signature of (self, group_structure) + group_structure: + A 1D array of numbers representing the group boundaries, from high + energy to low energy (low lethargy to high lethargy). + + Returns + ------- + xs: + formatted to the correct group structure. + """ + redundant_xs = endf_record.get_xs(self.redundant_mt, group_structure) + main_xs = np.sum( + [mt.resolve_xs(endf_record, group_structure) for mt in self.mt_tuple], + axis=0, + ) + if np.sum(redundant_xs): + if np.sum(redundant_xs) < np.sum(main_xs) and not np.isclose( + np.sum(redundant_xs), np.sum(main_xs), atol=1e-10, rtol=0.01 + ): + # raise ValueError( + # f"mt={self.redundant_mt} failed to include everything." + # ) + pass + return redundant_xs + return main_xs + + +MT_N2N = MTTreeNode( + "n,2n", + 16, + range(875, 892), + {11: "n,2nd", 21: "n,2nf", 24: "n,2na", 30: "n,2n2a", 41: "n,2np"}, +) +MT_N3N = MTTreeNode("n,3n", 17, (), {25: "n,3na", 38: "n,3nf", 42: "n,3np"}) +MT_N4N = MTTreeNode("n,4n", 37, (), {}) +MT_TRITON = MTTreeNode( + "n,t", 105, range(700, 750), {33: "n,nt", 113: "n,t2a", 116: "n,pt"} +) +MT_SCAT_INELASTIC = MTTreeNode( + "n,n'", + 4, + range(50, 92), + { + 20: "n,nf", + 22: "n,na", + 23: "n,n3a", + 28: "n,np", + 29: "n,n2a", + 32: "n,nd", + 33: "n,nt", + 34: "n,n3He", + 35: "n,nd2a", + 36: "n,nt2a", + 44: "n,n2p", + 45: "n,npa", + }, +) + + +class ExtractedNuclearData: + """A class for storing the useful nuclear data extracted out of the ENDFRecord.""" + + rules: ClassVar[dict[str, MTResolutionRule]] = { + "total": MTResolutionRule((), "n,tot", 1), + "elastic_scattering": MTResolutionRule((), "n,n", 2), + "inelastic_scattering": MTResolutionRule((MT_SCAT_INELASTIC,), "n,n'", 4), + "neutron_producing": MTResolutionRule((MT_N2N, MT_N3N, MT_N4N), "n,Xn", 201), + "triton_producing": MTResolutionRule((MT_TRITON,), "n,Xt", 205), + } + + def __init__( + self, + group_structure: npt.NDArray[np.float64], + species: str, + atomic_mass: float, + endf_record: ENDFRecord, + ): + self.group_structure = np.asarray(group_structure) + self.species = species + self.atomic_mass = atomic_mass + self.endf_record = endf_record + + self.sigma_total = self.rules["total"].resolve_xs( + self.endf_record, self.group_structure + ) + self.sigma_triton = self.rules["triton_producing"].resolve_xs( + self.endf_record, self.group_structure + ) + + self.sigma_scatter = ( + self.rules["elastic_scattering"].resolve_xs( + self.endf_record, self.group_structure + ) + * scattering_weight_matrix(self.group_structure, self.atomic_mass).T + ).T + self.sigma_scatter += ( + self.rules["inelastic_scattering"].resolve_xs( + self.endf_record, self.group_structure + ) + * scattering_weight_matrix( + self.group_structure, + self.atomic_mass, + self.endf_record.q_values.get(4, 0.0), + ).T # TODO: calculate with an inelastic scattering weight matrix instead. + ).T + + neutron_producing = self.rules["neutron_producing"].resolve_xs( + self.endf_record, self.group_structure + ) + if np.sum(neutron_producing) == 0: + self.sigma_in = np.zeros(len(self.group_structure) - 1) + return + n2n = self.endf_record.get_xs(16, self.group_structure) + n3n = self.endf_record.get_xs(17, self.group_structure) + n4n = self.endf_record.get_xs(37, self.group_structure) + all_n2n = MT_N2N.resolve_xs(self.endf_record, self.group_structure) + all_n3n = MT_N3N.resolve_xs(self.endf_record, self.group_structure) + all_n4n = MT_N4N.resolve_xs(self.endf_record, self.group_structure) + mt201 = self.endf_record.get_xs(201, self.group_structure) # noqa: F841 + if np.isclose(n2n + n3n + n4n, neutron_producing, atol=0.0).all(): + self.sigma_in = ( + n2n + * nXn_weight_matrix( + self.group_structure, + self.endf_record.q_values.get(16, 0.0), + 2, + ).T + + n3n + * nXn_weight_matrix( + self.group_structure, + self.endf_record.q_values.get(17, 0.0), + 3, + ).T + + n4n + * nXn_weight_matrix( + self.group_structure, + self.endf_record.q_values.get(37, 0.0), + 4, + ).T + ).T + elif np.isclose(n2n + n3n + n4n, neutron_producing, atol=0.0).all(): + self.sigma_in = ( + all_n2n + * nXn_weight_matrix( + self.group_structure, + self.endf_record.q_values.get(16, 0.0), + 2, + ).T + + all_n3n + * nXn_weight_matrix( + self.group_structure, + self.endf_record.q_values.get(17, 0.0), + 3, + ).T + + all_n4n + * nXn_weight_matrix( + self.group_structure, + self.endf_record.q_values.get(37, 0.0), + 4, + ).T + ).T + else: + self.sigma_in = ( + neutron_producing + * nXn_weight_matrix( + self.group_structure, + self.endf_record.q_values.get(16, 0.0), + 2, + ).T + ).T + self.sigma_in = ( + neutron_producing + * nXn_weight_matrix( + self.group_structure, self.endf_record.q_values.get(16, 0.0), 2 + ).T + ).T + return + + +class DummyExtractedNuclearData(ExtractedNuclearData): + """A dummy class that returns zeros for all of the relevant cross-sections.""" + + def __init__( + self, + group_structure: npt.NDArray[np.float64], + species: str, + atomic_mass: str | int, + ): + self.group_structure = np.asarray(group_structure) + self.species = species + self.atomic_mass = atomic_mass + n_groups = len(self.group_structure) - 1 + self.sigma_total = np.zeros(n_groups) + self.sigma_scatter = np.zeros([n_groups, n_groups]) + self.sigma_in = np.zeros([n_groups, n_groups]) + self.sigma_triton = np.zeros(n_groups) + + +def read_elem_and_mass(iso_name: str) -> tuple[str, str]: + """ + Read the element and mass number of a string representation of an isotope name. + + Parameters + ---------- + iso_name: + an isotope name e.g. "Fe56" + + Returns + ------- + element_name: + A string + mass_number: + An integer + """ + length = len(iso_name) + name, mass = [], [] + for i in range(length): + name.append(iso_name[i]) + if iso_name[i + 1].isnumeric(): + break + mass = iso_name[i + 1 : length] + return "".join(name), "".join(mass) + + +def is_natural(iso_name: str): + _element, mass = read_elem_and_mass(iso_name) + return mass == "0" + + +def _get_alpha(atomic_mass: float): + return ((atomic_mass - 1) / (atomic_mass + 1)) ** 2 + + +def scattering_weight_matrix( + group_structure: npt.NDArray[np.float64], + atomic_mass: float, + energy_lost=0.0, # noqa: ARG001 +) -> npt.NDArray: + """ + Parameters + ---------- + group_structure: + the n+1 energy bin boundaries for the n neutron groups, in descending energies. + atomic_mass: + atomic mass of the medium + energy_lost: + The energy lost in J. Currently a placeholder variable that isn't used. + TODO: make the inelastic scattering weight matrix account for this loss. + + Returns + ------- + : + An upper triangular matrix, where the i-th row contains the normalized weights for + scattering down from the i-th bin to the j-th bin. The main-diagonal contains + the self-scattering cross-section. + e.g. [2,1] would be the fraction of elastic scattering reactions that causes + neutrons from group 3 to scatter into group 2. + The lower triangle must be all zeros. + np.sum(axis=1) == np.ones(len(group_structure)-1). + """ + + alpha = _get_alpha(atomic_mass) + n_groups = len(group_structure) - 1 + matrix = np.zeros([n_groups, n_groups], dtype=float) + for i, in_group in enumerate(pairwise(group_structure)): + for g, out_group in enumerate(pairwise(group_structure)): + if i == g: + for energy_limits, case_descr in _split_into_energy_limits( + alpha, in_group + ): + matrix[i, g] += _convolved_scattering_fraction( + energy_limits, alpha, in_group, out_group, case_descr + ) + elif i < g: + for energy_limits, case_descr in _split_into_energy_limits( + alpha, in_group, out_group + ): + matrix[i, g] += _convolved_scattering_fraction( + energy_limits, alpha, in_group, out_group, case_descr + ) + return matrix + + +def _split_into_energy_limits( + alpha, + in_group: tuple[float, float], + out_group: tuple[float, float] | None = None, +): + """ + Spit out a list of integration limits to be used by _convolved_scattering_fraction. + + Parameters + ---------- + alpha: + Parameters + in_group: + Descending energy bounds of the input group. Scattering neutrons are + generated from this energy group. (e_i1, e_i) + out_group: + Descending energy bounds of the output group. Scattering neutrons are + born into this energy group. (e_g1, e_g) + + Returns + ------- + : + list of 2-tuples, each two tuple contains the following items: + energy_limits: + A tuple of two floats in descending order, describing the integration + limits. + case_descr: + A string to describe which case it belongs to. + """ + e_i1, e_i = in_group + max_min_scattered_energy = alpha * e_i1 + min_min_scattered_energy = alpha * e_i + + if not out_group: + # min_min_scattered_energy < e_i < max_min_scattered_energy < e_i1 + if (alpha > 0) and (e_i < max_min_scattered_energy): + mid_e = min(e_i / alpha, e_i1) + return [ + ((e_i1, mid_e), "self, complete"), + ((mid_e, e_i), "self, upper-half"), + ] + # min_min_scattered_energy < max_min_scattered_energy < e_i < e_i1 + return [((e_i1, e_i), "self, upper-half")] + e_g1, e_g = out_group + if alpha == 0: + # min_min_scattered_energy < max_min_scattered_energy < e_g < e_g1 + return [((e_i1, e_i), "down, middle")] + + top_e = min(e_g1 / alpha, e_i1) + mid_e = min(e_g / alpha, e_i1) + + # e_g < e_g1 < min_min_scattered_energy < max_min_scattered_energy + if e_g1 <= min_min_scattered_energy: + return [] + + # e_g < min_min_scattered_energy < e_g1 < max_min_scattered_energy + # e_g < min_min_scattered_energy < max_min_scattered_energy < e_g1 + if e_g <= min_min_scattered_energy: + return [((top_e, e_i), "down, upper-half")] + + # min_min_scattered_energy < e_g < e_g1 < max_min_scattered_energy + # min_min_scattered_energy < e_g < max_min_scattered_energy < e_g1 + if e_g < max_min_scattered_energy: + return [ + ((top_e, mid_e), "down, upper-half"), + ((mid_e, e_i), "down, middle"), + ] + + # min_min_scattered_energy < max_min_scattered_energy < e_g < e_g1 + return [((top_e, e_i), "down, middle")] + + +def _convolved_scattering_fraction( + energy_limits: tuple[float, float], + alpha: float, + in_group: tuple[float, float], + out_group: tuple[float, float], + case_description: str, +): + r""" + The fraction of neutron flux from bin i that would get scattered into bin g + is calculated as $M_{ig} = \int_{E_{min}}^{E_{max}} flux_i(E) dist(E) dE$, + where $flux_i=$ normalized flux (we assume the neutron flux in bin i has + constant in per-unit-lethargy space), i.e. follows a 1/E distribution. + Hence after intergration, frac is always accompanied by a factor of + $1/(ln(E_{i-1}) - ln(E_i))$. + """ + e_max, e_min = energy_limits + e_i1, e_i = in_group + e_g1, e_g = out_group + + const = 1 / (np.log(e_i1) - np.log(e_i)) + am1i = 1 / (1 - alpha) + diff_log_e = np.log(e_max) - np.log(e_min) + diff_inv_e = 1 / e_min - 1 / e_max + match case_description: + case "self, complete": + return const * diff_log_e + case "self, upper-half": + return const * am1i * (diff_log_e - e_g * diff_inv_e) + case "down, upper-half": + return const * am1i * -(alpha * diff_log_e - e_g1 * diff_inv_e) + case "down, middle": + return const * am1i * (e_g1 - e_g) * diff_inv_e + + +def nXn_weight_matrix( # noqa: N802 + group_structure: npt.NDArray[np.float64], + q_value: float, + X: int, # noqa: N803 +) -> npt.NDArray: + """ + Parameters + ---------- + group_structure: + the n+1 energy bin boundaries for the n neutron groups, in descending energies, + in J. + q_value: + the q-value of the reaction in J. + + Returns + ------- + : + A macroscopic cross-section matrix, where the j-th column of the i-th row + expresses the probability of one of the n2n neutrons trigged by a i-th + bin neutron ends up in the j-th bin. + np.sum(axis=1) <= np.ones(len(group_structure)-1) * 2, i.e. the + probability distribution in each row (i.e. i-th bin) is normalized to + 1, i.e. the number of neutrons. + """ + # Assume that the two neutrons would share the resulting energy evenly, i.e. + # each take half of the neutron + # To make things even simpler, we'll assume the neutron flux is + shift_e = -q_value / X + + e_i1, e_i = group_structure[:-1], group_structure[1:] + weight = 1 / (np.log(e_i1) - np.log(e_i)) + + n_groups = len(group_structure) - 1 + e_g1 = np.broadcast_to(e_i1, [n_groups, n_groups]) + e_g = np.broadcast_to(e_i, [n_groups, n_groups]) + + e_min = np.clip((e_g + shift_e).T, e_i, e_i1).T + e_max = np.clip((e_g1 + shift_e).T, e_min.T, e_i1).T + + matrix = np.log(e_max) - np.log(e_min) + return (weight * matrix.T).T + + +def get_diffusion_coefficient_and_length( + avg_atomic_mass: float, + total_xs: float, + scattering_xs: float, + in_source_xs: float, +) -> tuple[float, float]: + r""" + Calculate the diffusion coefficient for a given scattering and total macro-scopic + cross-section in a given medium. + + Parameters + ---------- + total_xs: + macroscopic total cross-section `\sigma_{total}`, i.e. any reaction between + nuclei and neutrons, that either changes the neutron's path or remove it from + that energy group. + Unit: m^-1 + scattering_xs: + macroscopic total cross-section `\sigma_{scatter}`, i.e. number of reactions per + unit distance travelled by the neutron that leads to it being scattered (without + getting absorbed). + Unit: m^-1 + avg_atomic_mass: + Average atomic mass in [amu]. This can be approximated by the atomic number 'A' + of the medium that the neutron passes through. The effect of the more-anisotropic + scattering due to smaller atomic number can be accounted for by increasing the + diffusion coefficient (which decreases the transport macroscopic cross-section, + :math:`\Sigma_{tr}=\frac{1}{3D}`). + + Returns + ------- + diffusion_const: + The diffusion coefficient as given by Reactor Analysis, Duderstadt and Hamilton. + unit: [m] + diffusion_len_2: + The square of the characteristic diffusion length as given by Reactor Analysis, + Duderstadt and Hamilton. + unit: [m^2] + """ + + transport_xs = total_xs - 2 / (3 * avg_atomic_mass) * scattering_xs + diffusion_const = 1 / 3 / transport_xs + diffusion_len_2 = diffusion_const / (total_xs - scattering_xs - in_source_xs) + return diffusion_const, diffusion_len_2 + + +class MaterialMacroInfo: + def __init__( + self, + group_structure: npt.NDArray[np.float64], + density: float, + elements: dict[str, float], + name: str = "", + source: str = "", + comment: str = "", + ): + """ + Parameters + ---------- + group_structure: + energy bin edges, 1D array of len = n+1, in [J]. + density: + density of the material in kg/m3 + name: + name of the material (optional) + source: + data source described by a string. + comment: + comment on the data (string) + """ + self.group_structure = np.asarray(group_structure) + if (np.diff(self.group_structure) >= 0).any(): + raise ValueError( + "The group structure must be defined descendingly, from the " + "highest energy bin (i.e. lowest lethargy bin) edge to the " + "lowest energy bin edge, which can't be zero (infinite " + "lethargy). Similarly the cross-section must be arranged " + "according to these bin edges." + ) + if (self.group_structure <= 0).any(): + warnings.warn("Zero energy (inf. lethargy) not allowed.", stacklevel=2) + self.group_structure = np.clip(self.group_structure, 1e-9 * EV_TO_J, np.inf) + self.density = float(density) + self.name = name + self.source = source + self.comment = comment + self._populated = False + self.elements = elements + self.avg_atomic_mass = get_avg_atomic_mass(elem_to_isotopic_comp(self.elements)) + self.number_density = N_A / self.avg_atomic_mass * 1000 + + if (self.group_structure <= 0).any(): + warnings.warn("Zero energy (inf. lethargy) not allowed.", stacklevel=2) + self.group_structure = np.clip(self.group_structure, 1e-9 * EV_TO_J, np.inf) + if (np.diff(self.group_structure) >= 0).any(): + raise ValueError( + "The group structure must be defined descendingly, from the " + "highest energy bin (i.e. lowest lethargy bin) edge to the " + "lowest energy bin edge, which can't be zero (infinite " + "lethargy). Similarly the cross-section must be arranged " + "according to these bin edges." + ) + + def __repr__(self): + return super().__repr__().replace(" at ", f" '{self.name}' at ") + + def _set_sigma(self, sigma_t, sigma_s, sigma_in=None, sigma_triton=None) -> None: + """Populate the values directly. Mainly to make unit-testing easier.""" + self._sigma_total = np.asarray(sigma_t) + self._sigma_scatter = np.asarray(sigma_s) + if sigma_in is not None: + self._sigma_in = np.asarray(sigma_in) + else: + self._sigma_in = np.zeros([self.n_groups, self.n_groups]) + if sigma_triton is not None: + self._sigma_triton = np.asarray(sigma_triton) + else: + self._sigma_triton = np.zeros(self.n_groups) + self._confirm_sigma() + + def _confirm_sigma(self) -> None: + if np.shape(self._sigma_total) != (self.n_groups,): + raise ProcessValidationError( + f"total group-wise cross-sections should have {self.n_groups} " + "groups as specified by the group_structure." + ) + if np.shape(self._sigma_scatter) != (self.n_groups, self.n_groups): + raise ProcessValidationError( + "Group-wise scattering cross-sections be a square matrix of " + f"shape n*n, where n= number of groups = {self.n_groups}." + ) + if (self._sigma_scatter.sum(axis=1) > self._sigma_total).any(): + raise ProcessValidationError( + "Total cross-section should include the scattering cross-section." + ) + if np.tril(self._sigma_scatter, k=-1).any(): + warnings.warn( + "Elastic up-scattering seems unlikely in this model! " + "Check if the group structure is chosen correctly?", + stacklevel=2, + ) + self._diffusion_const, self._l2 = [], [] + for i in range(self.n_groups): + diff_const, l2_i = get_diffusion_coefficient_and_length( + self.avg_atomic_mass, + self._sigma_total[i], + self._sigma_scatter[i, i], + self._sigma_in[i, i], + ) + self._diffusion_const.append(diff_const) + self._l2.append(l2_i) + self._populated = True + return + + def _add_data_from_single_record( + self, xs_data: ExtractedNuclearData, partial_number_density: float + ) -> None: + if not np.isclose(self.group_structure, xs_data.group_structure, atol=0.0).all(): + raise ValueError(f"Mismatched group structure with {xs_data}.") + self._sigma_total += xs_data.sigma_total * partial_number_density * BARNS_TO_M2 + self._sigma_scatter += ( + xs_data.sigma_scatter * partial_number_density * BARNS_TO_M2 + ) + self._sigma_in += xs_data.sigma_in * partial_number_density * BARNS_TO_M2 + self._sigma_triton += xs_data.sigma_triton * partial_number_density * BARNS_TO_M2 + if isinstance(xs_data, DummyExtractedNuclearData): + species = xs_data.species + fraction = elem_to_isotopic_comp(self.elements)[species] + self.comment += f" Missing {species} ({fraction * 100} at.%) " + self.comment += "(filled with dummy zeros)." + + def populate_from_data_library(self, xs_dict: [str, ExtractedNuclearData]) -> None: + """Populate the cross-section values according to the nuclear data library.""" + self._sigma_total = np.zeros(self.n_groups) + self._sigma_scatter = np.zeros([self.n_groups, self.n_groups]) + self._sigma_in = np.zeros([self.n_groups, self.n_groups]) + self._sigma_triton = np.zeros(self.n_groups) + for element, elem_frac in self.elements.items(): + natural = element + "0" + if natural in xs_dict: + self._add_data_from_single_record( + xs_dict[natural], elem_frac * self.number_density + ) + else: + for isotope, natural_abundance in get_isotopic_composition( + element + ).items(): + self._add_data_from_single_record( + xs_dict[isotope], + natural_abundance * elem_frac * self.number_density, + ) + self._confirm_sigma() + return + + @property + def sigma_t(self) -> npt.NDArray[np.float64]: + """total macroscopic cross-section, 1D array of len = n.""" + if not self._populated: + raise ValueError("Empty cross-section data!") + return self._sigma_total + + @property + def sigma_s(self) -> npt.NDArray: + """ + Macroscopic scattering cross-section from group i to j, forming a 2D + array of shape (n, n). It should be mostly-upper-triangular, i.e. the + lower triangle (excluding the main diagonal) must have small values + compared to the average macroscopic cross-section value of the matrix. + Neutrons fluxes are assumed to be isotropic before and after scattering. + + e.g. [0,3] would be the cross-section for group 4 neutrons scattered-in + from group 1 neutrons. + """ + if not self._populated: + raise ValueError("Empty cross-section data!") + return self._sigma_scatter + + @property + def sigma_in(self) -> npt.NDArray: + """ + In-source matrix: for now, it includes a sum of the matrix of (n,2n) + reactions and fission reactions. Same logic as the scattering matrix, + i.e. probability of group j neutrons produced (presumed to be + isotropic) per unit flux of group i neutrons. + + e.g. [0,3] would be the cross-section for the proudction of group 4 + neutrons due to n,2n and fission reactions caused by group 1 neutrons. + """ + if not self._populated: + raise ValueError("Empty cross-section data!") + return self._sigma_in + + @property + def sigma_triton(self) -> npt.NDArray[np.float64]: + if not self._populated: + raise ValueError("Empty cross-section data!") + return self._sigma_triton + + @property + def n_groups(self) -> int: + """ + Number of groups in the group structure. + Store this attribute upon first retrieval. + """ + if not hasattr(self, "_n_groups"): + self._n_groups = len(self.group_structure) - 1 + return self._n_groups + + @property + def diffusion_const(self) -> float: + """ + The diffusion coefficient as given by Reactor Analysis, Duderstadt and Hamilton. + unit: [m] + """ + if not self._populated: + raise ValueError("Empty diffusion constant data!") + return self._diffusion_const + + @property + def l2(self) -> float: + """ + The square of the characteristic diffusion length as given by Reactor Analysis, + Duderstadt and Hamilton. + """ + if not self._populated: + raise ValueError("Empty diffusion length data!") + return self._l2 + + @property + def downscatter_only(self): + """ + Checks if the source matrix suggests that neutrons from each group can + only cause neutrons to be born in higher-lethargy (lower energy) groups. + + If True, the transport equation can be solved acyclically, from low + to high lethargy groups in a single-pass. + + If False, and the transport equation will need to be solved iteratively, + as neutron fluxes in higher-lethargy groups in turn affects the neutron + flux in lower-lethargy groups. + """ + return ~(np.tril(self.sigma_s, k=-1).any() or np.tril(self.sigma_in, k=-1).any()) + + @property + def element_set(self): + if not hasattr(self, "_element_set"): + self._element_set = set(self.elements.keys()) + return self._element_set + + +def accumulate_data_requirements( + mat_list: Iterable[MaterialMacroInfo], +) -> tuple[set[str], set[str]]: + """ + Get the set of isotopes and elements that needs to be extracted from the + nuclear data library. + + Parameters + ---------- + mat_list: + an Iterable of MaterialMacroInfo + + Returns + ------- + required_element_set: + a set containing all of the elements that has appeared on the mat_list + required_isotope_set: + a set containing all of the isotopes that has appeared on the mat_list + """ + required_element_set, required_isotope_set = set(), set() + for mat in mat_list: + required_element_set = required_element_set.union(mat.element_set) + for elem in required_element_set: + required_isotope_set.update(get_isotopic_composition(elem)) + return required_element_set, required_isotope_set + + +def populate_from_nuclear_data_library( + endf_record_path_generator: Iterable[Path], + mat_list: Iterable[MaterialMacroInfo], + quick_isotope_checker: Callable[[Path], tuple[str, str, bool]], + nuclear_data_extractor: Callable[ + [Path, npt.NDArray[np.float64]], ExtractedNuclearData + ], + group_structure: npt.NDArray[np.float64], + *, + suppress_missing_error: bool = False, +) -> None: + """ + Read only the relevant files from endf_record_path_generator, and then + save these data as ExtractedNuclearData. These data is then used to mutate + the MaterialMacroInfo in mat_list, so that they're now populated with the + required data. + + Parameters + ---------- + endf_record_path_generator: + a function that generates the list of paths that will be checked for + matching nuclear data + mat_list: + an iterable of MaterialMacroInfo from which the required isotope set + will be observed. + quick_isotope_checker: + A function to quickly get the isotope name using only the endf file + name. This should preferably not involve parsing the entire endf file. + nuclear_data_extractor: + A function to make the ExtractedNuclearData when given an endf_record + Path. + + Returns + ------- + : + """ + # scrape for all isotopes required. + required_element_set, required_isotope_set = accumulate_data_requirements(mat_list) + + # Gather the records + data_extracted = {} + for endf_record in endf_record_path_generator: + isotope = quick_isotope_checker(endf_record) + element, _mass = read_elem_and_mass(isotope) + if is_natural(isotope) and element in required_element_set: + required_element_set.remove(read_elem_and_mass(isotope)[0]) + data_extracted[isotope] = nuclear_data_extractor( + endf_record, group_structure + ) + elif isotope in required_isotope_set: + required_isotope_set.remove(isotope) + data_extracted[isotope] = nuclear_data_extractor( + endf_record, group_structure + ) + + # Check no data is missing + for isotope in list(required_isotope_set): + if read_elem_and_mass(isotope)[0] not in required_element_set: + required_isotope_set.remove(isotope) + if required_isotope_set: + if not suppress_missing_error: + raise RuntimeError( + f"Missing nuclear data records for {len(required_isotope_set)}" + " isotopes. Their relative abundances are: " + + str({ + iso: NATURAL_ABUNDANCE[iso] + for iso in sorted( + required_isotope_set, + key=lambda i: read_elem_and_mass(i)[1].zfill(4), + ) + }) + ) + # fill with dummy + for isotope in required_isotope_set: + data_extracted[isotope] = DummyExtractedNuclearData( + group_structure, isotope, read_elem_and_mass(isotope)[1] + ) + + for mat in mat_list: + mat.populate_from_data_library(data_extracted) + return diff --git a/tests/regression/test_neutronics.py b/tests/regression/test_neutronics.py new file mode 100644 index 000000000..da8760e0c --- /dev/null +++ b/tests/regression/test_neutronics.py @@ -0,0 +1,422 @@ +import numpy as np +import pytest +from scipy.integrate import trapezoid + +from process.models.neutronics.base import NeutronFluxProfile +from process.models.neutronics.data import ( + DT_NEUTRON_E, + EV_TO_J, + MaterialMacroInfo, + scattering_weight_matrix, +) + +MAX_E = DT_NEUTRON_E * 1.01 +MIN_E = 1 / 40 * EV_TO_J + + +def _diffusion_equation_in_layer(test_profile, n, num_layer, x): + """ + Get the three terms in the diffusion equation (equation 5 in the paper. + """ + diffusion_out = test_profile.materials[num_layer].diffusion_const[ + n + ] * test_profile._groupwise_flux_curvature_in_layer(n, num_layer, x) # noqa: SLF001 + total_removal = test_profile.materials[num_layer].sigma_t[ + n + ] * test_profile.groupwise_neutron_flux_in_layer(n, num_layer, x) + + source_in_terms = [] + in_matrix = ( + test_profile.materials[num_layer].sigma_s + + test_profile.materials[num_layer].sigma_in + ) + for g, all_sources_entering_from_g in enumerate(in_matrix[:, n]): + source_in_terms.append( + all_sources_entering_from_g + * test_profile.groupwise_neutron_flux_in_layer(g, num_layer, x) + ) + + return diffusion_out, total_removal, np.sum(source_in_terms) + + +def test_one_group(): + """ + Regression test against Desmos snapshot: + https://www.desmos.com/calculator/18xojespuo + """ + dummy = [MAX_E, MIN_E] # dummy group structure + # translate from mean-free-path lengths (mfp) to macroscopic cross-sections + mfp_fw_s = 118 * 0.01 # [m] + mfp_fw_t = 16.65 * 0.01 # [m] + sigma_fw_t = 1 / mfp_fw_t # [1/m] + sigma_fw_s = 1 / mfp_fw_s # [1/m] + a_fw = 52 + fw_material = MaterialMacroInfo(dummy, 1.0, {"Te": 1.0}, name="fw") + fw_material.avg_atomic_mass = a_fw + fw_material._set_sigma([sigma_fw_t], [[sigma_fw_s]]) # noqa: SLF001 + + mfp_bz_s = 97 * 0.01 # [m] + mfp_bz_t = 35.8 * 0.01 # [m] + sigma_bz_s = 1 / mfp_bz_s # [1/m] + sigma_bz_t = 1 / mfp_bz_t # [1/m] + a_bz = 71 + bz_material = MaterialMacroInfo(dummy, 1.0, {"Lu": 1.0}, name="bz") + bz_material.avg_atomic_mass = a_bz + bz_material._set_sigma([sigma_bz_t], [[sigma_bz_s]]) # noqa: SLF001 + + x_fw, x_bz = 5.72 * 0.01, 85 * 0.01 + incoming_flux = 41 + neutron_profile = NeutronFluxProfile( + incoming_flux, [x_fw, x_bz], [fw_material, bz_material] + ) + + layer_group_coefs = neutron_profile.coefficients + assert np.isclose(layer_group_coefs[0, 0].c[0], 80.5346770788), "c5" + assert np.isclose(layer_group_coefs[0, 0].s[0], -76.5562120985), "c6" + assert np.isclose(layer_group_coefs[1, 0].c[0], 60.6871656017), "c7" + assert np.isclose(layer_group_coefs[1, 0].s[0], -60.7123696772), "c8" + + assert np.isclose(neutron_profile.neutron_flux_in_layer(0, x_fw), 48.72444) + assert np.isclose(neutron_profile.neutron_flux_in_layer(1, x_fw), 48.72444) + assert np.isclose(neutron_profile.neutron_flux_at(x_fw), 48.72444) + + assert np.isclose( + neutron_profile.neutron_current_through_interface(1), 22.3980214162 + ) + assert np.isclose(neutron_profile.neutron_current_escaped(), 1.22047369356) + + fw_removal = sigma_fw_t - sigma_fw_s - fw_material.sigma_in[0, 0] + bz_removal = sigma_bz_t - sigma_bz_s - bz_material.sigma_in[0, 0] + assert np.isclose( + neutron_profile.fluxes[0], + neutron_profile.neutron_current_escaped() + + fw_removal * neutron_profile.integrated_flux_in_layer(0) + + bz_removal * neutron_profile.integrated_flux_in_layer(1), + ), "Conservation of neutrons" + + x_fw = np.linspace(*neutron_profile.interface_x[0:2], 100000) + manually_integrated_heating_fw = trapezoid( + neutron_profile.neutron_heating_in_layer(0, x_fw), + x_fw, + ) + x_bz = np.linspace(*neutron_profile.interface_x[1:3], 100000) + manually_integrated_heating_bz = trapezoid( + neutron_profile.neutron_heating_in_layer(1, x_bz), + x_bz, + ) + assert np.isclose( + neutron_profile.integrated_heating_in_layer(0), + manually_integrated_heating_fw, + atol=0, + rtol=1e-8, + ), "Correctly integrated heating in FW" + assert np.isclose( + neutron_profile.integrated_heating_in_layer(1), + manually_integrated_heating_bz, + atol=0, + rtol=1e-8, + ), "Correctly integrated heating in BZ" + assert np.isclose(neutron_profile.neutron_current_at(0), incoming_flux) + + +def test_one_group_with_fission(): + """ + Regression test against Desmos snapshot with fission involved: + https://www.desmos.com/calculator/cd830add9c + Expecting a cosine-shape (dome shape!) of neutron flux profile. + """ + dummy = [MAX_E, MIN_E] + mfp_fw_s = 118 * 0.01 # [m] + mfp_fw_t = 16.65 * 0.01 # [m] + sigma_fw_t = 1 / mfp_fw_t # [1/m] + sigma_fw_s = 1 / mfp_fw_s # [1/m] + a_fw = 52 + fw_material = MaterialMacroInfo(dummy, 1.0, {"Te": 1.0}, name="fw") + fw_material.avg_atomic_mass = a_fw + fw_material._set_sigma([sigma_fw_t], [[sigma_fw_s]]) # noqa: SLF001 + + mfp_bz_s = 97 * 0.01 # [m] + mfp_bz_t = 35.8 * 0.01 # [m] + sigma_bz_s = 1 / mfp_bz_s # [1/m] + sigma_bz_t = 1 / mfp_bz_t # [1/m] + a_bz = 71 + + g = 1.2 + nu_sigma_bz_f = g * (sigma_bz_t - sigma_bz_s) + bz_material = MaterialMacroInfo(dummy, 1.0, {"Lu": 1.0}, name="bz") + bz_material.avg_atomic_mass = a_bz + bz_material._set_sigma([sigma_bz_t], [[sigma_bz_s]], [[nu_sigma_bz_f]]) # noqa: SLF001 + + x_fw, x_bz = 5.72 * 0.01, 85 * 0.01 + incoming_flux = 41 + neutron_profile = NeutronFluxProfile( + incoming_flux, + [x_fw, x_bz], + [fw_material, bz_material], + ) + assert np.isclose(neutron_profile.materials[1].l2[0], -((58.2869567709 / 100) ** 2)) + assert np.isclose(neutron_profile.neutron_flux_at(-4.79675 / 100), 159.9434), ( + "Minimum flux in FW" + ) + assert np.isclose(neutron_profile.neutron_flux_at(4.79675 / 100), 159.9434), ( + "Minimum flux in FW" + ) + assert np.isclose(neutron_profile.neutron_flux_at(18.96382 / 100), 164.81245), ( + "Maximum flux in BZ" + ) + assert np.isclose(neutron_profile.neutron_flux_at(-18.96382 / 100), 164.81245), ( + "Maximum flux in BZ" + ) + assert np.isclose( + neutron_profile.neutron_flux_in_layer(0, x_fw), + neutron_profile.neutron_flux_in_layer(1, x_fw), + ), "Flux continuity assurance" + assert np.isclose( + neutron_profile.neutron_current_through_interface(1), + -7.6275782637960745, + ), "Negative current because BZ (breeding) is backflowing into the FW" + assert np.isclose(neutron_profile.neutron_current_escaped(), 30.665951670177186), ( + "positive escaped current." + ) + fw_removal = sigma_fw_t - sigma_fw_s - fw_material.sigma_in[0, 0] + bz_removal = sigma_bz_t - sigma_bz_s - bz_material.sigma_in[0, 0] + + assert np.isclose( + neutron_profile.fluxes[0], + neutron_profile.neutron_current_escaped() + + fw_removal * neutron_profile.integrated_flux_in_layer(0) + + bz_removal * neutron_profile.integrated_flux_in_layer(1), + ), "Conservation of neutrons" + x_fw = np.linspace(*neutron_profile.interface_x[0:2], 100000) + manually_integrated_heating_fw = trapezoid( + neutron_profile.neutron_heating_in_layer(0, x_fw), + x_fw, + ) + x_bz = np.linspace(*neutron_profile.interface_x[1:3], 100000) + manually_integrated_heating_bz = trapezoid( + neutron_profile.neutron_heating_in_layer(1, x_bz), + x_bz, + ) + assert np.isclose( + neutron_profile.integrated_heating_in_layer(0), + manually_integrated_heating_fw, + atol=0, + rtol=1e-8, + ), "Correctly integrated heating in FW" + assert np.isclose( + neutron_profile.integrated_heating_in_layer(1), + manually_integrated_heating_bz, + atol=0, + rtol=1e-8, + ), "Correctly integrated heating in BZ" + + +def test_fission_plot(): + """Same regression test as test_one_group_with_fission + https://www.desmos.com/calculator/cd830add9c + But we also plot. + """ + dummy = [MAX_E, MIN_E] + mfp_fw_s = 118 * 0.01 # [m] + mfp_fw_t = 16.65 * 0.01 # [m] + sigma_fw_t = 1 / mfp_fw_t # [1/m] + sigma_fw_s = 1 / mfp_fw_s # [1/m] + a_fw = 52 + fw_material = MaterialMacroInfo(dummy, 1.0, {"Te": 1.0}, name="fw") + fw_material.avg_atomic_mass = a_fw + fw_material._set_sigma([sigma_fw_t], [[sigma_fw_s]]) # noqa: SLF001 + + mfp_bz_s = 97 * 0.01 # [m] + mfp_bz_t = 35.8 * 0.01 # [m] + sigma_bz_s = 1 / mfp_bz_s # [1/m] + sigma_bz_t = 1 / mfp_bz_t # [1/m] + a_bz = 71 + + g = 1.2 + nu_sigma_bz_f = g * (sigma_bz_t - sigma_bz_s) + bz_material = MaterialMacroInfo(dummy, 1.0, {"Lu": 1.0}, name="bz") + bz_material.avg_atomic_mass = a_bz + bz_material._set_sigma([sigma_bz_t], [[sigma_bz_s]], [[nu_sigma_bz_f]]) # noqa: SLF001 + + x_fw, x_bz = 5.72 * 0.01, 85 * 0.01 + incoming_flux = 41 + neutron_profile = NeutronFluxProfile( + incoming_flux, + [x_fw, x_bz], + [fw_material, bz_material], + ) + neutron_profile.plot("flux") + neutron_profile.plot("heating") + neutron_profile.plot("tritium_production") + neutron_profile.plot("current") + + +def test_two_identical_materials(): + """ + A 2-layer model (both layers being made of material A) should have the same + neutron spectrum and flux profiles as a one-layer model. + """ + + +@pytest.mark.filterwarnings("ignore:Calculation of flux") +def test_5_5(): + """Create an arbitrary 5-layer 5-group model. Check for continuity and conformity to the equation.""" + dummy_group_structure = np.geomspace(MAX_E, MIN_E, 5 + 1) + mat_list = [] + at_masses = np.geomspace(1, 100, 5)[[3, 0, 2, 4, 1]] + sigma_t_lists = [ # arbitrarily chosen and rearranged numbers + 1 / (80 + np.linspace(-40, 40, 5)[[0, 2, 4, 1, 3]]), + 1 / (200 + np.linspace(-30, 30, 5)[[0, 3, 1, 4, 2]]), + 1 / (300 + np.linspace(-20, 20, 5)[[1, 3, 0, 2, 4]]), + 1 / (100 + np.linspace(-40, 40, 5)[[1, 4, 2, 0, 3]]), + 1 / (50 + np.linspace(-10, 10, 5)[[2, 0, 3, 1, 4]]), + ] + sigma_s_list = [ + sigma_t_lists[0] * [0.8, 0.7, 0.6, 0.6, 0.5], + sigma_t_lists[1] * [0.4, 0.3, 0.2, 0.1, 0.0], + sigma_t_lists[2] * [0.8, 0.7, 0.6, 0.6, 0.5], + sigma_t_lists[3] * [0.9, 0.9, 0.6, 0.4, 0.1], + sigma_t_lists[4] * [0.6, 0.5, 0.3, 0.3, 0.2], + ] + sigma_in_list = [ + [0.001, 0.002, 0, 0, 0.005], + np.where([0, 0, 1, 1, 0], sigma_t_lists[1] * 1.0, [0, 0, 0, 0, 0]), + np.where([0, 1, 0, 1, 0], sigma_t_lists[2] * 0.6, [0, 0, 0, 0, 0]), + np.where([1, 0, 1, 1, 1], sigma_t_lists[3] * 2.2, [0, 0, 0, 0, 0]), + [0, 0, 0, 0, 0], + ] + # sigma_in_list = [[0,0,0,0,0] for _ in range(5)] + for i in range(5): + mat = MaterialMacroInfo(dummy_group_structure, 1.0, {"C": 1.0}, name=f"mat{i}") + mat.avg_atomic_mass = at_masses[i] + mat._set_sigma( # noqa: SLF001 + sigma_t=sigma_t_lists[i], + sigma_s=( + sigma_s_list[i] + * scattering_weight_matrix(dummy_group_structure, at_masses[i]).T + ).T, + sigma_in=( + sigma_in_list[i] + * scattering_weight_matrix(dummy_group_structure, at_masses[i]).T + ).T, + ) + mat_list.append(mat) + incoming_flux = 100.0 + neutron_profile = NeutronFluxProfile(incoming_flux, [5, 10, 15, 20, 25], mat_list) + neutron_profile.solve() + for num_layer in range(neutron_profile.n_layers): + mid_point = np.mean(neutron_profile.interface_x[num_layer : num_layer + 2]) + layer_x = neutron_profile.layer_x[num_layer] + for n in range(neutron_profile.n_groups): + # Check for conformity with the diffusion equation + diffusion_out, total_removal, source_in = _diffusion_equation_in_layer( + neutron_profile, n, num_layer, mid_point + ) + assert np.isclose(diffusion_out, total_removal - source_in), ( + "Check that the diffusion equation holds up at an arbitrary point." + ) + if num_layer == neutron_profile.n_layers - 1: + continue + # Check for continuity of flux and current + assert np.isclose( + neutron_profile.groupwise_neutron_flux_in_layer(n, num_layer, layer_x), + neutron_profile.groupwise_neutron_flux_in_layer( + n, num_layer + 1, layer_x + ), + ) + assert np.isclose( + neutron_profile.groupwise_neutron_current_in_layer( + n, num_layer, layer_x + ), + neutron_profile.groupwise_neutron_current_in_layer( + n, num_layer + 1, layer_x + ), + ) + assert np.isclose( + neutron_profile.neutron_flux_in_layer(num_layer, layer_x), + neutron_profile.neutron_flux_in_layer(num_layer + 1, layer_x), + ) + assert np.isclose( + neutron_profile.neutron_current_in_layer(num_layer, layer_x), + neutron_profile.neutron_current_in_layer(num_layer + 1, layer_x), + ) + # Check for extended boundary flux = 0 + num_layer = neutron_profile.n_layers - 1 + for n in range(neutron_profile.n_groups): + assert np.isclose( + neutron_profile.groupwise_neutron_flux_in_layer( + n, num_layer, neutron_profile.extended_boundary[n] + ), + 0, + ), f"flux at Extended boundary of group {n} should = 0" + + no_incident_flux_err_msg = ( + "Expected no incident neutron flux from the plasma except in energy group 0." + ) + for n in range(neutron_profile.n_groups): + assert np.isclose( + neutron_profile.groupwise_neutron_current_at(n, 0), + incoming_flux * int(n == 0), + ), no_incident_flux_err_msg + + sigma_t = np.array([mat.sigma_t for mat in neutron_profile.materials]) + sigma_s = np.array([mat.sigma_s for mat in neutron_profile.materials]) + sigma_in = np.array([mat.sigma_in for mat in neutron_profile.materials]) + shape = np.array([neutron_profile.n_layers, neutron_profile.n_groups]) + in_flow = np.zeros(shape) + in_scatter = np.zeros(shape) + removal = np.zeros(shape) + for n in range(neutron_profile.n_groups): + for num_layer in range(neutron_profile.n_layers): + in_flow[num_layer, n] = neutron_profile.groupwise_neutron_current_in_layer( + n, num_layer, neutron_profile.interface_x[num_layer] + ) - neutron_profile.groupwise_neutron_current_in_layer( + n, num_layer, neutron_profile.interface_x[num_layer + 1] + ) + in_scatter[num_layer, n] = sum( + (sigma_s[num_layer, in_group, n] + sigma_in[num_layer, in_group, n]) + * neutron_profile.groupwise_integrated_flux_in_layer(in_group, num_layer) + for in_group in range(neutron_profile.n_groups) + if in_group < n + ) + removal[num_layer, n] = ( + sigma_t[num_layer, n] + - sigma_s[num_layer, n, n] + - sigma_in[num_layer, n, n] + ) * neutron_profile.groupwise_integrated_flux_in_layer(n, num_layer) + assert np.isclose( + neutron_profile.groupwise_neutron_current_through_interface( + n, num_layer + 1 + ), + neutron_profile.groupwise_neutron_current_escaped(n), + ) + assert np.isclose( + neutron_profile.groupwise_neutron_current_at( + n, neutron_profile.layer_x[num_layer] + ), + neutron_profile.groupwise_neutron_current_through_interface( + n, num_layer + 1 + ), + ) + assert np.isclose(in_flow + in_scatter, removal, atol=0, rtol=1e-9).all(), ( + f"Mismatch between {num_layer} group {n} influx and outflux" + ) + + removal_xs = np.zeros(shape) + int_flux = np.zeros(shape) + for num_layer in range(neutron_profile.n_layers): + removal_xs[num_layer] = ( + neutron_profile.materials[num_layer].sigma_t + # Count all neutrons consumed, but not if they popped up anywhere else. + - neutron_profile.materials[num_layer].sigma_s.sum(axis=1) + - neutron_profile.materials[num_layer].sigma_in.sum(axis=1) + ) + int_flux[num_layer] = [ + neutron_profile.groupwise_integrated_flux_in_layer(n, num_layer) + for n in range(neutron_profile.n_groups) + ] + + assert np.isclose( + sum(neutron_profile.fluxes), + neutron_profile.neutron_current_escaped() + (removal_xs * int_flux).sum(), + ), "Conservation of neutrons" diff --git a/tests/unit/test_neutronics.py b/tests/unit/test_neutronics.py new file mode 100644 index 000000000..e4de0c719 --- /dev/null +++ b/tests/unit/test_neutronics.py @@ -0,0 +1,445 @@ +import numpy as np +import pytest + +from process.core.exceptions import ProcessValidationError +from process.models.neutronics.base import ( + AutoPopulatingDict, + LayerSpecificGroupwiseConstants, + NeutronFluxProfile, + _get_sign_of, +) +from process.models.neutronics.data import DT_NEUTRON_E, EV_TO_J, MaterialMacroInfo + +MAX_E = DT_NEUTRON_E * 1.01 +MIN_E = 1 / 40 * EV_TO_J + + +def test_group_structure_0_energy(): + with pytest.warns(): + mat = MaterialMacroInfo([1.0, 0.0], 1.0, {"C": 1.0}) + mat._set_sigma([1.0], [[1.0]]) # noqa: SLF001 + + +def test_group_structure_too_short(): + with pytest.raises(ProcessValidationError): + mat = MaterialMacroInfo([1.0], 1.0, {"C": 1.0}) + mat._set_sigma([1.0], [[1.0]]) # noqa: SLF001 + + +def test_sigma_s_incorrect_shape(): + with pytest.raises(ProcessValidationError): + mat = MaterialMacroInfo([1000, 10, 1.0], 1.0, {"C": 1.0}) + mat._set_sigma([1.0, 2.0], [1.0, 1.0]) # noqa: SLF001 + + +def test_sigma_s_too_large(): + with pytest.raises(ProcessValidationError): + mat = MaterialMacroInfo([1000, 10, 1.0], 1.0, {"C": 1.0}) + mat._set_sigma([1.0, 2.0], [[1.0, 1.0], [1.0, 1.0]]) # noqa: SLF001 + + +def test_warn_up_elastic_scatter(): + with pytest.warns(): + mat = MaterialMacroInfo([1000, 10, 1.0], 1.0, {"C": 1.0}) + mat._set_sigma([1.0, 2.0], [[0.5, 0.5], [1.0, 1.0]]) # noqa: SLF001 + + +def test_throw_index_error(): + layer_specific_const = LayerSpecificGroupwiseConstants( + lambda x: x, ["", ""], ["Dummy constants"] + ) + with pytest.raises(IndexError): + layer_specific_const[0, 0, 0] + with pytest.raises(IndexError): + layer_specific_const[0, 0, 0] = 1 + + +def test_iter_and_len(): + layer_specific_const = LayerSpecificGroupwiseConstants( + lambda x: x, ["", ""], ["Dummy constants"] + ) + assert len(layer_specific_const) == 2 + as_list = list(layer_specific_const) + assert len(as_list) == 2 + assert isinstance(as_list[0], AutoPopulatingDict) + + +def test_has_local_fluxes(): + """Test that the groupwise decorator has worked on the local fluxes methods.""" + assert hasattr(NeutronFluxProfile, "neutron_flux_at") + assert hasattr(NeutronFluxProfile, "groupwise_neutron_flux_at") + assert hasattr(NeutronFluxProfile, "neutron_flux_in_layer") + assert hasattr(NeutronFluxProfile, "groupwise_neutron_flux_in_layer") + + +def test_has_boundary_current(): + """Test that the groupwise decorator has worked on the boundary fluxes methods.""" + assert hasattr(NeutronFluxProfile, "groupwise_neutron_current_in_layer") + assert hasattr(NeutronFluxProfile, "neutron_current_in_layer") + assert hasattr(NeutronFluxProfile, "groupwise_neutron_current_through_interface") + assert hasattr(NeutronFluxProfile, "neutron_current_through_interface") + assert hasattr(NeutronFluxProfile, "groupwise_neutron_current_escaped") + assert hasattr(NeutronFluxProfile, "neutron_current_escaped") + + +def test_has_reaction_rates(): + """Test that the groupwise decorator has worked on the reactions methods.""" + assert hasattr(NeutronFluxProfile, "groupwise_integrated_flux_in_layer") + assert hasattr(NeutronFluxProfile, "integrated_flux_in_layer") + assert hasattr(NeutronFluxProfile, "groupwise_integrated_heating_in_layer") + assert hasattr(NeutronFluxProfile, "integrated_heating_in_layer") + assert hasattr( + NeutronFluxProfile, "groupwise_integrated_tritium_production_in_layer" + ) + assert hasattr(NeutronFluxProfile, "integrated_tritium_production_in_layer") + + +def test_has_volumetric_heating(): + assert hasattr(NeutronFluxProfile, "groupwise_neutron_heating_in_layer") + assert hasattr(NeutronFluxProfile, "neutron_heating_in_layer") + assert hasattr(NeutronFluxProfile, "groupwise_neutron_heating_at") + assert hasattr(NeutronFluxProfile, "neutron_heating_at") + + +def test_has_tritium_production(): + assert hasattr(NeutronFluxProfile, "groupwise_tritium_production_in_layer") + assert hasattr(NeutronFluxProfile, "tritium_production_in_layer") + assert hasattr(NeutronFluxProfile, "groupwise_tritium_production_at") + assert hasattr(NeutronFluxProfile, "tritium_production_at") + + +def test_has_plot(): + assert hasattr(NeutronFluxProfile, "plot") + + +def test_units(): + nfp = NeutronFluxProfile + assert nfp.get_output_unit(nfp.groupwise_integrated_flux_in_layer) == "m^-1 s^-1" + assert nfp.get_output_unit(nfp.groupwise_integrated_heating_in_layer) == "W m^-2" + assert ( + nfp.get_output_unit(nfp.groupwise_integrated_tritium_production_in_layer) + == "mole m^-2" + ) + assert nfp.get_output_unit(nfp.groupwise_linear_heating_density_in_layer) == "J m^-1" + assert nfp.get_output_unit(nfp.groupwise_neutron_current_at) == "m^-2 s^-1" + assert nfp.get_output_unit(nfp.groupwise_neutron_current_escaped) == "m^-2 s^-1" + assert nfp.get_output_unit(nfp.groupwise_neutron_current_in_layer) == "m^-2 s^-1" + assert ( + nfp.get_output_unit(nfp.groupwise_neutron_current_through_interface) + == "m^-2 s^-1" + ) + assert nfp.get_output_unit(nfp.groupwise_neutron_flux_at) == "m^-2 s^-1" + assert nfp.get_output_unit(nfp.groupwise_neutron_flux_in_layer) == "m^-2 s^-1" + assert nfp.get_output_unit(nfp.groupwise_neutron_heating_at) == "W m^-3" + assert nfp.get_output_unit(nfp.groupwise_neutron_heating_in_layer) == "W m^-3" + assert nfp.get_output_unit(nfp.groupwise_tritium_production_at) == "mole m^-3" + assert nfp.get_output_unit(nfp.groupwise_tritium_production_in_layer) == "mole m^-3" + + assert nfp.get_output_unit(nfp.integrated_flux_in_layer) == "m^-1 s^-1" + assert nfp.get_output_unit(nfp.integrated_heating_in_layer) == "W m^-2" + assert nfp.get_output_unit(nfp.integrated_tritium_production_in_layer) == "mole m^-2" + assert nfp.get_output_unit(nfp.neutron_current_at) == "m^-2 s^-1" + assert nfp.get_output_unit(nfp.neutron_current_escaped) == "m^-2 s^-1" + assert nfp.get_output_unit(nfp.neutron_current_in_layer) == "m^-2 s^-1" + assert nfp.get_output_unit(nfp.neutron_current_through_interface) == "m^-2 s^-1" + assert nfp.get_output_unit(nfp.neutron_flux_at) == "m^-2 s^-1" + assert nfp.get_output_unit(nfp.neutron_flux_in_layer) == "m^-2 s^-1" + assert nfp.get_output_unit(nfp.neutron_heating_in_layer) == "W m^-3" + assert nfp.get_output_unit(nfp.neutron_heating_at) == "W m^-3" + assert nfp.get_output_unit(nfp.tritium_production_in_layer) == "mole m^-3" + + +def test_get_sign_func(): + signs = _get_sign_of(np.array([-np.inf, -2, -1, -0.0, 0.0, 1.0, 2.0, np.inf])) + np.testing.assert_equal(signs, [-1, -1, -1, -1, 1, 1, 1, 1]) + + +def _diffusion_equation_in_layer(test_profile, n, num_layer, x): + """ + Get the three terms in the diffusion equation (equation 5 in the paper. + """ + diffusion_out = test_profile.materials[num_layer].diffusion_const[ + n + ] * test_profile._groupwise_flux_curvature_in_layer(n, num_layer, x) # noqa: SLF001 + total_removal = test_profile.materials[num_layer].sigma_t[ + n + ] * test_profile.groupwise_neutron_flux_in_layer(n, num_layer, x) + + source_in_terms = [] + in_matrix = ( + test_profile.materials[num_layer].sigma_s + + test_profile.materials[num_layer].sigma_in + ) + for g, all_sources_entering_from_g in enumerate(in_matrix[:, n]): + source_in_terms.append( + all_sources_entering_from_g + * test_profile.groupwise_neutron_flux_in_layer(g, num_layer, x) + ) + + return diffusion_out, total_removal, np.sum(source_in_terms) + + +def test_same_l_in_2_groups_warns(): + dummy = np.geomspace(MAX_E, MIN_E, 3) # dummy group structure + # translate from mean-free-path lengths (mfp) to macroscopic cross-sections + mfp_fw_s = 118 * 0.01 # [m] + mfp_fw_t = 16.65 * 0.01 # [m] + sigma_fw_t = 1 / mfp_fw_t # [1/m] + sigma_fw_s = 1 / mfp_fw_s # [1/m] + x_fw = 5.72 * 0.01 + fw_material = MaterialMacroInfo(dummy, 1.0, {"Te": 1.0}, name="fw") + fw_material._set_sigma( # noqa: SLF001 + [sigma_fw_t, sigma_fw_t], [[sigma_fw_s, sigma_fw_s], [0.0, sigma_fw_s]] + ) + incoming_flux = 100.0 + neutron_profile = NeutronFluxProfile(incoming_flux, [x_fw], [fw_material]) + with pytest.warns(UserWarning): + neutron_profile.solve() + + +@pytest.mark.filterwarnings( + "ignore:Group 0 and group 1 has the same neutron diffusion lengths" +) +@pytest.mark.filterwarnings("error") +@pytest.mark.xfail +def test_same_l_in_2_groups_calculate_flux(): + dummy = np.geomspace(MAX_E, MIN_E, 3) # dummy group structure + # translate from mean-free-path lengths (mfp) to macroscopic cross-sections + mfp_fw_s = 118 * 0.01 # [m] + mfp_fw_t = 16.65 * 0.01 # [m] + sigma_fw_t = 1 / mfp_fw_t # [1/m] + sigma_fw_s = 1 / mfp_fw_s # [1/m] + x_fw = 5.72 * 0.01 + fw_material = MaterialMacroInfo(dummy, 1.0, {"Te": 1.0}, name="fw") + fw_material._set_sigma( # noqa: SLF001 + [sigma_fw_t, sigma_fw_t], [[sigma_fw_s, sigma_fw_s], [0.0, sigma_fw_s]] + ) + incoming_flux = 100.0 + neutron_profile = NeutronFluxProfile(incoming_flux, [x_fw], [fw_material]) + + neutron_profile.solve() + assert np.isclose( + neutron_profile.groupwise_neutron_flux_in_layer( + 0, 0, neutron_profile.extended_boundary[0] + ), + 0, + ), "Extended boundary condition check for group 0" + assert np.isclose( + neutron_profile.groupwise_neutron_flux_in_layer( + 1, 0, neutron_profile.extended_boundary[1] + ), + 0, + ), "Extended boundary condition check for group 1" + num_layer = 0 + mid_point = np.mean(neutron_profile.interface_x[num_layer : num_layer + 2]) + for n in range(neutron_profile.n_groups): + diffusion_out, total_removal, source_in = _diffusion_equation_in_layer( + neutron_profile, n, 0, mid_point + ) + assert np.isclose(diffusion_out, total_removal - source_in), ( + "Check that the diffusion equation holds up at an arbitrary point." + ) + + +def test_two_group(): + dummy = np.geomspace(MAX_E, MIN_E, 3) # dummy group structure + # translate from mean-free-path lengths (mfp) to macroscopic cross-sections + mfp_fw_s = 118 * 0.01 # [m] + mfp_fw_t = 16.65 * 0.01 # [m] + sigma_fw_s = 1 / mfp_fw_s # [1/m] + x_fw = 5.72 * 0.1 + fw_material = MaterialMacroInfo(dummy, 1.0, {"Te": 1.0}, name="fw") + fw_material._set_sigma( # noqa: SLF001 + [1 / mfp_fw_t, 1 / (mfp_fw_t + 0.5)], + [[sigma_fw_s, sigma_fw_s], [0.0, sigma_fw_s]], + ) + incoming_flux = 100.0 + neutron_profile = NeutronFluxProfile(incoming_flux, [x_fw], [fw_material]) + neutron_profile.solve() + assert np.isclose( + neutron_profile.groupwise_neutron_flux_in_layer( + 0, 0, neutron_profile.extended_boundary[0] + ), + 0, + ), "Extended boundary condition check for group 0" + assert np.isclose( + neutron_profile.groupwise_neutron_flux_in_layer( + 1, 0, neutron_profile.extended_boundary[1] + ), + 0, + ), "Extended boundary condition check for group 1" + num_layer = 0 + mid_point = np.mean(neutron_profile.interface_x[num_layer : num_layer + 2]) + for n in range(neutron_profile.n_groups): + diffusion_out, total_removal, source_in = _diffusion_equation_in_layer( + neutron_profile, n, 0, mid_point + ) + assert np.isclose(diffusion_out, total_removal - source_in), ( + "Check that the diffusion equation holds up at an arbitrary point." + ) + removal_xs = [ + mat.sigma_t - mat.sigma_s.sum(axis=1) for mat in neutron_profile.materials + ] + assert np.isclose( + sum(neutron_profile.fluxes), + neutron_profile.neutron_current_escaped() + + sum( + sum( + removal_xs[num_layer][n] + * neutron_profile.groupwise_integrated_flux_in_layer(n, num_layer) + for n in range(neutron_profile.n_groups) + ) + for num_layer in range(neutron_profile.n_layers) + ), + ), "Conservation of neutrons" + assert np.isclose(neutron_profile.neutron_current_at(0), incoming_flux) + + +@pytest.mark.filterwarnings("ignore:Calculation of flux") +def test_three_group(): + dummy = np.geomspace(MAX_E, MIN_E, 4) # dummy group structure + # translate from mean-free-path lengths (mfp) to macroscopic cross-sections + mfp_fw_s = 118 * 0.01 # [m] + mfp_fw_t = 16.65 * 0.01 # [m] + sigma_fw_s = 1 / mfp_fw_s # [1/m] + x_fw = 5.72 * 0.1 + fw_material = MaterialMacroInfo(dummy, 1.0, {"Te": 1.0}, name="fw") + fw_material._set_sigma( # noqa: SLF001 + [1 / mfp_fw_t, 1 / (mfp_fw_t + 0.25), 1 / (mfp_fw_t + 0.5)], + [ + [sigma_fw_s, sigma_fw_s, sigma_fw_s], + [0, sigma_fw_s, sigma_fw_s], + [0, 0, sigma_fw_s], + ], + ) + incoming_flux = 100.0 + neutron_profile = NeutronFluxProfile(incoming_flux, [x_fw], [fw_material]) + neutron_profile.solve() + assert np.isclose( + neutron_profile.groupwise_neutron_flux_in_layer( + 0, 0, neutron_profile.extended_boundary[0] + ), + 0, + ), "Extended boundary condition check for group 0" + assert np.isclose( + neutron_profile.groupwise_neutron_flux_in_layer( + 1, 0, neutron_profile.extended_boundary[1] + ), + 0, + ), "Extended boundary condition check for group 1" + assert np.isclose( + neutron_profile.groupwise_neutron_flux_in_layer( + 2, 0, neutron_profile.extended_boundary[2] + ), + 0, + ), "Extended boundary condition check for group 2" + num_layer = 0 + mid_point = np.mean(neutron_profile.interface_x[num_layer : num_layer + 2]) + for n in range(neutron_profile.n_groups): + diffusion_out, total_removal, source_in = _diffusion_equation_in_layer( + neutron_profile, n, 0, mid_point + ) + assert np.isclose(diffusion_out, total_removal - source_in), ( + "Check that the diffusion equation holds up at an arbitrary point." + ) + removal_xs = [ + mat.sigma_t - mat.sigma_s.sum(axis=1) for mat in neutron_profile.materials + ] + assert np.isclose( + sum(neutron_profile.fluxes), + neutron_profile.neutron_current_escaped() + + sum( + sum( + removal_xs[num_layer][n] + * neutron_profile.groupwise_integrated_flux_in_layer(n, num_layer) + for n in range(neutron_profile.n_groups) + ) + for num_layer in range(neutron_profile.n_layers) + ), + ), "Conservation of neutrons" + assert np.isclose(neutron_profile.neutron_current_at(0), incoming_flux) + + +def test_four_group(): + dummy = np.geomspace(MAX_E, MIN_E, 5) # dummy group structure + # translate from mean-free-path lengths (mfp) to macroscopic cross-sections + mfp_fw_s = 118 * 0.01 # [m] + mfp_fw_t = 16.65 * 0.01 # [m] + + sigma_fw_s = 1 / mfp_fw_s # [1/m] + x_fw = 5.72 * 0.1 + fw_material = MaterialMacroInfo(dummy, 1.0, {"Te": 1.0}, name="fw") + fw_material._set_sigma( # noqa: SLF001 + [ + 1 / mfp_fw_t, + 1 / (mfp_fw_t + 0.25), + 1 / (mfp_fw_t + 0.5), + 1 / (mfp_fw_t + 0.75), + ], + [ + [sigma_fw_s / 4, sigma_fw_s / 4, sigma_fw_s, sigma_fw_s], + [0, sigma_fw_s / 3, sigma_fw_s / 3, sigma_fw_s], + [0, 0, sigma_fw_s / 3, sigma_fw_s], + [0, 0, 0, 0.3], + ], + # [ + # [0,0.001, 0.001, 0.001], + # [0,0.01,0, 0], + # [0,0,0,0], + # [0,0,0,0], + # ], + ) + incoming_flux = 100.0 + neutron_profile = NeutronFluxProfile(incoming_flux, [x_fw], [fw_material]) + neutron_profile.solve() + assert np.isclose( + neutron_profile.groupwise_neutron_flux_in_layer( + 0, 0, neutron_profile.extended_boundary[0] + ), + 0, + ), "Extended boundary condition check for group 0" + assert np.isclose( + neutron_profile.groupwise_neutron_flux_in_layer( + 1, 0, neutron_profile.extended_boundary[1] + ), + 0, + ), "Extended boundary condition check for group 1" + assert np.isclose( + neutron_profile.groupwise_neutron_flux_in_layer( + 2, 0, neutron_profile.extended_boundary[2] + ), + 0, + ), "Extended boundary condition check for group 2" + assert np.isclose( + neutron_profile.groupwise_neutron_flux_in_layer( + 3, 0, neutron_profile.extended_boundary[3] + ), + 0, + ), "Extended boundary condition check for group 3" + + num_layer = 0 + mid_point = np.mean(neutron_profile.interface_x[num_layer : num_layer + 2]) + for n in range(neutron_profile.n_groups): + diffusion_out, total_removal, source_in = _diffusion_equation_in_layer( + neutron_profile, n, 0, mid_point + ) + assert np.isclose(diffusion_out, total_removal - source_in), ( + "Check that the diffusion equation holds up at an arbitrary point." + ) + assert np.isclose(neutron_profile.neutron_current_at(0), incoming_flux) + removal_xs = [ + mat.sigma_t - mat.sigma_s.sum(axis=1) - mat.sigma_in.sum(axis=1) + for mat in neutron_profile.materials + ] + assert np.isclose( + sum(neutron_profile.fluxes), + neutron_profile.neutron_current_escaped() + + sum( + sum( + removal_xs[num_layer][n] + * neutron_profile.groupwise_integrated_flux_in_layer(n, num_layer) + for n in range(neutron_profile.n_groups) + ) + for num_layer in range(neutron_profile.n_layers) + ), + ), "Conservation of neutrons" diff --git a/tests/unit/test_neutronics_data.py b/tests/unit/test_neutronics_data.py new file mode 100644 index 000000000..20de71fc9 --- /dev/null +++ b/tests/unit/test_neutronics_data.py @@ -0,0 +1,88 @@ +import numpy as np +import pytest +from numpy import typing as npt + +from process.models.neutronics.data import nXn_weight_matrix, scattering_weight_matrix + +rng = np.random.default_rng(1) + + +@pytest.mark.parametrize( + "group_structure, atomic_mass", + [ + (np.cumsum(100 ** abs(rng.normal(5)))[::-1], rng.random() * 200), + (np.cumsum(100 ** abs(rng.normal(10)))[::-1], rng.random() * 239), + (np.cumsum(100 ** abs(rng.normal(20)))[::-1], rng.random() * 150), + (np.cumsum(100 ** abs(rng.normal(30)))[::-1], 1), + (np.cumsum(100 ** abs(rng.normal(40)))[::-1], 2), + ], +) +def test_scattering_matrix(group_structure: npt.NDArray[np.float64], atomic_mass: float): + """ + Check that even randomly generated scattering matrix are normalized + and non-negative. + """ + scattering_matrix = scattering_weight_matrix(group_structure, atomic_mass) + row_sum = scattering_matrix.sum(axis=1) + assert np.logical_or(np.isclose(row_sum, 1, rtol=0), row_sum < 1).all(), ( + "Every row must be unitary or less." + ) + assert (scattering_matrix >= 0).all(), "Must be all non-negative." + + +def test_n2n_matrix_neg1(): + fibo_gs = np.array([8, 5, 3, 2, 1]) + n2n_matrix = nXn_weight_matrix(fibo_gs, -1, 2) + row_sum = n2n_matrix.sum(axis=1) + np.testing.assert_allclose( + row_sum, + [1, 1, 1, np.log(2 / 1.5) / np.log(2 / 1)], + err_msg="Expected bin 4 to be partially out-of-bounds", + ) + assert (n2n_matrix >= 0).all(), "Must be all non-negative." + + +def test_n2n_matrix_neg2(): + fibo_gs = np.array([8, 5, 3, 2, 1]) + n2n_matrix = nXn_weight_matrix(fibo_gs, -2, 2) + row_sum = n2n_matrix.sum(axis=1) + np.testing.assert_allclose( + row_sum, [1, 1, 1, 0], err_msg="Expected bin 4 to be out-of-bounds" + ) + assert (n2n_matrix >= 0).all(), "Must be all non-negative." + + +def test_n2n_matrix_neg3(): + fibo_gs = np.array([8, 5, 3, 2, 1]) + n2n_matrix = nXn_weight_matrix(fibo_gs, -3, 2) + row_sum = n2n_matrix.sum(axis=1) + np.testing.assert_allclose( + row_sum, + [1, 1, np.log(3 / 2.5) / np.log(3 / 2), 0], + err_msg="Expected bin 3 to be partially out-of-bounds", + ) + assert (n2n_matrix >= 0).all(), "Must be all non-negative." + + +def test_n2n_matrix_pos3(): + fibo_gs = np.array([8, 5, 3, 2, 1]) + n2n_matrix = nXn_weight_matrix(fibo_gs, 3, 2) + row_sum = n2n_matrix.sum(axis=1) + np.testing.assert_allclose( + row_sum, + [np.log(6.5 / 5) / np.log(8 / 5), 1, 1, 1], + err_msg="Expected bin 1 to be partially out-of-bounds", + ) + assert (n2n_matrix >= 0).all(), "Must be all non-negative." + + +def test_n2n_matrix_pos6(): + fibo_gs = np.array([8, 5, 3, 2, 1]) + n2n_matrix = nXn_weight_matrix(fibo_gs, 6, 2) + row_sum = n2n_matrix.sum(axis=1) + np.testing.assert_allclose( + row_sum, + [0, 1, 1, 1], + err_msg="Expected bin 1 to be completely out-of-bounds", + ) + assert (n2n_matrix >= 0).all(), "Must be all non-negative."