RadCluster
Graph-based cluster dynamics for irradiated materials
Loading...
Searching...
No Matches
input_data.py
Go to the documentation of this file.
1"""
2input_data.py — InputData class for RadCluster_2_1.
3
4Reads material, irradiation, and model parameters from a 5-sheet Excel
5workbook (radcluster_code/input/input_parameters.xlsx) and computes
6all derived quantities needed by the ODE system.
7
8Sheet layout
9------------
10Production — cascade production parameters (fission vs. fusion)
11Energetics — lattice constants, migration/formation energies, He EOS
12Diffusion — diffusion pre-factors, EUROFER solute trapping
13Dissociation — binding energy parameters, trap mutation barriers
14Reactions — rate constant prefactors, sink parameters, solver settings
15
16Physics reference
17-----------------
18Ghoniem, N.M. (2026), "A Cluster Dynamics Model for Radiation Damage
19Evolution in Ferritic-Martensitic Steels" (Rate_Equations.pdf).
20
21Naming convention
22-----------------
23I — max SIA cluster size
24V — max vacancy cluster size
25i_mobile — max mobile SIA cluster size
26v_mobile — max mobile vacancy cluster size
27i_discrete — max discrete SIA size (individually tracked; default = i_mobile)
28v_discrete — max discrete vacancy size (individually tracked; default = v_mobile)
29I_bin — number of SIA bin-moment equations beyond i_discrete
30V_bin — number of VAC bin-moment equations beyond v_discrete
31i_cascade — max SIA cluster size from cascade
32v_cascade — max vacancy cluster size from cascade
33
34All units: SI (lengths in m, concentrations dimensionless as atom fractions,
35energies in eV, time in s).
36"""
37
38import warnings
39import numpy as np
40import pandas as pd
41from pathlib import Path
42
43_kB = 8.617333262e-5 # Boltzmann constant [eV K^-1]
44
45
47 """Module-level callable for the SIA 1D-glide diffusivity D_n^{1D}(n).
48
49 Defined at module scope (rather than as a closure inside
50 InputData._calculate_derived) so that an InputData object containing a
51 reference to it can be pickled — local closures are not picklable.
52 """
53 __slots__ = ('D1D_base', 's_1D', 'trap_loop')
54
55 def __init__(self, D1D_base, s_1D, trap_loop):
56 self.D1D_base = D1D_base
57 self.s_1D = s_1D
58 self.trap_loop = trap_loop
59
60 def __call__(self, n):
61 return self.D1D_base / float(n) ** self.s_1D / (1.0 + self.trap_loop)
62
63BASE_DIR = Path(__file__).parent.parent
64INPUT_FILE = BASE_DIR / 'input' / 'input_parameters.xlsx'
65
66# Valid options
67_SOLVER_MODES = ('full_system', 'active_window')
68
69# Backward-compat aliases for the previous solver-mode names. Old configs,
70# Excel files, and saved output dirs still reference the legacy strings; this
71# map silently translates them to the new names with no warning, since the
72# physics is identical.
73_SOLVER_MODE_ALIASES = {
74 'cpp_full': 'full_system',
75 'sliding_OpenMP': 'active_window',
76}
77_PHYSICS_OPTIONS = ('full_CD_fission', 'full_CD_fusion',
78 'bin_moment_CD_fission', 'bin_moment_CD_fusion')
79
80# Two-axis decomposition of physics_option.
81_EQUATIONS = ('discrete', 'bin_moment')
82_CASCADES = ('fission', 'fusion')
83
84# Legacy equations-axis aliases (silently accepted). The internal
85# physics_option strings still use the historical "full_CD" token; only the
86# user-facing equations selector has been renamed.
87_EQUATIONS_ALIASES = {
88 'full_CD': 'discrete',
89}
90
91
92def make_physics_option(equations, cascade):
93 """Combine an (equations, cascade) pair into the canonical physics_option string.
94
95 equations : 'discrete' | 'bin_moment' (legacy 'full_CD' also accepted)
96 cascade : 'fission' | 'fusion'
97 """
98 equations = _EQUATIONS_ALIASES.get(equations, equations)
99 if equations not in _EQUATIONS:
100 raise ValueError(f"equations must be one of {_EQUATIONS}, got '{equations}'")
101 if cascade not in _CASCADES:
102 raise ValueError(f"cascade must be one of {_CASCADES}, got '{cascade}'")
103 if equations == 'bin_moment':
104 return f'bin_moment_CD_{cascade}'
105 return f'full_CD_{cascade}'
106
107
108def split_physics_option(po):
109 """Reverse of make_physics_option: return (equations, cascade).
110
111 The 'full_CD_*' physics_option prefix maps back to the user-facing
112 equations name 'discrete'.
113 """
114 if po.startswith('bin_moment_CD_'):
115 return ('bin_moment', po[len('bin_moment_CD_'):])
116 if po.startswith('full_CD_'):
117 return ('discrete', po[len('full_CD_'):])
118 raise ValueError(f"Cannot split physics_option='{po}'")
119_SHAPE_FUNCTIONS = ('constant', 'linear', 'lognormal')
120
121# Backward-compat key aliases (old → new)
122# NOTE: n1_bin is NOT aliased to i_discrete — different semantics.
123# n1_bin was the starting bin edge; i_discrete is the number of discrete sizes.
124_KEY_ALIASES = {
125 'N': 'I',
126 'M': 'V',
127 'n_max_i': 'i_mobile',
128 'm_max_v': 'v_mobile',
129 'm1': 'i_cascade',
130 'n1': 'v_cascade',
131}
132
133
135 """
136 Material, irradiation, and model parameters for RadCluster_2_1.
137
138 Reads from the 5-sheet Excel workbook. Call display_parameters() to
139 inspect all loaded values.
140
141 Parameters
142 ----------
143 excel_file : path-like, optional
144 I : int, optional — override max SIA cluster size
145 V : int, optional — override max vacancy cluster size
146 solver_mode : str, optional — override solver mode
147 physics_option : str, optional — override physics option
148 """
149
150 def __init__(self, excel_file=INPUT_FILE, I=None, V=None,
151 solver_mode=None, physics_option=None,
152 # Backward-compat kwargs
153 N=None, M=None):
154 # Support old N/M kwargs
155 if I is None and N is not None:
156 I = N
157 if V is None and M is not None:
158 V = M
159
160 self.excel_file = Path(excel_file)
161 if not self.excel_file.is_file():
162 raise FileNotFoundError(
163 f"Excel file not found: {self.excel_file}\n"
164 f"Run radcluster_code/create_excel.py to generate it."
165 )
166 print(f"Loading parameters from: {self.excel_file.resolve()}")
167 self._load_data()
168
169 # Apply caller overrides
170 if I is not None:
171 self.reactions['I'] = int(I)
172 if V is not None:
173 self.reactions['V'] = int(V)
174 if solver_mode is not None:
175 self.reactions['solver_mode'] = str(solver_mode)
176 if physics_option is not None:
177 self.reactions['physics_option'] = str(physics_option)
178
179 self._calculate_derived()
180 self._validate()
181
182 # ── Sheet loading ─────────────────────────────────────────────────────────
183
184 @staticmethod
186 """Convert Symbol/Value sheet to dict. Skips header rows (no symbol)."""
187 d = {}
188 sym_col = None
189 val_col = None
190 for c in df.columns:
191 cl = str(c).lower()
192 if 'symbol' in cl:
193 sym_col = c
194 if 'value' in cl or 'fission' in cl:
195 val_col = c
196 if sym_col is None:
197 sym_col = df.columns[1]
198 if val_col is None:
199 val_col = df.columns[2]
200 for _, row in df.iterrows():
201 k = row.get(sym_col, None)
202 v = row.get(val_col, None)
203 if pd.notna(k) and str(k).strip() and str(k).strip() != 'nan':
204 d[str(k).strip()] = v
205 return d
206
207 @staticmethod
209 """Read Production sheet — returns fission and fusion sub-dicts."""
210 fis, fus = {}, {}
211 for _, row in df.iterrows():
212 sym = row.get('Symbol', None)
213 if not pd.notna(sym) or not str(sym).strip():
214 continue
215 k = str(sym).strip()
216 f_val = row.get('Fission', None)
217 u_val = row.get('Fusion', None)
218 if pd.notna(f_val):
219 fis[k] = f_val
220 if pd.notna(u_val):
221 fus[k] = u_val
222 return fis, fus
223
224 @staticmethod
226 """Translate old key names to new convention (in-place)."""
227 for old, new in _KEY_ALIASES.items():
228 if old in d and new not in d:
229 d[new] = d.pop(old)
230
231 def _load_data(self):
232 """Read all five Excel worksheets."""
233 try:
234 prod_df = pd.read_excel(self.excel_file, sheet_name='Production')
235 ener_df = pd.read_excel(self.excel_file, sheet_name='Energetics')
236 diff_df = pd.read_excel(self.excel_file, sheet_name='Diffusion')
237 diss_df = pd.read_excel(self.excel_file, sheet_name='Dissociation')
238 reac_df = pd.read_excel(self.excel_file, sheet_name='Reactions')
239 except Exception as exc:
240 raise RuntimeError(f"Failed to read Excel file: {exc}") from exc
241
243 self._production_to_dict(prod_df)
244
245 self.energetics = self._sheet_to_dict(ener_df)
246 self.diffusion = self._sheet_to_dict(diff_df)
247 self.dissociation= self._sheet_to_dict(diss_df)
248 self.reactions = self._sheet_to_dict(reac_df)
249
250 # Apply backward-compat aliases (old Excel → new names)
251 for d in (self.reactions, self.diffusion, self.production_fission,
252 self.production_fusion):
253 self._apply_aliases(d)
254
255 # Cast integer fields
256 _int_keys = ('I', 'V', 'L_He_max', 'n_points', 'log_time',
257 'i_discrete', 'v_discrete', 'I_bin', 'V_bin',
258 'n_moments', 'n_group', 'window_width',
259 'v_mobile', 'i_mobile',
260 'i_cascade', 'v_cascade')
261 for k in _int_keys:
262 for d in (self.reactions, self.diffusion, self.production_fission,
263 self.production_fusion):
264 if k in d:
265 try:
266 d[k] = int(float(d[k]))
267 except (TypeError, ValueError):
268 pass
269
270 print("Successfully loaded all five parameter sheets.")
271
272 # ── Derived quantities ────────────────────────────────────────────────────
273
275 """Compute all derived physics quantities."""
276 e = self.energetics
277 d = self.diffusion
278 re = self.reactions
279
280 # Temperature and irradiation conditions
281 T = float(re.get('T', 600.0))
282 G = float(re.get('G', 1.0e-6))
283 kBT = _kB * T
284
285 # Lattice
286 a_nm = float(e.get('a', 0.2867))
287 a_m = a_nm * 1.0e-9 # nm → m
288 Omega = float(e.get('Omega', 1.18e-29))
289 r0 = (3.0 * Omega / (4.0 * np.pi)) ** (1.0 / 3.0)
290 b_111 = float(e.get('b_111', 0.2482)) * 1.0e-9 # nm → m
291
292 # Energetics
293 E_f_v = float(e.get('E_f_v', 2.0))
294 E_m_v = float(e.get('E_m_v', 0.67))
295 E_m_i = float(e.get('E_m_i', 0.34))
296 E_m_h = float(e.get('E_m_h', 0.06))
297 E_s_He = float(e.get('E_s_He', 2.35))
298 gamma_s = float(e.get('gamma_s', 2.0)) # J/m^2
299
300 # Attempt frequencies [s^-1]
301 nu_v = float(e.get('nu_v', 1.0e13))
302 nu_i = float(e.get('nu_i', 1.0e13))
303 nu_h = float(e.get('nu_h', 3.0e12))
304
305 # Pure Fe diffusivities [m^2/s] — Eq. 17
306 Dv_Fe = a_m**2 * nu_v * np.exp(-E_m_v / kBT)
307 Di_Fe = a_m**2 * nu_i * np.exp(-E_m_i / kBT)
308 Dh_Fe = a_m**2 * nu_h * np.exp(-E_m_h / kBT)
309
310 # Jump frequencies [s^-1] — Eq. 22-24
311 omega_v_Fe = Dv_Fe / a_m**2
312 omega_i_Fe = Di_Fe / a_m**2
313 omega_h_Fe = Dh_Fe / a_m**2
314
315 # EUROFER solute trapping (Eq. 42, 48)
316 c_Cr = float(d.get('c_Cr', 0.094))
317 c_W = float(d.get('c_W', 0.0033))
318 c_Mn = float(d.get('c_Mn', 0.0047))
319 c_C = float(d.get('c_C', 5.0e-4))
320 c_N = float(d.get('c_N', 2.0e-4))
321
322 # SIA trapping (Eq. 42)
323 def _trap_sum_SIA():
324 # z_s · c_s · exp(E_b^{s,i} / kBT)
325 E_b_C_SIA = float(d.get('E_b_C_SIA', 0.45)); z_C_SIA = 4
326 E_b_N_SIA = float(d.get('E_b_N_SIA', 0.40)); z_N_SIA = 4
327 E_b_Cr_SIA = float(d.get('E_b_Cr_SIA', 0.10)); z_Cr_SIA = 8
328 E_b_Mn_SIA = float(d.get('E_b_Mn_SIA', 0.20)); z_Mn_SIA = 6
329 return (z_C_SIA * c_C * np.exp(E_b_C_SIA / kBT) +
330 z_N_SIA * c_N * np.exp(E_b_N_SIA / kBT) +
331 z_Cr_SIA * c_Cr * np.exp(E_b_Cr_SIA / kBT) +
332 z_Mn_SIA * c_Mn * np.exp(E_b_Mn_SIA / kBT))
333
334 # Vacancy trapping (Eq. 48)
335 def _trap_sum_VAC():
336 E_b_C_V = float(d.get('E_b_C_V', 0.45)); z_C_V = 3
337 E_b_N_V = float(d.get('E_b_N_V', 0.40)); z_N_V = 3
338 E_b_W_V = float(d.get('E_b_W_V', 0.27)); z_W_V = 8
339 E_b_Mn_V = float(d.get('E_b_Mn_V', 0.10)); z_Mn_V = 8
340 E_b_Cr_V = float(d.get('E_b_Cr_V', 0.05)); z_Cr_V = 8
341 return (z_C_V * c_C * np.exp(E_b_C_V / kBT) +
342 z_N_V * c_N * np.exp(E_b_N_V / kBT) +
343 z_W_V * c_W * np.exp(E_b_W_V / kBT) +
344 z_Mn_V * c_Mn * np.exp(E_b_Mn_V / kBT) +
345 z_Cr_V * c_Cr * np.exp(E_b_Cr_V / kBT))
346
347 # SIA cluster loop trapping (Eq. 52)
348 def _trap_sum_loop():
349 E_b_C_loop = float(d.get('E_b_C_loop', 0.50)); z_C_loop = 2
350 E_b_N_loop = float(d.get('E_b_N_loop', 0.40)); z_N_loop = 2
351 E_b_Cr_loop = float(d.get('E_b_Cr_loop', 0.10)); z_Cr_loop = 4
352 return (z_C_loop * c_C * np.exp(E_b_C_loop / kBT) +
353 z_N_loop * c_N * np.exp(E_b_N_loop / kBT) +
354 z_Cr_loop * c_Cr * np.exp(E_b_Cr_loop / kBT))
355
356 trap_SIA = _trap_sum_SIA()
357 trap_VAC = _trap_sum_VAC()
358 trap_loop = _trap_sum_loop()
359
360 # Effective diffusivities and jump frequencies (Eqs. 42, 48, 52)
361 omega_i_eff = omega_i_Fe / (1.0 + trap_SIA)
362 omega_v_eff = omega_v_Fe / (1.0 + trap_VAC)
363 omega_h_eff = omega_h_Fe # He not trapped by solutes
364
365 Di_eff = omega_i_eff * a_m**2
366 Dv_eff = omega_v_eff * a_m**2
367 Dh_eff = omega_h_eff * a_m**2
368
369 # SIA cluster 1D glide (Eq. 33)
370 nu0_1D = float(d.get('nu0_1D', 6.0e12))
371 E_m_1D = float(d.get('E_m_1D', 0.03))
372 s_1D = float(d.get('s_1D', 0.7))
373 i_mobile = int(float(d.get('i_mobile', 1)))
374 v_mobile = int(float(d.get('v_mobile', 1)))
375
376 # Boundary flux option for coalescence at upper size limit
377 # 'absorption' (default): product lost at boundary (open boundary)
378 # 'reflection': product folded back into largest tracked size (closed)
379 boundary_flux = str(re.get('boundary_flux',
380 d.get('boundary_flux', 'absorption'))).lower().strip()
381 if boundary_flux not in ('absorption', 'reflection'):
382 import warnings
383 warnings.warn(f"Unknown boundary_flux='{boundary_flux}', using 'absorption'")
384 boundary_flux = 'absorption'
385
386 # D_n^{1D}(n) = (3a²ν_0^{1D}) / (2n^{s_1D}) · exp(−E_m^{1D}/k_BT) (Eq. 33)
387 D1D_base = (3.0 * a_m**2 * nu0_1D / 2.0) * np.exp(-E_m_1D / kBT)
388
389 # Loop trapping correction for 1D glide (Eq. 52).
390 # Module-level callable (picklable); same call signature D1D(n).
391 D1D = _D1DFunction(D1D_base, s_1D, trap_loop)
392
393 # Mean free path for 1D/3D mixed (Eq. 121)
394 L_hat = float(d.get('L_hat', 50.0)) # L/a (dimensionless)
395 B_rot = float(d.get('B_rot', 2.627))
396
397 # Equilibrium vacancy concentration
398 Cv_eq = np.exp(-E_f_v / kBT)
399
400 # Geometric rate constant prefactors (Eq. 128)
401 A_sph = (48.0 * np.pi**2)**(1.0/3.0) # ≈ 7.818
402 A_loop = 8.0 * np.sqrt(np.pi / np.sqrt(3.0)) # ≈ 10.78
403 A_1D = 9.0 / (8.0 * np.pi**(2.0/3.0)) # ≈ 2.632
404
405 # He production.
406 # G_He_r (He production rate, appm He/dpa) is read from the Excel
407 # Reactions sheet — this is the SINGLE AUTHORITATIVE SOURCE. It is
408 # stored in derived['G_He_r'] and forwarded to
409 # defect_production.production_rates(); there is no hard-coded
410 # duplicate in the FISSION/FUSION spectrum dicts (Stage-2 fix).
411 # The spectrum-appropriate defaults below (0.75 fission / 10.0 fusion)
412 # apply only when the Excel value is absent entirely.
413 spectrum = str(re.get('spectrum', 'fission')).lower()
414 _G_He_r_default = 0.75 if 'fiss' in spectrum else 10.0 # appm He/dpa
415 G_He_r = float(re.get('G_He_r', _G_He_r_default))
416 G_He = G_He_r * 1.0e-6 * G # appm/dpa * dpa/s → atom frac/s
417
418 self.derived = {
419 'T': T,
420 'G': G,
421 'kBT': kBT,
422 'a_m': a_m,
423 'Omega': Omega,
424 'r0': r0,
425 'b_111': b_111,
426 'E_f_v': E_f_v,
427 'E_m_v': E_m_v,
428 'E_m_i': E_m_i,
429 'E_m_h': E_m_h,
430 'E_s_He': E_s_He,
431 'gamma_s': gamma_s,
432 'nu_v': nu_v,
433 'nu_i': nu_i,
434 'nu_h': nu_h,
435 'Di_Fe': Di_Fe,
436 'Dv_Fe': Dv_Fe,
437 'Dh_Fe': Dh_Fe,
438 'Di_eff': Di_eff,
439 'Dv_eff': Dv_eff,
440 'Dh_eff': Dh_eff,
441 'omega_i_eff': omega_i_eff,
442 'omega_v_eff': omega_v_eff,
443 'omega_h_eff': omega_h_eff,
444 'trap_SIA': trap_SIA,
445 'trap_VAC': trap_VAC,
446 'trap_loop': trap_loop,
447 'D1D_base': D1D_base,
448 'D1D': D1D,
449 's_1D': s_1D,
450 'i_mobile': i_mobile,
451 'v_mobile': v_mobile,
452 'L_hat': L_hat,
453 'B_rot': B_rot,
454 'Cv_eq': Cv_eq,
455 'A_sph': A_sph,
456 'A_loop': A_loop,
457 'A_1D': A_1D,
458 'G_He': G_He,
459 'G_He_r': G_He_r,
460 'spectrum': spectrum,
461 'boundary_flux': boundary_flux,
462 }
463
464 print(f"Derived: T={T} K Cv_eq={Cv_eq:.3e}"
465 f" Di_eff={Di_eff:.3e} Dv_eff={Dv_eff:.3e} m2/s"
466 f" spectrum='{spectrum}'")
467
468 # ── Validation ────────────────────────────────────────────────────────────
469
470 def _validate(self):
471 T = self.derived['T']
472 G = self.derived['G']
473 rho = float(self.reactions.get('rho_d', 1.0e14))
474
475 if not (300 <= T <= 1200):
476 warnings.warn(f"Temperature {T} K is outside typical range [300–1200 K]")
477 if not (1e-9 <= G <= 1e-3):
478 warnings.warn(f"Dose rate {G} dpa/s is outside [1e-9–1e-3]")
479 if not (1e12 <= rho <= 1e16):
480 warnings.warn(f"Dislocation density {rho} m^-2 outside typical range")
481
482 sm = self.solver_mode
483 if sm in _SOLVER_MODE_ALIASES:
484 self.reactions['solver_mode'] = _SOLVER_MODE_ALIASES[sm]
485 sm = self.reactions['solver_mode']
486 if sm not in _SOLVER_MODES:
487 warnings.warn(f"Unknown solver_mode='{sm}'. Using 'full_system'.")
488 self.reactions['solver_mode'] = 'full_system'
489
491 if po not in _PHYSICS_OPTIONS:
492 warnings.warn(f"Unknown physics_option='{po}'. Using 'full_CD_fission'.")
493 self.reactions['physics_option'] = 'full_CD_fission'
494
495 sf = self.shape_function
496 if sf not in _SHAPE_FUNCTIONS:
497 warnings.warn(f"Unknown shape_function='{sf}'. Using 'linear'.")
498 self.reactions['shape_function'] = 'linear'
499
500 # ── Convenience properties ────────────────────────────────────────────────
501
502 @property
503 def I(self):
504 """Max SIA cluster size."""
505 return int(float(self.reactions.get('I', 500)))
506
507 @property
508 def V(self):
509 """Max vacancy cluster size."""
510 return int(float(self.reactions.get('V', 500)))
511
512 # Backward-compat aliases
513 @property
514 def N(self):
515 return self.I
516
517 @N.setter
518 def N(self, val):
519 self.reactions['I'] = int(val)
520
521 @property
522 def M(self):
523 return self.V
524
525 @M.setter
526 def M(self, val):
527 self.reactions['V'] = int(val)
528
529 @property
530 def i_mobile(self):
531 """Max mobile SIA cluster size."""
532 return self.derived.get('i_mobile', 1)
533
534 @property
535 def v_mobile(self):
536 """Max mobile vacancy cluster size."""
537 return self.derived.get('v_mobile', 1)
538
539 @property
540 def i_discrete(self):
541 """Max discrete SIA size (individually tracked). Defaults to i_mobile."""
542 val = self.reactions.get('i_discrete', None)
543 if val is not None:
544 return int(float(val))
545 return self.i_mobile
546
547 @property
548 def v_discrete(self):
549 """Max discrete vacancy size (individually tracked). Defaults to v_mobile."""
550 val = self.reactions.get('v_discrete', None)
551 if val is not None:
552 return int(float(val))
553 return self.v_mobile
554
555 @property
556 def I_bin(self):
557 """Number of SIA bin-moment equations beyond i_discrete.
558 Defaults to ceil(log(I/i_discrete)/log(2)) if not specified."""
559 val = self.reactions.get('I_bin', None)
560 if val is not None:
561 return int(float(val))
562 # Default: auto-compute from r=2.0
563 id = self.i_discrete
564 if id >= self.I:
565 return 0
566 return int(np.ceil(np.log(self.I / max(id, 1)) / np.log(2.0)))
567
568 @property
569 def V_bin(self):
570 """Number of VAC bin-moment equations beyond v_discrete.
571 Defaults to ceil(log(V/v_discrete)/log(2)) if not specified."""
572 val = self.reactions.get('V_bin', None)
573 if val is not None:
574 return int(float(val))
575 # Default: auto-compute from r=2.0
576 vd = self.v_discrete
577 if vd >= self.V:
578 return 0
579 return int(np.ceil(np.log(self.V / max(vd, 1)) / np.log(2.0)))
580
581 @property
582 def L_He_max(self):
583 val = self.reactions.get('L_He_max', 'mf')
584 if str(val).lower() in ('mf', 'mean-field', 'nan', 'none', ''):
585 return None # signal to use mean-field reduction
586 return int(float(val))
587
588 @property
589 def solver_mode(self):
590 sm = str(self.reactions.get('solver_mode', 'full_system')).strip()
591 return _SOLVER_MODE_ALIASES.get(sm, sm)
592
593 @property
594 def physics_option(self):
595 return str(self.reactions.get('physics_option', 'full_CD_fission')).strip()
596
597 @property
598 def equations(self):
599 """Equation system: 'full_CD' or 'bin_moment' (derived from physics_option)."""
600 return split_physics_option(self.physics_optionphysics_option)[0]
601
602 @property
603 def cascade(self):
604 """Cascade type: 'fission' or 'fusion' (derived from physics_option)."""
605 return split_physics_option(self.physics_optionphysics_option)[1]
606
607 @property
608 def alpha_He(self):
609 return float(self.reactions.get('alpha_He', 1.7))
610
611 @property
612 def shape_function(self):
613 """Intra-bin shape function: 'constant', 'linear', or 'lognormal'."""
614 return str(self.reactions.get('shape_function', 'linear')).strip().lower()
615
616 # ── Display ───────────────────────────────────────────────────────────────
617
619 sections = [
620 ('ENERGETICS', self.energetics),
621 ('DIFFUSION', self.diffusion),
622 ('REACTIONS', self.reactions),
623 ('DERIVED', {k: v for k, v in self.derived.items()
624 if not callable(v)}),
625 ]
626 for title, d in sections:
627 print(f"\n{'='*60}\n{title}\n{'='*60}")
628 for k, v in d.items():
629 fmt = f" {k}: {v:.4e}" if isinstance(v, float) else f" {k}: {v}"
630 print(fmt)
__init__(self, D1D_base, s_1D, trap_loop)
Definition input_data.py:55
__init__(self, excel_file=INPUT_FILE, I=None, V=None, solver_mode=None, physics_option=None, N=None, M=None)