RadCluster
Graph-based cluster dynamics for irradiated materials
Loading...
Searching...
No Matches
defect_production.py
Go to the documentation of this file.
1"""
2defect_production.py — Cascade defect production fractions for bcc Fe / EUROFER97.
3
4Implements the power-law cluster number distributions for SIA and vacancy
5clusters produced in displacement cascades, for fission and fusion spectra.
6
7Physics reference
8-----------------
9Ghoniem, N.M. (2026), Sections 1-2 (Rate_Equations.pdf):
10 Eqs. 1-13, Tables 2 and 5.
11
12Production rates [atom frac / s]
13---------------------------------
14For SIA clusters (i = 2..i_cascade):
15 P_i^SIA = η · G · ε_i^SIA (Eq. 12)
16 ε_i^SIA = C_i · i^{−s_i} (Eq. 7)
17 C_i = f_i^cl / Σ_{i=2}^{i_cascade} i^{1−s_i} (Eq. 9)
18
19For vacancy clusters (v = 2..v_cascade):
20 P_v^VAC = η · G · ε_v^VAC (Eq. 13)
21 ε_v^VAC = C_v · v^{−s_v} (Eq. 8)
22 C_v = f_v^cl / Σ_{v=2}^{v_cascade} v^{1−s_v} (Eq. 10)
23
24Point-defect production (monomer):
25 P_1^SIA = η · G · (1 − Σ_{m≥2} m·ε_m^SIA) (Eq. 11 — surviving free SIAs)
26 P_1^VAC = η · G · (1 − Σ_{m≥2} m·ε_m^VAC) (Eq. 11 — surviving free vacancies)
27
28The monomer term absorbs the EXACT remaining cascade mass over the
29actually-used (possibly truncated) ε array. When the size grid is not
30truncated (I_max ≥ i_cascade) the sum equals f_i^cl by construction, so this
31reduces to the textbook P_1 = η·G·(1 − f_i^cl); when I_max < i_cascade it
32guarantees exact atom conservation Σ_{m≥1} m·P_m = η·G.
33"""
34
35import numpy as np
36
37
38# ── Cascade parameters from Rate_Equations.pdf Tables 2 and 5 ────────────────
39
40# Fission neutron spectrum (typical PWR/FFTF, average PKA ~10-20 keV)
41FISSION = {
42 'label': 'Fission',
43 'eta': 0.30, # survival efficiency η (Table 2)
44 'f_cl_i': 0.58, # SIA clustering fraction (Table 2)
45 's_i': 1.6, # SIA power-law exponent (Table 2, Eq. 7)
46 'i_cascade': 20, # max SIA cluster size from cascade (Table 2)
47 'f_cl_v': 0.15, # vacancy clustering fraction (Table 2)
48 's_v': 2.5, # vacancy power-law exponent (Table 2, Eq. 8)
49 'v_cascade': 10, # max vacancy cluster size from cascade (Table 2)
50 'b0_res': 0.01, # re-solution parameter b_0 [dpa^-1/He] (Table 29)
51 # NOTE: G_He_r (He production rate) is NOT stored here. The single
52 # authoritative source is the Excel Reactions-sheet 'G_He_r', loaded into
53 # InputData.derived['G_He_r']; production_rates() takes it as an explicit
54 # argument. (Stage-2 fix: eliminated the duplicate hard-coded value.)
55}
56
57# Fusion neutron spectrum (14 MeV, higher PKA energies)
58FUSION = {
59 'label': 'Fusion',
60 'eta': 0.28, # survival efficiency η (Table 2)
61 'f_cl_i': 0.65, # SIA clustering fraction (Table 2)
62 's_i': 1.5, # SIA power-law exponent (Table 2, Eq. 7)
63 'i_cascade': 50, # max SIA cluster size from cascade (Table 2)
64 'f_cl_v': 0.20, # vacancy clustering fraction (Table 2)
65 's_v': 2.3, # vacancy power-law exponent (Table 2, Eq. 8)
66 'v_cascade': 20, # max vacancy cluster size from cascade (Table 2)
67 'b0_res': 0.10, # re-solution parameter b_0 [dpa^-1/He] (Table 29)
68 # NOTE: G_He_r is NOT stored here — see the corresponding note in FISSION.
69}
70
71# Backward-compat aliases for old key names
72_KEY_ALIASES = {'m1': 'i_cascade', 'n1': 'v_cascade'}
73
74
75def normalisation_constant(f_cl, s, m_max):
76 """
77 Compute the normalisation constant C such that:
78 Σ_{m=2}^{m_max} m · C · m^{−s} = f_cl
79
80 C = f_cl / Σ_{m=2}^{m_max} m^{1−s} (Eqs. 9-10)
81
82 Parameters
83 ----------
84 f_cl : float — clustering fraction
85 s : float — power-law exponent
86 m_max : int — maximum cluster size
87
88 Returns
89 -------
90 C : float
91 """
92 ms = np.arange(2, m_max + 1, dtype=float)
93 denom = np.sum(ms**(1.0 - s))
94 return f_cl / denom if denom > 0 else 0.0
95
96
97def compute_epsilon(f_cl, s, m_max):
98 """
99 Compute production fractions ε_m = C · m^{−s} for m = 2..m_max.
100
101 The fraction of surviving defects in clusters of size m is m·ε_m,
102 summing to f_cl over all cluster sizes.
103
104 Parameters
105 ----------
106 f_cl : float — total clustering fraction
107 s : float — power-law exponent
108 m_max : int — maximum cluster size
109
110 Returns
111 -------
112 epsilon : ndarray [m_max+1]
113 epsilon[0] = 0 (size 0 unused)
114 epsilon[1] = 0 (monomer handled separately)
115 epsilon[m] = C · m^{−s} for m = 2..m_max
116
117 Notes
118 -----
119 Array length is m_max+1 so that epsilon[m] = ε_m (1-indexed access).
120 """
121 C = normalisation_constant(f_cl, s, m_max)
122 eps = np.zeros(m_max + 1)
123 for m in range(2, m_max + 1):
124 eps[m] = C * m**(-s)
125 return eps
126
127
128def _spec_get(spec, key):
129 """Get a value from a spectrum dict, supporting old key aliases."""
130 if key in spec:
131 return spec[key]
132 alias = _KEY_ALIASES.get(key)
133 if alias and alias in spec:
134 return spec[alias]
135 # Try reverse alias
136 for old, new in _KEY_ALIASES.items():
137 if new == key and old in spec:
138 return spec[old]
139 raise KeyError(key)
140
141
142def _merge_spec(spec, over):
143 """Module-default cascade spec overlaid with workbook values.
144
145 ``over`` is ``InputData.production_fission`` / ``production_fusion`` — the
146 Production sheet as loaded from Excel. Only keys that are present AND
147 numerically valid override the module default, so a blank cell or a text
148 entry (e.g. 'user', '~10') falls back to the table value rather than
149 poisoning the spectrum with NaN.
150
151 Before this existed, ``production_rates`` read the module dicts directly and
152 the Production sheet was never consulted: eta / f_cl_i / f_cl_v / s_i / s_v /
153 i_cascade / v_cascade were silently un-settable from the workbook.
154 """
155 if not over:
156 return spec
157 merged = dict(spec)
158 for k in ('eta', 'f_cl_i', 's_i', 'i_cascade',
159 'f_cl_v', 's_v', 'v_cascade', 'b0_res'):
160 if k not in over:
161 continue
162 try:
163 v = float(over[k])
164 except (TypeError, ValueError):
165 continue
166 if not np.isfinite(v):
167 continue
168 merged[k] = int(v) if k in ('i_cascade', 'v_cascade') else v
169 return merged
170
171
172def production_rates(G, spectrum, I_max, V_max, G_He_r, spec_over=None):
173 """
174 Compute cascade production rates P_i^SIA [s^-1] and P_v^VAC [s^-1]
175 scaled by displacement rate G [dpa/s].
176
177 Returns arrays indexed from 1:
178 Pr_SIA[i] = η·G·ε_i^SIA for i=2..i_cascade; P_1 absorbs exact remainder
179 Pr_VAC[v] = η·G·ε_v^VAC for v=2..v_cascade; P_1 absorbs exact remainder
180
181 Parameters
182 ----------
183 G : float [dpa/s]
184 spectrum : str 'fission' | 'fusion'
185 I_max : int — maximum SIA cluster size tracked
186 V_max : int — maximum vacancy cluster size tracked
187 G_He_r : float [appm He/dpa]
188 He production rate. SINGLE AUTHORITATIVE SOURCE — supplied by the
189 caller from the Excel Reactions-sheet value loaded into
190 InputData.derived['G_He_r']. No hard-coded fallback lives here.
191
192 Returns
193 -------
194 Pr_SIA : ndarray [I_max+1] (index 0 unused; Pr_SIA[1]=monomer)
195 Pr_VAC : ndarray [V_max+1] (index 0 unused; Pr_VAC[1]=monomer)
196 G_He : float [atom frac/s] — He transmutation production rate
197 """
198 spec = FISSION if 'fiss' in spectrum.lower() else FUSION
199 spec = _merge_spec(spec, spec_over)
200
201 eta = spec['eta']
202 f_cl_i = spec['f_cl_i']
203 s_i = spec['s_i']
204 i_cascade = _spec_get(spec, 'i_cascade')
205 f_cl_v = spec['f_cl_v']
206 s_v = spec['s_v']
207 v_cascade = _spec_get(spec, 'v_cascade')
208
209 # SIA clusters (Eq. 12)
210 i_top = min(i_cascade, I_max)
211 eps_i = compute_epsilon(f_cl_i, s_i, i_top)
212 Pr_SIA = np.zeros(I_max + 1)
213 for i in range(2, i_top + 1):
214 Pr_SIA[i] = eta * G * eps_i[i] # cluster (Eq. 12)
215 # Monomer (Eq. 11) — absorb the EXACT remaining cascade mass over the
216 # actually-used (possibly truncated) eps_i array, so that
217 # Σ_{m≥1} m·P_m = η·G holds exactly even when i_cascade > I_max.
218 clustered_i = sum(m * eps_i[m] for m in range(2, i_top + 1))
219 Pr_SIA[1] = eta * G * (1.0 - clustered_i)
220
221 # Vacancy clusters (Eq. 13)
222 v_top = min(v_cascade, V_max)
223 eps_v = compute_epsilon(f_cl_v, s_v, v_top)
224 Pr_VAC = np.zeros(V_max + 1)
225 for v in range(2, v_top + 1):
226 Pr_VAC[v] = eta * G * eps_v[v] # cluster (Eq. 13)
227 # Monomer (Eq. 11) — same exact-remainder treatment as the SIA monomer.
228 clustered_v = sum(m * eps_v[m] for m in range(2, v_top + 1))
229 Pr_VAC[1] = eta * G * (1.0 - clustered_v)
230
231 # He transmutation production (Eq. 5)
232 # G_He_r in appm He/dpa → [atom frac/s]: appm * 1e-6 * G
233 G_He = G_He_r * 1.0e-6 * G
234
235 return Pr_SIA, Pr_VAC, G_He
normalisation_constant(f_cl, s, m_max)