RadCluster
Graph-based cluster dynamics for irradiated materials
Loading...
Searching...
No Matches
parameters.h
Go to the documentation of this file.
1
42#pragma once
43
44#include <cmath>
45#include <fstream>
46#include <iostream>
47#include <map>
48#include <sstream>
49#include <string>
50#include <vector>
51
52struct Parameters {
53 // ── Cluster size limits ─────────────────────────────────────────────────
54 int I, V, N_eq;
55 int Ni_max; // = I (kept for legacy compatibility)
56
57 // ── Physics and solver mode ─────────────────────────────────────────────
58 // physics_option: 0=full_CD_fission, 1=full_CD_fusion,
59 // 2=bin_moment_CD_fission, 3=bin_moment_CD_fusion
60 // he_mode: 0=Case 2 (decoupled/fission), 1=Case 1 (mean-field/fusion)
61 // he_kinetics: 0=dynamic (ODE), 1=quasi_steady_state (algebraic c_h)
64 int he_kinetics; // 0=dynamic, 1=quasi_steady_state
65
66 // ── Geometric rate constant prefactors (Eq. 128) ─────────────────────────
67 double A_sph; // (48π²)^{1/3} ≈ 7.818
68 double A_loop; // 8√(π/√3) ≈ 10.78
69 double B_rot; // (4/π)(8π/3)^{1/3} ≈ 2.627
70 double L_hat; // mean free path L/a (dimensionless)
71
72 // ── Solute trapping sums (Eq. 42, 48, 52) ────────────────────────────────
73 double trap_SIA; // Σ z_s·c_s·exp(E_b^{s,i}/kBT)
74 double trap_VAC; // Σ z_s·c_s·exp(E_b^{s,v}/kBT)
75 double trap_loop; // Σ z_s·c_s·exp(E_b^{s,loop}/kBT)
76
77 // ── Mobility cutoffs ──────────────────────────────────────────────────────
78 int i_mobile; // max mobile SIA cluster size (1D glide cutoff)
79 int v_mobile; // max mobile vacancy cluster size
80
81 // ── Boundary flux option ─────────────────────────────────────────────────
82 // 0 = absorption (open boundary): product at I+1 or V+1 is lost
83 // 1 = reflection (closed boundary): product folded back into I or V
85
86 // ── Vacancy cluster arrays [V] ────────────────────────────────────────────
87 std::vector<double> KVV; // Cv capture by void m (K_VAC_grow)
88 std::vector<double> KVI; // Ci annihilation at void m (K_VAC_shrink)
89 std::vector<double> GVV; // thermal vacancy emission from void m
90 std::vector<double> KHeV; // He capture by void m
91 std::vector<double> Pr_VAC; // cascade vacancy production rate [V]
92 std::vector<double> m13; // m^{1/3} factors [V]
93
94 // ── SIA cluster arrays [I] ────────────────────────────────────────────────
95 std::vector<double> KII; // Ci capture by SIA loop n (K_SIA_grow)
96 std::vector<double> KIV; // Cv capture by SIA loop n (K_SIA_shrink)
97 std::vector<double> GII; // thermal SIA emission from loop n
98 std::vector<double> k2_SIA; // total sink rate for SIA cluster n
99 std::vector<double> Pr_SIA; // cascade SIA production rate [I]
100
101 // ── 1D glide prefactors (Eq. 141) ─────────────────────────────────────────
102 // K_1D_eff(n, m) = K_1D_pref[n-1] · m^{1/3} / (1 + B_rot·L̂²·m^{-1/3})
103 std::vector<double> K_1D_pref; // [I]: A_sph · ω_n^{1D} / Ω
104
105 // Legacy separable cross-term (for backward compatibility with solver.cpp)
106 std::vector<double> K_IclV_ns; // [I]: 4π·r0·Di·n^{-2/3} / Ω
107 std::vector<double> K_IclV_ni; // [I]: 4π·r0·Di / (n·Ω)
108
109 // ── Mobile cluster effective 3D diffusivities (for coalescence) ─────────
110 std::vector<double> D_SIA_eff; // [I]: effective 3D D for SIA cluster n
111 std::vector<double> D_VAC_eff; // [V]: effective 3D D for vac cluster m
112 // ½⟨111⟩ loop coarsening (LOOP_COAL). D_SIA_eff is truncated to 0 above
113 // i_mobile, so a loop that grows past the cutoff can never coalesce again
114 // and the mean size pins AT the cutoff. D_loop_coal continues the same
115 // glide law past it and is used ONLY by the loop–loop coalescence edge.
116 int loop_coal = 0; // 0 = off (legacy), 1 = on
117 int sia_emission_monomer = 0; // 1 = SIA emission returns its monomer to I_1 (v2.1.0); 0 = before v2.1.0
118 std::vector<double> D_loop_coal; // [I]: glide D with no mobility cutoff
119 double A_sph_inv_O23; // A_sph / Ω^{2/3} [m^-2]
120 double A_loop_inv_O23; // A_loop / Ω^{2/3} [m^-2] (loop geometry for n≥4)
121 double Z_i_loop; // SIA dislocation bias at loops (Table 26, Eq. P3_i)
122 // Per-size loop bias Z_i^loop(n) (Wolfer). Empty => use the scalar above,
123 // which reproduces every pre-2026-08-31 run bit-for-bit.
124 std::vector<double> Z_i_loop_arr;
125 double Z_ii; // SIA-SIA coalescence bias factor (elastic interaction)
126
127 // ── Scalar physics ────────────────────────────────────────────────────────
128 double G_He; // He transmutation rate [at.frac/s]
129 double k2_disl_v; // vacancy fixed sink [s^-1]
130 double k2_disl_i; // SIA fixed sink [s^-1]
131 double k2_disl_He; // He fixed sink [s^-1]
132 double Cv_eq; // thermal vacancy equilibrium concentration
133 double K_iv; // V–SIA recombination prefactor (P1, mutual diffusivity)
134 double K_3D_cav_pref; // 3D cavity absorption prefactor: A_sph · Di_eff / Ω^{2/3}
135
136 // ── He parameters ─────────────────────────────────────────────────────────
137 double beta_He; // He emission: ν_h·exp(−(E_b_hV+E_m_h)/kBT) [s^-1]
138 double delta_He; // He pressure coefficient [eV]
139 double beta_He_exp; // He pressure power-law exponent
140 double kBT; // k_B·T [eV]
141
142 // ── Bin-moment parameters (Chapter 9) ────────────────────────────────────
143 int I_bin; // number of SIA logarithmic bins
144 int V_bin; // number of vacancy logarithmic bins
145 double r_ratio; // bin ratio r > 1 (Eq. 188) — diagnostic only;
146 // bin edges are transmitted explicitly (see below)
147 int i_discrete; // max discrete SIA size (minimum cluster size in binned region)
148 int v_discrete; // max discrete vacancy size (default = v_mobile)
149 int shape_function; // intra-bin closure: 0=constant, 1=linear, 2=lognormal
150 int n_mom; // moments per bin: 1 (constant), 2 (linear), 3 (lognormal)
151 // DERIVED as shape_function + 1 (not read independently)
152
153 // Explicit integer bin edges transmitted from Python (cpp_bridge.py).
154 // Bin k covers integer sizes [bin_lo[k], bin_hi[k]-1]. These are the
155 // exact partition Python computed; re-deriving them in C++ from r_ratio
156 // via std::floor can diverge from numpy.floor over many bins.
157 std::vector<int> sia_bin_lo; // [I_bin]
158 std::vector<int> sia_bin_hi; // [I_bin]
159 std::vector<int> vac_bin_lo; // [V_bin]
160 std::vector<int> vac_bin_hi; // [V_bin]
161
162 // ── Initial conditions ─────────────────────────────────────────────────────
163 // ── ½⟨111⟩ → ⟨100⟩ loop conversion (optional; default OFF) ───────────────
164 // When ON, an appended c_i100 block of length I is kept at the END of the
165 // state vector (after the conservation ODEs), so all c_v / Q_tot / c_h /
166 // conservation offsets are unchanged and the OFF path is byte-identical.
167 int loop_conversion = 0; // 0 = off (no SIA100 block), 1 = on
168 int sia100_off = 0; // state offset of the appended c_i100 block
169 int sia100_len = 0; // length of the appended ⟨100⟩ block:
170 // discrete modes -> I (full per-size)
171 // bin_moment modes-> i_discrete + n_mom*I_bin
172 // (same reduction as ½⟨111⟩)
173 int cons_off = 0; // start of the 5 conservation ODEs (stable
174 // under the SIA100 append; = N_eq-5 when off)
175 int conv_n_loop_min = 4; // ⟨100⟩ loop-onset size (bulk-100 n_min)
176 int conv_n_j_min = 30; // junction minimum size
177 double conv_phi_max = 0.5; // junction peak yield at n = n'
178 double conv_sigma_s = 0.35; // junction log-size tolerance
179 double conv_psuccess = 1.0; // Marian two-step success prob P_success(T)
180 double conv_psuccess_abs = 1.0; // absorption-only success gate
181 // A_abs·[1+exp((ΔH₂^abs−ΔH_rev)/kT)]^-1;
182 // decoupled from conv_psuccess (junction)
183 double conv_absorb_boost = 1.0; // scales K_100_absorb ONLY (1-D-glide
184 // enhanced capture); 1.0 = legacy
185 // (gates junction + absorption; Fig. 3)
186 std::vector<double> Gamma_uni; // [I] unary ½⟨111⟩→⟨100⟩ conversion rate
187 std::vector<double> K_100_grow; // [I] ⟨100⟩ + I_1 capture (sessile)
188 std::vector<double> K_100_shrink; // [I] ⟨100⟩ + V_1 capture (sessile)
189 std::vector<double> G_100; // [I] ⟨100⟩ thermal SIA emission
190 // Loop → network-dislocation loss on the sessile ⟨100⟩ block
191 // (loop_network_loss.tex). Diagonal first-order sink Λ_n^net applied to the
192 // appended ⟨100⟩ loops AND routed into the J_SIA_fixed ledger so δ_FP is
193 // preserved. Zeros when LOOP_NETWORK_LOSS is off (byte-identical OFF path).
194 // The ½⟨111⟩ side is already folded into k2_SIA on the Python side.
195 std::vector<double> Lambda_net_100; // [I]
196 // Cavity → network-dislocation SWEEPING (VOID_NETWORK_LOSS). Diagonal
197 // first-order sink Λ_m^void on the VACANCY block: a climbing/gliding line
198 // that intersects a cavity absorbs it whole, so the cavity's m vacancies
199 // are routed into the J_VAC_fixed ledger and δ_FP is preserved. Λ ∝ d_cav
200 // ∝ m^(1/3), so large cavities are removed preferentially and the upper
201 // tail is truncated — the only size-dependent negative feedback on cavity
202 // growth in the model. Zeros when VOID_NETWORK_LOSS is off, so the OFF
203 // path is byte-identical.
204 std::vector<double> Lambda_net_void; // [V]
205
206 std::vector<double> y0; // [N_eq]
207
208 // ── Concentration floor ────────────────────────────────────────────────────
209 double C_floor;
210
211 // ── Solver settings ────────────────────────────────────────────────────────
212 double t_begin;
213 double t_end;
216 double rtol;
217 double atol;
218
219 // ── Integration method ─────────────────────────────────────────────────────
220 // CVODE BDF is the only integrator wired up; the linear solver is selectable.
221 int linsol; // 0=dense, 1=band, 2=gmres, 3=klu (sparse direct, full_CD only)
222 int mu, ml; // band solver bandwidths
223 int max_order; // 0 = solver default
224 double hmin; // minimum step size (0 = no limit)
225 double hmax; // maximum step size (0 = no limit)
226
227 // ── Dynamic window (active_window) ────────────────────────────────────────
228 // window_mode: 0=full_system, 4=active_window (two independent sliding windows + OpenMP-parallel RHS)
229 // Two independent windows: one for SIA cluster indices, one for VAC cluster indices.
231 int window_width; // initial window width (shared by SIA and VAC)
232 double concentration_threshold; // window expansion threshold (shared by SIA and VAC)
233 int window_pad; // SIA window expansion pad
234 int window_pad_v; // VAC window expansion pad (default = window_pad)
243
244 // ── Preconditioner storage ───────────────────────────────────────────────
245 // Jacobi diagonal (used when prec_type==0)
246 std::vector<double> prec_diag;
247
248 // Woodbury bordered-banded preconditioner (used when prec_type==1)
249 //
250 // The Jacobian has the structure J = T + U·V^T where:
251 // T is banded (half-bandwidth prec_bw)
252 // U is N_eq × prec_rank (dense columns from mobile species)
253 // V = [e_{j1}, ..., e_{jr}] (selector for mobile indices)
254 //
255 // The preconditioner solves (I - γJ)x = r via SMW:
256 // M = T̂ - γ U V^T, T̂ = I - γT (banded)
257 // M^{-1} = T̂^{-1} + T̂^{-1} U S^{-1} V^T T̂^{-1}
258 // S = -I/γ_scale + V^T T̂^{-1} U (r × r Schur complement)
259 int prec_type; // 0=Jacobi (legacy), 1=Woodbury (default when coalescence)
260 int prec_bw; // half-bandwidth of T (auto: max(2*i_mobile, 2*v_mobile) + 1)
261 int prec_rank; // rank of dense border (auto: i_mobile + v_mobile)
262
263 // Storage (allocated in prec_setup):
264 // prec_band: banded LU of T̂ in LAPACK dgbtrf layout
265 // rows: 2*prec_bw + prec_bw + 1 = 3*prec_bw + 1
266 // cols: N_eq
267 // prec_Tinv_U: T̂^{-1} U [N_eq × prec_rank]
268 // prec_schur: S factored [prec_rank × prec_rank]
269 // prec_ipiv_band: pivot array for dgbtrf [N_eq]
270 // prec_ipiv_schur:pivot array for dgetrf [prec_rank]
271 // prec_mobile_idx:indices of mobile species in state vector [prec_rank]
272 // prec_f0: base RHS evaluation [N_eq]
273 std::vector<double> prec_band;
274 std::vector<double> prec_Tinv_U;
275 std::vector<double> prec_schur;
276 std::vector<int> prec_ipiv_band;
277 std::vector<int> prec_ipiv_schur;
278 std::vector<int> prec_mobile_idx;
279 std::vector<double> prec_f0;
280 std::vector<double> prec_work; // scratch [N_eq]
281 std::vector<double> prec_y_save; // saved y during FD probing [N_eq]
282 std::vector<double> prec_deltas; // FD perturbation sizes [N_eq]
283 std::vector<double> prec_f_pert; // perturbed RHS [N_eq]
284 double prec_gamma; // cached γ from last setup
285
286 // ── KLU sparse direct solver (linsol == 3) ───────────────────────────────
287 // CSC sparsity pattern of the Jacobian (built once at startup). Values
288 // are filled by sparse_fd_jac() via colored finite differences.
289 // jac_col_ptr [N_eq + 1]: CSC column pointers
290 // jac_row_idx [nnz]: CSC row indices, sorted within each column
291 // jac_colors [N_eq]: CPR color of each column
292 // jac_n_colors: number of color groups
293 // jac_color_groups [N_eq]: columns flattened by color
294 // jac_color_offsets[n_colors+1]: offsets into jac_color_groups
295 std::vector<int> jac_col_ptr;
296 std::vector<int> jac_row_idx;
297 std::vector<int> jac_colors;
298 std::vector<int> jac_color_groups;
299 std::vector<int> jac_color_offsets;
301
302 // ── Size-only lookup tables (y-INDEPENDENT; built once by
303 // build_kernel_tables() in rate_kernels.cpp) ──────────────────────────
304 // The bin-moment RHS sweeps the reconstructed per-size spectra c_n[I] and
305 // c_v[V] on EVERY call, and the Woodbury preconditioner issues
306 // (2*prec_bw+1) + prec_rank RHS calls per Jacobian rebuild. The capture
307 // kernels K_ii / K_vv / K_vi / K_1D_eff and the Marian junction yield
308 // depend only on the INTEGER cluster sizes, so recomputing their
309 // cbrt/sqrt/pow/log/exp per call costs O(I+V) transcendentals per RHS for
310 // values that never change. These tables hoist that work to startup.
311 //
312 // Every table entry is produced by the SAME std:: call the kernel used
313 // before, and the kernel expressions are otherwise untouched, so results
314 // are bit-identical to the pre-table solver.
315 // size_tab_max == 0 ⇒ tables unbuilt; accessors fall back to std::.
316 int size_tab_max = 0; // tables valid for sizes 1..size_tab_max
317 std::vector<double> cbrt_tab; // [size_tab_max] cbrt(s), index s-1
318 std::vector<double> sqrt_tab; // [size_tab_max] sqrt(s), index s-1
319 std::vector<double> pow23_tab; // [size_tab_max] pow(s, 2/3), index s-1
320 std::vector<double> xi_tab; // [size_tab_max] cbrt(3s/8π), index s-1
321 // Marian junction yield fused with the SIA coalescence kernel:
322 // phiK_junc_tab[(np-1)*I + (npp-1)] = conv_phi_junc(np,npp)·K_ii(npp,np)
323 // Only projectiles np = 1..i_mobile ever appear, so the table is
324 // i_mobile × I. Empty when loop_conversion is off.
325 std::vector<double> phiK_junc_tab;
326 int phiK_junc_stride = 0; // == I when the table is populated
327 // Per-bin Σn and Σn² over the bin's integer sizes — the linear
328 // (hat-function) reconstruction's normal-equation moments. Depend only
329 // on the bin edges, but were recomputed per RHS call at O(I) cost.
330 std::vector<double> bin_S1, bin_S2; // [I_bin]
331 std::vector<double> vbin_S1, vbin_S2; // [V_bin]
332
333 // ── bin_moment RHS scratch ────────────────────────────────────────────────
334 // rhs_bin_moment used to heap-allocate ~5 vectors of length I or V on every
335 // call (≈4 MB of malloc/free per RHS at I=V=1e5). These persist across
336 // calls; the RHS refills them. Safe to share: CVODE calls the RHS serially,
337 // and the OpenMP regions inside it are joined before the next call.
338 std::vector<double> bm_c_n, bm_dc_n, bm_c_v, bm_dc_v, bm_GVV_eff;
339 std::vector<double> bm_c_n100, bm_d111, bm_d100;
341
342 // ── Diagnostics ────────────────────────────────────────────────────────────
343 bool verbose; // if false, suppress per-timestep [diag] / [ci5_rates] output
344};
345
346// ── CLI / file parsing helpers ───────────────────────────────────────────────
347
348inline double require_param(const std::map<std::string, double>& m,
349 const std::string& key) {
350 auto it = m.find(key);
351 if (it == m.end()) {
352 std::cerr << "Missing required parameter: " << key << "\n";
353 exit(1);
354 }
355 return it->second;
356}
357
358inline double optional_param(const std::map<std::string, double>& m,
359 const std::string& key, double def) {
360 auto it = m.find(key);
361 return (it != m.end()) ? it->second : def;
362}
363
368inline std::map<std::string, double> parse_param_file(const std::string& path) {
369 std::map<std::string, double> props;
370 std::ifstream f(path);
371 if (!f.is_open()) {
372 std::cerr << "Cannot open parameter file: " << path << "\n";
373 exit(1);
374 }
375 std::string line;
376 int lineno = 0;
377 while (std::getline(f, line)) {
378 ++lineno;
379 if (line.empty() || line[0] == '#') continue;
380 auto pos = line.find('=');
381 if (pos == std::string::npos) continue;
382 std::string key = line.substr(0, pos);
383 double val = 0.0;
384 try {
385 val = std::stod(line.substr(pos + 1));
386 } catch (...) {
387 std::cerr << "Line " << lineno << ": bad value for \"" << key << "\"\n";
388 exit(1);
389 }
390 props[key] = val;
391 }
392 return props;
393}
394
398inline Parameters build_parameters(const std::map<std::string, double>& p) {
399 Parameters P{};
400
401 // Cluster sizes — support old (Ni/Nv, N/M) and new (I/V) key names
402 P.I = static_cast<int>(optional_param(p, "I",
403 optional_param(p, "N",
404 optional_param(p, "Ni", -1))));
405 if (P.I < 0) { std::cerr << "Missing required parameter: I (or N or Ni)\n"; exit(1); }
406 P.V = static_cast<int>(optional_param(p, "V",
407 optional_param(p, "M",
408 optional_param(p, "Nv", -1))));
409 if (P.V < 0) { std::cerr << "Missing required parameter: V (or M or Nv)\n"; exit(1); }
410
411 P.Ni_max = static_cast<int>(optional_param(p, "Ni_max", static_cast<double>(P.I)));
412
413 // Physics mode
414 P.physics_option = static_cast<int>(optional_param(p, "physics_option_int", 0));
415 P.he_mode = static_cast<int>(optional_param(p, "he_mode", 0));
416 P.he_kinetics = static_cast<int>(optional_param(p, "qss_He", 0));
417 // he_kinetics: 0=dynamic (c_h is ODE state), 1=quasi_steady_state (c_h algebraic)
418
419 // he_mode duplicates information already encoded in physics_option:
420 // 0=full_CD_fission(Case2), 1=full_CD_fusion(Case1),
421 // 2=bin_moment_fission(Case2), 3=bin_moment_fusion(Case1)
422 // so the He-reduction case is physics_option % 2. Fail loudly on mismatch
423 // rather than silently running the wrong He physics.
424 if (P.he_mode != (P.physics_option % 2)) {
425 std::cerr << "Inconsistent contract: he_mode=" << P.he_mode
426 << " but physics_option=" << P.physics_option
427 << " requires he_mode=" << (P.physics_option % 2) << "\n";
428 exit(1);
429 }
430
431 // State vector size depends on he_mode and he_kinetics:
432 // Case 2 dynamic: I + V + 2
433 // Case 2 quasi_steady_state: I + V + 1 (c_h removed from state)
434 // Case 1 dynamic: I + 2V + 1
435 // Case 1 quasi_steady_state: I + 2V (c_h removed from state)
436 const bool qss = (P.he_kinetics == 1);
437 int n_he_extra;
438 if (P.he_mode == 1)
439 n_he_extra = qss ? P.V : P.V + 1;
440 else
441 n_he_extra = qss ? 1 : 2;
442 P.N_eq = P.I + P.V + n_he_extra + 5; // +5 for conservation accounting ODEs
443
444 // Geometric prefactors
445 P.A_sph = optional_param(p, "A_sph", 7.818);
446 P.A_loop = optional_param(p, "A_loop", 10.78);
447 P.B_rot = optional_param(p, "B_rot", 2.627);
448 P.L_hat = optional_param(p, "L_hat", 50.0);
449
450 // Solute trapping
451 P.trap_SIA = optional_param(p, "trap_SIA", 0.0);
452 P.trap_VAC = optional_param(p, "trap_VAC", 0.0);
453 P.trap_loop = optional_param(p, "trap_loop", 0.0);
454
455 // Mobility cutoffs — support both old and new key names
456 P.i_mobile = static_cast<int>(optional_param(p, "i_mobile",
457 optional_param(p, "n_max_i", 100.0)));
458 P.v_mobile = static_cast<int>(optional_param(p, "v_mobile",
459 optional_param(p, "m_max_v", 5.0)));
460
461 // Boundary flux: 0=absorption (default), 1=reflection
462 P.boundary_flux = static_cast<int>(optional_param(p, "boundary_flux", 0.0));
463
464 // Resize arrays
465 P.KVV.resize(P.V); P.KVI.resize(P.V); P.GVV.resize(P.V);
466 P.KHeV.resize(P.V); P.Pr_VAC.resize(P.V); P.m13.resize(P.V);
467 P.KII.resize(P.I); P.KIV.resize(P.I); P.GII.resize(P.I);
468 P.k2_SIA.resize(P.I); P.Pr_SIA.resize(P.I);
469 P.K_1D_pref.resize(P.I);
470 P.K_IclV_ns.resize(P.I); P.K_IclV_ni.resize(P.I);
471 P.D_SIA_eff.resize(P.I); P.D_VAC_eff.resize(P.V);
472 P.y0.resize(P.N_eq);
473
474 // Vacancy arrays
475 for (int k = 0; k < P.V; ++k) {
476 P.KVV[k] = require_param(p, "KVV_" + std::to_string(k));
477 P.KVI[k] = require_param(p, "KVI_" + std::to_string(k));
478 P.GVV[k] = require_param(p, "GVV_" + std::to_string(k));
479 P.KHeV[k] = require_param(p, "KHeV_" + std::to_string(k));
480 P.Pr_VAC[k] = require_param(p, "Pr_VAC_" + std::to_string(k));
481 P.m13[k] = require_param(p, "m13_" + std::to_string(k));
482 }
483
484 // SIA arrays
485 for (int k = 0; k < P.I; ++k) {
486 P.KII[k] = require_param(p, "KII_" + std::to_string(k));
487 P.KIV[k] = require_param(p, "KIV_" + std::to_string(k));
488 P.GII[k] = require_param(p, "GII_" + std::to_string(k));
489 P.k2_SIA[k] = require_param(p, "k2_SIA_" + std::to_string(k));
490 P.Pr_SIA[k] = require_param(p, "Pr_SIA_" + std::to_string(k));
491 P.K_1D_pref[k] = optional_param(p, "K_1D_pref_" + std::to_string(k), 0.0);
492 P.K_IclV_ns[k] = optional_param(p, "K_IclV_ns_" + std::to_string(k), 0.0);
493 P.K_IclV_ni[k] = optional_param(p, "K_IclV_ni_" + std::to_string(k), 0.0);
494 }
495
496 // Mobile cluster diffusivities (for coalescence)
497 for (int k = 0; k < P.I; ++k)
498 P.D_SIA_eff[k] = optional_param(p, "D_SIA_eff_" + std::to_string(k), 0.0);
499 for (int k = 0; k < P.V; ++k)
500 P.D_VAC_eff[k] = optional_param(p, "D_VAC_eff_" + std::to_string(k), 0.0);
501 P.loop_coal = static_cast<int>(optional_param(p, "loop_coal", 0.0));
502 P.sia_emission_monomer = static_cast<int>(optional_param(p, "sia_emission_monomer", 0.0));
503 P.D_loop_coal.assign(P.I, 0.0);
504 if (P.loop_coal)
505 for (int k = 0; k < P.I; ++k)
506 P.D_loop_coal[k] = optional_param(
507 p, "D_loop_coal_" + std::to_string(k), 0.0);
508 P.A_sph_inv_O23 = optional_param(p, "A_sph_inv_O23", 0.0);
509 P.A_loop_inv_O23 = optional_param(p, "A_loop_inv_O23", 0.0);
510 P.Z_i_loop = optional_param(p, "Z_i_loop", 1.05);
511 // Size-dependent bias array, only if the bridge shipped it.
512 {
513 P.Z_i_loop_arr.clear();
514 if (p.count("Z_i_loop_arr_0")) {
515 P.Z_i_loop_arr.assign(P.I, P.Z_i_loop);
516 for (int k = 0; k < P.I; ++k)
517 P.Z_i_loop_arr[k] = optional_param(
518 p, "Z_i_loop_arr_" + std::to_string(k), P.Z_i_loop);
519 }
520 }
521 P.Z_ii = optional_param(p, "Z_ii", 1.0);
522
523 // Scalar physics
524 P.G_He = require_param(p, "G_He");
525 P.k2_disl_v = require_param(p, "k2_disl_v");
526 P.k2_disl_i = require_param(p, "k2_disl_i");
527 P.k2_disl_He = require_param(p, "k2_disl_He");
528 P.Cv_eq = require_param(p, "Cv_eq");
529 // K_iv (recombination) and K_3D_cav_pref (cavity absorption) are required:
530 // a silent default of 0.0 would disable those physics entirely.
531 P.K_iv = require_param(p, "K_iv");
532 P.K_3D_cav_pref = require_param(p, "K_3D_cav_pref");
533 P.beta_He = require_param(p, "beta_He");
534 P.delta_He = require_param(p, "delta_He");
535 P.beta_He_exp= require_param(p, "beta_He_exp");
536 P.kBT = require_param(p, "kBT");
537
538 // Bin-moment parameters — support both old and new key names
539 P.I_bin = static_cast<int>(optional_param(p, "I_bin",
540 optional_param(p, "K_bins", 0.0)));
541 P.V_bin = static_cast<int>(optional_param(p, "V_bin",
542 optional_param(p, "K_v_bins", 0.0)));
543 P.r_ratio = optional_param(p, "r_ratio", 2.0);
544 P.i_discrete = static_cast<int>(optional_param(p, "i_discrete",
545 static_cast<double>(P.i_mobile)));
546 P.v_discrete = static_cast<int>(optional_param(p, "v_discrete",
547 static_cast<double>(P.v_mobile)));
548 P.shape_function = static_cast<int>(optional_param(p, "shape_function", 1.0));
549 // n_mom is DERIVED from shape_function (it duplicates that information):
550 // constant(0)->1 moment, linear(1)->2 moments, lognormal(2)->3 moments.
551 P.n_mom = P.shape_function + 1;
552
553 // Explicit integer bin edges transmitted from Python. Bin k covers
554 // integer sizes [bin_lo[k], bin_hi[k]-1]. Consumed directly so the C++
555 // never re-derives edges from r_ratio (numpy.floor vs std::floor can
556 // diverge over many bins, corrupting the last bins).
557 P.sia_bin_lo.resize(P.I_bin);
558 P.sia_bin_hi.resize(P.I_bin);
559 for (int k = 0; k < P.I_bin; ++k) {
560 P.sia_bin_lo[k] = static_cast<int>(
561 require_param(p, "sia_bin_lo_" + std::to_string(k)));
562 P.sia_bin_hi[k] = static_cast<int>(
563 require_param(p, "sia_bin_hi_" + std::to_string(k)));
564 }
565 P.vac_bin_lo.resize(P.V_bin);
566 P.vac_bin_hi.resize(P.V_bin);
567 for (int k = 0; k < P.V_bin; ++k) {
568 P.vac_bin_lo[k] = static_cast<int>(
569 require_param(p, "vac_bin_lo_" + std::to_string(k)));
570 P.vac_bin_hi[k] = static_cast<int>(
571 require_param(p, "vac_bin_hi_" + std::to_string(k)));
572 }
573
574 // If bin_moment mode (physics_option 2 or 3), override N_eq
575 // Layout: [discrete SIA | binned SIA moments | discrete VAC |
576 // binned VAC moments | He state]
577 // Always override when physics_option >= 2, even if I_bin=V_bin=0
578 // (pure-discrete subset of the full domain).
579 if (P.physics_option >= 2) {
580 int n_sia = P.i_discrete + P.n_mom * P.I_bin;
581 int n_vac = P.v_discrete + P.n_mom * P.V_bin;
582 int n_he;
583 if (P.he_mode == 1)
584 n_he = qss ? P.V_bin : P.V_bin + 1; // Q_k per vac bin
585 else
586 n_he = qss ? 1 : 2; // Q_tot scalar
587 P.N_eq = n_sia + n_vac + n_he + 5; // +5 for conservation accounting
588 P.y0.resize(P.N_eq);
589 }
590
591 // ── Optional appended ⟨100⟩ SIA block (loop conversion) ──────────────────
592 // Appended AFTER both N_eq computations (discrete + bin-moment) and the
593 // conservation block, so the existing offsets are untouched when OFF.
594 // The block carries the SAME size reduction as the ½⟨111⟩ population:
595 // full per-size (length I) in discrete modes, and the discrete-prefix +
596 // logarithmic-bin moment vector (i_discrete + n_mom*I_bin) in bin_moment
597 // modes — so "bin-moment for both SIA loop characters" is honoured.
598 P.cons_off = P.N_eq - 5; // conservation ODEs keep this position
599 P.loop_conversion = static_cast<int>(optional_param(p, "loop_conversion", 0.0));
600 if (P.loop_conversion) {
601 P.sia100_off = P.N_eq; // append at the end of the state vector
602 P.sia100_len = (P.physics_option >= 2)
603 ? (P.i_discrete + P.n_mom * P.I_bin) // bin-moment ⟨100⟩
604 : P.I; // discrete ⟨100⟩
605 P.N_eq += P.sia100_len; // c_i100 block (per-size or bin-moment)
606 P.y0.resize(P.N_eq); // re-extend; entries default in the loop below
607 P.conv_n_loop_min = static_cast<int>(optional_param(p, "n_loop_min", 4.0));
608 P.conv_n_j_min = static_cast<int>(optional_param(p, "n_j_min_junc", 30.0));
609 P.conv_phi_max = optional_param(p, "phi_max_junc", 0.5);
610 P.conv_sigma_s = optional_param(p, "sigma_s_junc", 0.35);
611 P.conv_psuccess = optional_param(p, "loop_conv_psuccess", 1.0);
612 // Default to conv_psuccess so a params file written by an older
613 // cpp_bridge (no such key) reproduces the legacy coupled behaviour.
614 P.conv_psuccess_abs = optional_param(p, "loop_conv_psuccess_abs",
615 P.conv_psuccess);
616 P.conv_absorb_boost = optional_param(p, "conv_absorb_boost", 1.0);
617 P.Gamma_uni.assign(P.I, 0.0);
618 P.K_100_grow.assign(P.I, 0.0);
619 P.K_100_shrink.assign(P.I, 0.0);
620 P.G_100.assign(P.I, 0.0);
621 P.Lambda_net_100.assign(P.I, 0.0);
622 for (int k = 0; k < P.I; ++k) {
623 P.Gamma_uni[k] = optional_param(p, "Gamma_uni_" + std::to_string(k), 0.0);
624 P.K_100_grow[k] = optional_param(p, "K_100_grow_" + std::to_string(k), 0.0);
625 P.K_100_shrink[k] = optional_param(p, "K_100_shrink_" + std::to_string(k), 0.0);
626 P.G_100[k] = optional_param(p, "G_100_" + std::to_string(k), 0.0);
627 P.Lambda_net_100[k] = optional_param(p, "lambda_net_100_" + std::to_string(k), 0.0);
628 }
629 }
630
631 // Cavity sweeping is independent of loop_conversion, so it is read outside
632 // the ⟨100⟩ block. Absent keys ⇒ all zeros ⇒ the channel is off and every
633 // downstream expression is unchanged.
634 P.Lambda_net_void.assign(P.V, 0.0);
635 for (int k = 0; k < P.V; ++k)
636 P.Lambda_net_void[k] = optional_param(p, "lambda_net_void_" + std::to_string(k), 0.0);
637
638 // Initial conditions
639 for (int k = 0; k < P.N_eq; ++k)
640 P.y0[k] = optional_param(p, "y0_" + std::to_string(k), 1e-100);
641
642 P.C_floor = optional_param(p, "C_floor", 1e-15);
643
644 // Solver settings
645 P.t_begin = require_param(p, "t_begin");
646 P.t_end = require_param(p, "t_end");
647 P.n_points = static_cast<int>(require_param(p, "n_points"));
648 P.log_time = (optional_param(p, "log_time", 1.0) > 0.5);
649 P.rtol = optional_param(p, "rtol", 1e-8);
650 P.atol = optional_param(p, "atol", 1e-20);
651
652 // Integration method
653 P.linsol = static_cast<int>(optional_param(p, "linsol", 0.0));
654 P.mu = static_cast<int>(optional_param(p, "mu",
655 static_cast<double>(P.N_eq - 1)));
656 P.ml = static_cast<int>(optional_param(p, "ml",
657 static_cast<double>(P.N_eq - 1)));
658 P.max_order = static_cast<int>(optional_param(p, "max_order", 4.0));
659 P.hmin = optional_param(p, "hmin", 0.0);
660 P.hmax = optional_param(p, "hmax", 0.0);
661
662 // Window parameters
663 P.window_mode = static_cast<int>(optional_param(p, "window_mode", 0.0));
664 // Single initial width shared by SIA and VAC; defaults to the larger domain.
665 P.window_width = static_cast<int>(optional_param(p, "window_width",
666 static_cast<double>(std::max(P.I, P.V))));
667 P.concentration_threshold = optional_param(p, "concentration_threshold", 1e-18);
668 P.window_pad = static_cast<int>(optional_param(p, "window_pad", 10.0));
669 // VAC expansion pad defaults to the SIA pad if not supplied.
670 P.window_pad_v = static_cast<int>(optional_param(p, "window_pad_v",
671 static_cast<double>(P.window_pad)));
672 P.window_expand_factor = optional_param(p, "window_expand_factor", 0.0);
673 P.window_check_every = static_cast<int>(optional_param(p, "window_check_every", 1.0));
674 P.window_C_contract = optional_param(p, "window_C_contract", 0.0);
675 P.window_min_active_i = static_cast<int>(optional_param(p, "window_min_active_i", 5.0));
676 P.window_nuc_guard = optional_param(p, "window_nuc_guard", 0.0);
677 P.window_t_start = optional_param(p, "window_t_start", 10.0);
678 P.Ni_extend_tol = optional_param(p, "Ni_extend_tol", 0.0);
679 P.Ni_extend_margin = static_cast<int>(optional_param(p, "Ni_extend_margin", 0.0));
680
681 // Preconditioner auto-selection: Jacobi by default, Woodbury when the
682 // Jacobian has a true bordered-arrow structure from coalescence reactions.
683 //
684 // Bordered-arrow ⇔ multiple mobile cluster sizes coalesce
685 // (i_mobile ≥ 2 or v_mobile ≥ 2). With only monomer mobility
686 // (i_mobile=1, v_mobile=1) the dense border collapses to two columns
687 // and Jacobi+GMRES converges adequately on its own.
688 //
689 // Additional gating:
690 // - linsol == 2 (GMRES) — preconditioners are only consulted by the
691 // iterative linear solver.
692 // - window_mode == 0 (full domain) — sliding-window modes keep the
693 // active system at 50–200 unknowns, where Woodbury's ~58-RHS setup
694 // cost outweighs its convergence advantage.
695 //
696 // The user can always override via the "prec_type" key in the param file.
697 {
698 const bool gmres = (P.linsol == 2);
699 const bool full_domain = (P.window_mode == 0);
700 const bool has_coalescence = (P.i_mobile >= 2) || (P.v_mobile >= 2);
701 // The Woodbury bordered-banded structure (band + mobile-species low rank)
702 // does not model the appended ⟨100⟩ block's coupling, so fall back to
703 // Jacobi-preconditioned GMRES when loop conversion is on (Jacobi is
704 // FD-based and handles arbitrary structure). Woodbury+conversion is a
705 // future optimization. The user can still force prec_type via the file.
706 const bool use_woodbury = gmres && full_domain && has_coalescence
707 && !P.loop_conversion;
708 P.prec_type = static_cast<int>(optional_param(p, "prec_type",
709 use_woodbury ? 1.0 : 0.0));
710 }
711 // Bandwidth auto-computed from mobility cutoffs
712 P.prec_bw = static_cast<int>(optional_param(p, "prec_bw",
713 static_cast<double>(std::max(2 * P.i_mobile, 2 * P.v_mobile) + 1)));
714 // Rank = number of mobile species (dense columns in Jacobian)
715 P.prec_rank = static_cast<int>(optional_param(p, "prec_rank",
716 static_cast<double>(P.i_mobile + P.v_mobile)));
717 P.prec_gamma = 0.0;
718
719 // Build mobile species index list for Woodbury preconditioner
720 if (P.prec_type == 1) {
721 P.prec_mobile_idx.resize(P.prec_rank);
722 // First i_mobile entries: SIA mobile indices 0..i_mobile-1
723 for (int k = 0; k < P.i_mobile; ++k)
724 P.prec_mobile_idx[k] = k;
725 // Next v_mobile entries: VAC mobile indices I..I+v_mobile-1
726 // (or i_discrete offset for bin_moment modes)
727 const int vac_off = (P.physics_option >= 2)
728 ? (P.i_discrete + P.n_mom * P.I_bin)
729 : P.I;
730 for (int k = 0; k < P.v_mobile; ++k)
731 P.prec_mobile_idx[P.i_mobile + k] = vac_off + k;
732 }
733
734 P.verbose = (optional_param(p, "verbose", 0.0) > 0.5);
735
736 return P;
737}
double optional_param(const std::map< std::string, double > &m, const std::string &key, double def)
Definition parameters.h:358
Parameters build_parameters(const std::map< std::string, double > &p)
Definition parameters.h:398
double require_param(const std::map< std::string, double > &m, const std::string &key)
Definition parameters.h:348
std::map< std::string, double > parse_param_file(const std::string &path)
Definition parameters.h:368
std::vector< double > Lambda_net_100
Definition parameters.h:195
std::vector< int > vac_bin_lo
Definition parameters.h:159
std::vector< double > m13
Definition parameters.h:92
int conv_n_loop_min
Definition parameters.h:175
int window_pad_v
Definition parameters.h:234
std::vector< double > bin_S2
Definition parameters.h:330
double hmin
Definition parameters.h:224
std::vector< double > bm_d111
Definition parameters.h:339
int loop_conversion
Definition parameters.h:167
double conv_psuccess
Definition parameters.h:179
double window_t_start
Definition parameters.h:240
std::vector< double > bm_dc_n
Definition parameters.h:338
double prec_gamma
Definition parameters.h:284
std::vector< double > bm_coal_dmu0
Definition parameters.h:340
std::vector< double > phiK_junc_tab
Definition parameters.h:325
int jac_n_colors
Definition parameters.h:300
std::vector< double > prec_Tinv_U
Definition parameters.h:274
std::vector< double > sqrt_tab
Definition parameters.h:318
std::vector< double > bm_GVV_eff
Definition parameters.h:338
double window_nuc_guard
Definition parameters.h:239
double trap_SIA
Definition parameters.h:73
double Z_i_loop
Definition parameters.h:121
double t_begin
Definition parameters.h:212
std::vector< int > jac_colors
Definition parameters.h:297
std::vector< double > pow23_tab
Definition parameters.h:319
std::vector< int > vac_bin_hi
Definition parameters.h:160
std::vector< int > sia_bin_lo
Definition parameters.h:157
double conv_absorb_boost
Definition parameters.h:183
std::vector< double > bin_S1
Definition parameters.h:330
std::vector< int > jac_col_ptr
Definition parameters.h:295
std::vector< int > jac_color_groups
Definition parameters.h:298
int window_min_active_i
Definition parameters.h:238
std::vector< double > Lambda_net_void
Definition parameters.h:204
double A_loop
Definition parameters.h:68
double k2_disl_i
Definition parameters.h:130
std::vector< int > prec_ipiv_schur
Definition parameters.h:277
std::vector< double > bm_dc_v
Definition parameters.h:338
int window_mode
Definition parameters.h:230
std::vector< double > Gamma_uni
Definition parameters.h:186
std::vector< double > bm_c_v
Definition parameters.h:338
std::vector< double > prec_deltas
Definition parameters.h:282
double A_loop_inv_O23
Definition parameters.h:120
double Cv_eq
Definition parameters.h:132
double k2_disl_v
Definition parameters.h:129
std::vector< double > K_IclV_ns
Definition parameters.h:106
double t_end
Definition parameters.h:213
std::vector< double > GII
Definition parameters.h:97
double A_sph
Definition parameters.h:67
std::vector< double > Pr_VAC
Definition parameters.h:91
double conv_sigma_s
Definition parameters.h:178
double hmax
Definition parameters.h:225
std::vector< int > prec_mobile_idx
Definition parameters.h:278
std::vector< double > Z_i_loop_arr
Definition parameters.h:124
std::vector< double > bm_c_n
Definition parameters.h:338
std::vector< double > KIV
Definition parameters.h:96
std::vector< double > bm_c_n100
Definition parameters.h:339
std::vector< double > xi_tab
Definition parameters.h:320
std::vector< double > KHeV
Definition parameters.h:90
std::vector< int > jac_row_idx
Definition parameters.h:296
double window_expand_factor
Definition parameters.h:235
double L_hat
Definition parameters.h:70
double A_sph_inv_O23
Definition parameters.h:119
std::vector< double > Pr_SIA
Definition parameters.h:99
std::vector< double > K_1D_pref
Definition parameters.h:103
std::vector< double > KVV
Definition parameters.h:87
std::vector< double > bm_d100
Definition parameters.h:339
int boundary_flux
Definition parameters.h:84
std::vector< int > sia_bin_hi
Definition parameters.h:158
std::vector< double > y0
Definition parameters.h:206
double conv_psuccess_abs
Definition parameters.h:180
std::vector< int > jac_color_offsets
Definition parameters.h:299
std::vector< double > prec_y_save
Definition parameters.h:281
int phiK_junc_stride
Definition parameters.h:326
std::vector< double > K_IclV_ni
Definition parameters.h:107
int window_width
Definition parameters.h:231
int physics_option
Definition parameters.h:62
double C_floor
Definition parameters.h:209
std::vector< double > vbin_S1
Definition parameters.h:331
double atol
Definition parameters.h:217
double G_He
Definition parameters.h:128
std::vector< double > KII
Definition parameters.h:95
std::vector< double > prec_work
Definition parameters.h:280
double beta_He_exp
Definition parameters.h:139
double K_3D_cav_pref
Definition parameters.h:134
double k2_disl_He
Definition parameters.h:131
int shape_function
Definition parameters.h:149
std::vector< double > G_100
Definition parameters.h:189
double trap_loop
Definition parameters.h:75
int Ni_extend_margin
Definition parameters.h:242
double concentration_threshold
Definition parameters.h:232
std::vector< double > prec_diag
Definition parameters.h:246
std::vector< double > K_100_shrink
Definition parameters.h:188
int size_tab_max
Definition parameters.h:316
double K_iv
Definition parameters.h:133
std::vector< double > prec_schur
Definition parameters.h:275
double conv_phi_max
Definition parameters.h:177
std::vector< double > D_loop_coal
Definition parameters.h:118
std::vector< double > vbin_S2
Definition parameters.h:331
std::vector< double > cbrt_tab
Definition parameters.h:317
std::vector< double > D_SIA_eff
Definition parameters.h:110
double beta_He
Definition parameters.h:137
int sia_emission_monomer
Definition parameters.h:117
int he_kinetics
Definition parameters.h:64
double Ni_extend_tol
Definition parameters.h:241
bool log_time
Definition parameters.h:215
std::vector< double > D_VAC_eff
Definition parameters.h:111
double r_ratio
Definition parameters.h:145
int window_check_every
Definition parameters.h:236
std::vector< double > k2_SIA
Definition parameters.h:98
std::vector< double > prec_f_pert
Definition parameters.h:283
int conv_n_j_min
Definition parameters.h:176
double trap_VAC
Definition parameters.h:74
std::vector< int > prec_ipiv_band
Definition parameters.h:276
std::vector< double > K_100_grow
Definition parameters.h:187
std::vector< double > bm_coal_dmu2
Definition parameters.h:340
double rtol
Definition parameters.h:216
double Z_ii
Definition parameters.h:125
std::vector< double > KVI
Definition parameters.h:88
std::vector< double > prec_f0
Definition parameters.h:279
double B_rot
Definition parameters.h:69
double kBT
Definition parameters.h:140
double delta_He
Definition parameters.h:138
double window_C_contract
Definition parameters.h:237
std::vector< double > prec_band
Definition parameters.h:273
std::vector< double > GVV
Definition parameters.h:89
std::vector< double > bm_coal_dmu1
Definition parameters.h:340