RadCluster
Graph-based cluster dynamics for irradiated materials
Loading...
Searching...
No Matches
solver.cpp
Go to the documentation of this file.
1
36#include "parameters.h"
37#include "rate_equations.h"
38
39#include <cvode/cvode.h>
40#include <nvector/nvector_serial.h>
41#include <sunlinsol/sunlinsol_dense.h>
42#include <sunlinsol/sunlinsol_band.h>
43#include <sunlinsol/sunlinsol_spgmr.h>
44#include <sunmatrix/sunmatrix_dense.h>
45#include <sunmatrix/sunmatrix_band.h>
46#include <sundials/sundials_types.h>
47
48#ifdef CD_HAVE_KLU
49# include "sparse_jacobian.h"
50# include <sunlinsol/sunlinsol_klu.h>
51# include <sunmatrix/sunmatrix_sparse.h>
52#endif
53
54#include <atomic>
55#include <cmath>
56#include <csignal>
57#include <cstdio>
58#include <iomanip>
59#include <iostream>
60#include <map>
61#include <string>
62#include <vector>
63
64#ifdef __APPLE__
65#include <sys/sysctl.h> // hw.perflevel0.logicalcpu — performance-core count
66#endif
67
68#ifdef CD_HAVE_OPENMP
69# include <omp.h>
70#endif
71
72#ifdef _WIN32
73# define WIN32_LEAN_AND_MEAN
74# ifndef NOMINMAX
75# define NOMINMAX // prevent windows.h from defining min/max macros
76# endif // that collide with std::min / std::max
77# include <windows.h>
78#endif
79
80// ── Graceful interrupt handling ──────────────────────────────────────────────
81// A SIGINT / SIGTERM (POSIX) or CTRL_C_EVENT / CTRL_BREAK_EVENT (Windows)
82// flips this flag. The integration loop checks it at the top of every
83// output-step iteration and breaks cleanly so the post-loop block (final
84// CVODE stats, file close) still runs and the partial .bin output is a
85// valid prefix of a normal completed run.
86namespace {
87std::atomic<bool> g_interrupt_requested{false};
88}
89
90#ifdef _WIN32
91static BOOL WINAPI win_console_handler(DWORD ctrl_type) {
92 switch (ctrl_type) {
93 case CTRL_C_EVENT:
94 case CTRL_BREAK_EVENT:
95 case CTRL_CLOSE_EVENT:
96 case CTRL_LOGOFF_EVENT:
97 case CTRL_SHUTDOWN_EVENT:
98 g_interrupt_requested.store(true);
99 return TRUE; // signal handled — don't terminate immediately
100 }
101 return FALSE;
102}
103#else
104extern "C" void posix_interrupt_handler(int /*sig*/) {
105 g_interrupt_requested.store(true);
106}
107#endif
108
110#ifdef _WIN32
111 SetConsoleCtrlHandler(win_console_handler, TRUE);
112#else
113 std::signal(SIGINT, posix_interrupt_handler);
114 std::signal(SIGTERM, posix_interrupt_handler);
115#endif
116}
117
118// ── CLI argument parser ────────────────────────────────────────────────────────
119
120static std::map<std::string, double> parse_cli_args(int argc, char* argv[]) {
121 std::map<std::string, double> props;
122 for (int i = 1; i < argc; ++i) {
123 std::string arg = argv[i];
124 if (arg.size() < 3 || arg[0] != '-' || arg[1] != '-') continue;
125 auto pos = arg.find('=');
126 if (pos == std::string::npos) continue;
127 std::string key = arg.substr(2, pos - 2);
128 try { props[key] = std::stod(arg.substr(pos + 1)); }
129 catch (...) { std::cerr << "Bad value for '" << key << "'\n"; }
130 }
131 return props;
132}
133
134// ── SUNDIALS error check ───────────────────────────────────────────────────────
135
136#define CHECK_SUNDIALS(call) do { \
137 int _r = (call); \
138 if (_r < 0) { \
139 std::cerr << "SUNDIALS error " << _r \
140 << " at " #call "\n"; \
141 return 1; \
142 } \
143} while (0)
144
145// ── Main ──────────────────────────────────────────────────────────────────────
146
147int main(int argc, char* argv[]) {
148
150
151 std::map<std::string, double> args;
152 std::string param_path;
153
154 if (argc >= 2) {
155 const std::string pf = "--param_file=";
156 std::string a1 = argv[1];
157 if (a1.rfind(pf, 0) == 0) {
158 param_path = a1.substr(pf.size());
159 args = parse_param_file(param_path);
160 } else {
161 args = parse_cli_args(argc, argv);
162 }
163 }
164 if (args.empty() && argc > 1) return 1;
165
167 const int N_EQ = P.N_eq;
168
169 // Precompute every y-independent kernel quantity (cbrt/sqrt/pow of the
170 // integer sizes, the Marian junction yield, the per-bin reconstruction
171 // moments). Must precede the first RHS call. Pure cost optimization —
172 // the RHS arithmetic is unchanged, see build_kernel_tables().
174
175 // ── Binary output ─────────────────────────────────────────────────────────
176 FILE* fp_bin = nullptr;
177 FILE* fp_win = nullptr; // sidecar CSV: t,x_hi_i,x_hi_v per output point
178 if (!param_path.empty()) {
179 std::string bp = param_path;
180 const std::string suf = ".txt";
181 if (bp.size() >= suf.size() &&
182 bp.compare(bp.size() - suf.size(), suf.size(), suf) == 0)
183 bp.replace(bp.size() - suf.size(), suf.size(), ".bin");
184 else bp += ".bin";
185#ifdef _MSC_VER
186 fopen_s(&fp_bin, bp.c_str(), "wb");
187#else
188 fp_bin = std::fopen(bp.c_str(), "wb");
189#endif
190 if (!fp_bin)
191 std::cerr << "Warning: cannot open " << bp << " — using stdout\n";
192
193 // Window-bounds sidecar: <bin_path>.window.csv
194 const std::string wp = bp + ".window.csv";
195#ifdef _MSC_VER
196 fopen_s(&fp_win, wp.c_str(), "w");
197#else
198 fp_win = std::fopen(wp.c_str(), "w");
199#endif
200 if (fp_win)
201 std::fprintf(fp_win, "t,x_hi_i,x_hi_v\n");
202 }
203
204 auto write_row = [&](double t, const double* data, int n) {
205 if (fp_bin) {
206 std::fwrite(&t, sizeof(double), 1, fp_bin);
207 std::fwrite(data, sizeof(double), n, fp_bin);
208 std::fflush(fp_bin); // flush so partial output survives kill/interrupt
209 } else {
210 std::cout << t;
211 for (int k = 0; k < n; ++k) std::cout << ' ' << data[k];
212 std::cout << '\n';
213 }
214 };
215
216 auto write_window = [&](double t, int x_hi_i, int x_hi_v) {
217 if (fp_win) {
218 std::fprintf(fp_win, "%.17e,%d,%d\n", t, x_hi_i, x_hi_v);
219 std::fflush(fp_win);
220 }
221 };
222
223 // Window mode validation
224 constexpr int WINDOW_N_THRESH = 500;
225 if (P.window_mode == 4 &&
226 P.I < WINDOW_N_THRESH && P.V < WINDOW_N_THRESH) {
227 std::cerr << "[Window] I=" << P.I << " and V=" << P.V
228 << " both < threshold=" << WINDOW_N_THRESH
229 << " — using full solver.\n";
230 P.window_mode = 0;
231 }
232#ifndef CD_HAVE_OPENMP
233 if (P.window_mode == 4) {
234 std::cerr << "[active_window] OpenMP unavailable — running serial (single thread).\n";
235 }
236#endif
237
238 // Auto-select OMP thread count from problem size.
239 //
240 // The metric is the length of the parallel RHS loops, NOT N_eq. In the
241 // discrete modes the two coincide, but bin_moment integrates only
242 // i_discrete + n_mom·I_bin + v_discrete + n_mom·V_bin + ...
243 // unknowns (O(100)) while still sweeping the RECONSTRUCTED per-size
244 // spectra c_n[I] and c_v[V] on every call. Keying off N_eq there put a
245 // 200 000-element sweep on a single thread — an I = V = 1e5 bin_moment
246 // run picked 1 thread for N_eq = 135. max(N_eq, I + V) covers both.
247 //
248 // User can still override via OMP_NUM_THREADS — if it is set in the
249 // environment, we honor it as-is.
250 const int OMP_WORK = std::max(N_EQ, P.I + P.V);
251#ifdef CD_HAVE_OPENMP
252 int omp_threads = 1;
253#ifdef _MSC_VER
254 char* omp_env = nullptr;
255 size_t omp_env_len = 0;
256 _dupenv_s(&omp_env, &omp_env_len, "OMP_NUM_THREADS");
257#else
258 const char* omp_env = std::getenv("OMP_NUM_THREADS");
259#endif
260 if (omp_env && *omp_env) {
261 omp_threads = omp_get_max_threads(); // already honored by runtime
262 std::cerr << "[OpenMP] OMP_NUM_THREADS=" << omp_env
263 << " honored — using " << omp_threads << " thread"
264 << (omp_threads == 1 ? "" : "s") << "\n";
265 } else {
266 int hw_max = omp_get_max_threads();
267#ifdef __APPLE__
268 // Apple silicon is heterogeneous (performance + efficiency cores).
269 // The RHS parallel regions are barrier-synchronised sweeps over
270 // equal-sized chunks, so a thread landing on a slow E-core holds up
271 // every join. Measured on a 12P+4E M-series part, I=V=1e5 bin_moment:
272 // 4 thr 76.4 s, 8 thr 73.5 s, 12 thr 76.9 s, 16 thr 80.1 s — using all
273 // 16 is ~9% SLOWER than staying on the performance cores. Cap there.
274 {
275 int perf = 0;
276 size_t len = sizeof(perf);
277 if (sysctlbyname("hw.perflevel0.logicalcpu", &perf, &len, nullptr, 0) == 0
278 && perf > 0 && perf < hw_max)
279 hw_max = perf;
280 }
281#endif
282 int picked;
283 if (OMP_WORK < 500) picked = 1;
284 else if (OMP_WORK < 2000) picked = 2;
285 else if (OMP_WORK < 5000) picked = 4;
286 else if (OMP_WORK < 10000) picked = 8;
287 else if (OMP_WORK < 20000) picked = 12;
288 else if (OMP_WORK < 40000) picked = 16;
289 else if (OMP_WORK < 80000) picked = 20;
290 else picked = hw_max; // largest problems get all cores
291 if (picked > hw_max) picked = hw_max; // cap at machine maximum
292 omp_set_num_threads(picked);
293 omp_threads = picked;
294 std::cerr << "[OpenMP] auto-selected " << omp_threads
295 << " thread" << (omp_threads == 1 ? "" : "s")
296 << " for work=" << OMP_WORK
297 << " (N_eq=" << N_EQ << ", I+V=" << (P.I + P.V)
298 << ", hw_max=" << hw_max << ")\n";
299 }
300 // Emit a machine-readable line so the Python provenance can capture
301 // the actual thread count chosen by the solver.
302 std::cerr << "[OpenMP_threads] " << omp_threads << "\n";
303#ifdef _MSC_VER
304 if (omp_env) std::free(omp_env);
305#endif
306#else
307 std::cerr << "[OpenMP] not available — running serial (1 thread)\n";
308 std::cerr << "[OpenMP_threads] 1\n";
309#endif
310
311 const char* he_kin_str = (P.he_kinetics == 1) ? "quasi_steady_state" : "dynamic";
312 std::cerr << "RadCluster_2_1 solver: N_eq=" << N_EQ
313 << " physics_option=" << P.physics_option
314 << " he_mode=" << P.he_mode
315 << " he_kinetics=" << he_kin_str
316 << " C_floor=" << P.C_floor
317 << " window_mode=" << P.window_mode;
318 if (P.window_mode != 0) {
319 std::cerr << " win_SIA=[0," << std::min(P.window_width - 1, P.I - 1) << "->" << (P.I - 1) << "]"
320 << " win_VAC=[0," << std::min(P.window_width - 1, P.V - 1) << "->" << (P.V - 1) << "]";
321 }
322 std::cerr << "\n";
323
324 // ── Time grid ─────────────────────────────────────────────────────────────
325 std::vector<double> t_eval(P.n_points);
326 if (P.log_time) {
327 double l0 = std::log10(P.t_begin), lf = std::log10(P.t_end);
328 double step = (lf - l0) / (P.n_points - 1);
329 for (int i = 0; i < P.n_points; ++i)
330 t_eval[i] = std::pow(10.0, l0 + i * step);
331 } else {
332 double step = (P.t_end - P.t_begin) / (P.n_points - 1);
333 for (int i = 0; i < P.n_points; ++i)
334 t_eval[i] = P.t_begin + i * step;
335 }
336
337 // ── SUNDIALS context ──────────────────────────────────────────────────────
338 SUNContext sunctx;
339 if (SUNContext_Create(SUN_COMM_NULL, &sunctx) != 0) {
340 std::cerr << "SUNContext_Create failed\n"; return 1;
341 }
342
343 // ── State vector ──────────────────────────────────────────────────────────
344 N_Vector y = N_VNew_Serial(N_EQ, sunctx);
345 if (!y) { std::cerr << "N_VNew_Serial failed\n"; return 1; }
346 double* ydata = N_VGetArrayPointer_Serial(y);
347 for (int k = 0; k < N_EQ; ++k)
348 ydata[k] = std::max(P.y0[k], P.C_floor);
349
350 // ── Offset of the first VAC state in the full state vector ───────────────
351 // full_CD: P.I
352 // bin_moment: P.i_discrete + P.n_mom * P.I_bin
353 const int i_VAC_off = (P.physics_option >= 2)
354 ? (P.i_discrete + P.n_mom * P.I_bin)
355 : P.I;
356 // Number of VAC state-vector entries per window domain
357 const int V_states = (P.physics_option >= 2)
358 ? (P.v_discrete + P.n_mom * P.V_bin)
359 : P.V;
360
361 // ── Select RHS ────────────────────────────────────────────────────────────
362 CVRhsFn rhs_fn;
363 if (P.physics_option >= 2)
364 rhs_fn = rhs_bin_moment;
365 else
366 rhs_fn = rhs_full_CD;
367
368 // ── User data ─────────────────────────────────────────────────────────────
369 UserData ud;
370 ud.P = &P;
371 ud.rhs_fn = rhs_fn;
372 ud.x_lo_i = 0;
373 ud.x_hi_i = P.I - 1;
374 ud.x_lo_v = 0;
375 ud.x_hi_v = V_states - 1;
376 ud.window_active = (P.window_mode != 0);
377
378 // ── Create CVODE ──────────────────────────────────────────────────────────
379 void* cvode_mem = CVodeCreate(CV_BDF, sunctx);
380 if (!cvode_mem) { std::cerr << "CVodeCreate failed\n"; return 1; }
381
382 CHECK_SUNDIALS(CVodeInit(cvode_mem, rhs_fn, P.t_begin, y));
383 CHECK_SUNDIALS(CVodeSStolerances(cvode_mem, P.rtol, P.atol));
384 CHECK_SUNDIALS(CVodeSetUserData(cvode_mem, &ud));
385 CHECK_SUNDIALS(CVodeSetMaxNumSteps(cvode_mem, 500000));
386 if (P.max_order > 0)
387 CHECK_SUNDIALS(CVodeSetMaxOrd(cvode_mem, P.max_order));
388 if (P.hmin > 0.0)
389 CHECK_SUNDIALS(CVodeSetMinStep(cvode_mem, P.hmin));
390 if (P.hmax > 0.0)
391 CHECK_SUNDIALS(CVodeSetMaxStep(cvode_mem, P.hmax));
392
393 // Non-negativity is enforced via C_floor clamping at output time (line 317-318).
394 // CVODE constraint enforcement (CVodeSetConstraints) is intentionally disabled
395 // because it causes Newton corrector failures when near-floor concentrations
396 // interact with the stiff SIA–vacancy coupling at moderate doses.
397
398 // ── Linear solver ─────────────────────────────────────────────────────────
399 SUNMatrix sunmat = nullptr;
400 SUNLinearSolver sunls = nullptr;
401
402 if (P.linsol == 2) {
403 // GMRES is always preconditioned (Jacobi or Woodbury, selected by prec_type).
404 constexpr int GMRES_MAXL = 50;
405 sunls = SUNLinSol_SPGMR(y, SUN_PREC_RIGHT, GMRES_MAXL, sunctx);
406 if (!sunls) { std::cerr << "SPGMR create failed\n"; return 1; }
407 CHECK_SUNDIALS(CVodeSetLinearSolver(cvode_mem, sunls, nullptr));
408 CHECK_SUNDIALS(CVodeSetPreconditioner(cvode_mem, prec_setup, prec_solve));
409 std::cout << "[solver] preconditioner: "
410 << (P.prec_type == 1 ? "Woodbury (bordered-arrow, rank "
411 + std::to_string(P.prec_rank)
412 + ", bw " + std::to_string(P.prec_bw) + ")"
413 : "Jacobi (diagonal)")
414 << "\n";
415 } else if (P.linsol == 1) {
416 // Band
417 int mu = (P.mu > 0) ? P.mu : N_EQ - 1;
418 int ml = (P.ml > 0) ? P.ml : N_EQ - 1;
419 sunmat = SUNBandMatrix(N_EQ, mu, ml, sunctx);
420 sunls = SUNLinSol_Band(y, sunmat, sunctx);
421 if (!sunmat || !sunls) { std::cerr << "Band solver create failed\n"; return 1; }
422 CHECK_SUNDIALS(CVodeSetLinearSolver(cvode_mem, sunls, sunmat));
423 } else if (P.linsol == 3) {
424#ifdef CD_HAVE_KLU
425 // KLU sparse direct solver with colored-FD Jacobian.
426 // Restricted to full_CD modes (physics_option 0 or 1).
427 if (P.physics_option != 0 && P.physics_option != 1) {
428 std::cerr << "[KLU] linsol=klu only supports full_CD modes "
429 "(physics_option 0 or 1). Use gmres for bin_moment.\n";
430 return 1;
431 }
433 if (nnz <= 0) {
434 std::cerr << "[KLU] sparsity pattern build failed\n"; return 1;
435 }
438 std::cout << "[KLU] N_eq=" << N_EQ
439 << " nnz=" << nnz
440 << " density=" << std::scientific << std::setprecision(2)
441 << double(nnz) / (double(N_EQ) * double(N_EQ))
442 << " colors=" << P.jac_n_colors
443 << " (~" << P.jac_n_colors + 1
444 << " RHS evals per Jacobian)\n";
445
446 sunmat = SUNSparseMatrix(N_EQ, N_EQ, nnz, CSC_MAT, sunctx);
447 if (!sunmat) { std::cerr << "Sparse matrix create failed\n"; return 1; }
448 // Install the precomputed CSC pattern (sorted row indices per column).
449 sunindextype* col_ptr = SUNSparseMatrix_IndexPointers(sunmat);
450 sunindextype* row_idx = SUNSparseMatrix_IndexValues (sunmat);
451 for (int j = 0; j <= N_EQ; ++j) col_ptr[j] = P.jac_col_ptr[j];
452 for (int k = 0; k < nnz; ++k) row_idx[k] = P.jac_row_idx[k];
453
454 sunls = SUNLinSol_KLU(y, sunmat, sunctx);
455 if (!sunls) { std::cerr << "KLU create failed\n"; return 1; }
456 CHECK_SUNDIALS(CVodeSetLinearSolver(cvode_mem, sunls, sunmat));
457 CHECK_SUNDIALS(CVodeSetJacFn(cvode_mem, sparse_fd_jac));
458#else
459 std::cerr << "[KLU] linsol=klu requested but solver was built without "
460 "KLU support (CD_HAVE_KLU not defined). Rebuild SUNDIALS "
461 "with -DENABLE_KLU=ON and link sunlinsolklu.\n";
462 return 1;
463#endif
464 } else {
465 // Dense (default)
466 sunmat = SUNDenseMatrix(N_EQ, N_EQ, sunctx);
467 sunls = SUNLinSol_Dense(y, sunmat, sunctx);
468 if (!sunmat || !sunls) { std::cerr << "Dense solver create failed\n"; return 1; }
469 CHECK_SUNDIALS(CVodeSetLinearSolver(cvode_mem, sunls, sunmat));
470 }
471
472 // ── Integration loop ──────────────────────────────────────────────────────
473 // Two independent sliding windows:
474 // SIA window: active SIA state indices x_lo_i .. x_hi_i (0-based, ≤ I-1)
475 // VAC window: active VAC state indices x_lo_v .. x_hi_v (0-based, ≤ V_states-1)
476 // window_mode==0: windows span the full domain (no truncation).
477 // window_mode==4: start from a user-specified initial width (window_width,
478 // shared by SIA and VAC) and expand independently as the leading
479 // concentration in each axis exceeds concentration_threshold.
480 int x_lo_i = 0;
481 int x_hi_i = (P.window_mode == 0) ? P.I - 1
482 : std::min(P.window_width - 1, P.I - 1);
483 int x_lo_v = 0;
484 int x_hi_v = (P.window_mode == 0) ? V_states - 1
485 : std::min(P.window_width - 1, V_states - 1);
486
487 // Output buffer
488 std::vector<double> out_row(N_EQ);
489
490 // Write initial condition
491 for (int k = 0; k < N_EQ; ++k)
492 out_row[k] = std::max(ydata[k], 0.0);
493 write_row(P.t_begin, out_row.data(), N_EQ);
494 write_window(P.t_begin, x_hi_i, x_hi_v);
495
496 int n_written = 1;
497 int check_every = std::max(P.window_check_every, 1);
498 bool was_interrupted = false;
499
500 for (int i = 1; i < P.n_points; ++i) {
501 if (g_interrupt_requested.load()) {
502 std::cerr << "[interrupt] received at pt=" << i << "/" << P.n_points
503 << " — finalising " << n_written
504 << " saved time points and exiting cleanly.\n";
505 was_interrupted = true;
506 break;
507 }
508 double t_out = t_eval[i];
509
510 // Update both window upper bounds (active_window)
511 if (P.window_mode != 0) {
512 if ((i - 1) % check_every == 0) {
513 // ── SIA window expansion ───────────────────────────────────
514 // Expand when the leading SIA cluster concentration exceeds
515 // concentration_threshold (absolute index x_hi_i in state vector).
516 if (x_hi_i < P.I - 1 && x_hi_i < N_EQ - 1 &&
517 ydata[x_hi_i] > P.concentration_threshold) {
518 x_hi_i = std::min(x_hi_i + P.window_pad, P.I - 1);
519 }
520 // ── VAC window expansion ───────────────────────────────────
521 // ydata[i_VAC_off + x_hi_v] is the leading VAC concentration
522 // in the state vector (absolute index i_VAC_off + x_hi_v).
523 if (x_hi_v < V_states - 1) {
524 const int vac_abs = i_VAC_off + x_hi_v;
525 if (vac_abs < N_EQ && ydata[vac_abs] > P.concentration_threshold) {
526 x_hi_v = std::min(x_hi_v + P.window_pad_v, V_states - 1);
527 }
528 }
529 }
530 ud.x_hi_i = x_hi_i;
531 ud.x_lo_i = x_lo_i;
532 ud.x_hi_v = x_hi_v;
533 ud.x_lo_v = x_lo_v;
534 }
535
536 double t_now = P.t_begin;
537 if (i > 0) CVodeGetCurrentTime(cvode_mem, &t_now);
538
539 int retval = CVode(cvode_mem, t_out, y, &t_now, CV_NORMAL);
540
541 // ── Progress diagnostics (always, to stderr) ──────────────────────
542 {
543 long int nst = 0, nfe = 0, nni = 0, ncfn = 0, netf = 0;
544 double hlast = 0.0;
545 CVodeGetNumSteps(cvode_mem, &nst);
546 CVodeGetNumRhsEvals(cvode_mem, &nfe);
547 CVodeGetNumNonlinSolvIters(cvode_mem, &nni);
548 CVodeGetNumNonlinSolvConvFails(cvode_mem, &ncfn);
549 CVodeGetNumErrTestFails(cvode_mem, &netf);
550 CVodeGetLastStep(cvode_mem, &hlast);
551 std::cerr << "[cvode] pt=" << i << "/" << P.n_points
552 << " t=" << t_now
553 << " steps=" << nst << " rhs=" << nfe
554 << " nlcf=" << ncfn << " etf=" << netf
555 << " h=" << hlast
556 << " ret=" << retval << "\n";
557 }
558
559 if (retval < 0) {
560 std::cerr << "CVode failed at t=" << t_out << " retval=" << retval
561 << " — stopping integration (no reinit to preserve conservation)\n";
562 // Do NOT reinit: CVodeReInit resets BDF history, which corrupts the
563 // cumulative conservation-accounting ODEs and produces the "Sum > 1"
564 // artifacts and solution branch jumps visible in post-processed plots.
565 // Instead, stop cleanly so the partial output up to t_now is valid.
566 break;
567 }
568
569 // Only write output for time points where the solver succeeded.
570 // Writing a post-ReInit failure row produces the spike artefacts in plots.
571 if (retval >= 0) {
572 // Apply C_floor post-step: clamp the CVODE state before writing so
573 // output concentrations never fall below the prescribed minimum.
574 // This keeps the RHS smooth (no kink above zero) while still
575 // enforcing the user-specified floor at every output point.
576 for (int k = 0; k < N_EQ; ++k)
577 ydata[k] = std::max(ydata[k], P.C_floor);
578 for (int k = 0; k < N_EQ; ++k)
579 out_row[k] = ydata[k];
580 write_row(t_out, out_row.data(), N_EQ);
581 write_window(t_out, x_hi_i, x_hi_v);
582 ++n_written;
583
584 // ── Progress diagnostics ───────────────────────────────────────
585 // Enabled only when verbose=1 is passed in the parameter file.
586 if (!P.verbose) continue;
587
588 const int I = P.I;
589 const int V = P.V;
590
591 // State vector layout depends on physics_option:
592 // full_CD: y[0..I-1] = c_i, y[I..I+V-1] = c_v, ...
593 // bin_moment: y[0..i_d-1] = discrete SIA,
594 // y[i_d..i_d+PM*Ib-1] = binned SIA moments,
595 // y[i_VAC..i_VAC+v_d-1] = discrete VAC,
596 // y[i_VAC+v_d..i_VAC+v_d+PM*Kv-1] = binned VAC moments,
597 // y[i_Q_base] = Q_tot (case2) or Q_k (case1)
598 const bool is_bin = (P.physics_option >= 2);
599 const int Ib = is_bin ? P.I_bin : 0;
600 const int Kv = is_bin ? P.V_bin : 0;
601 const int PM = is_bin ? P.n_mom : 0;
602 const int i_d = is_bin ? P.i_discrete : 0;
603 const int v_d = is_bin ? P.v_discrete : 0;
604 const int i_VAC = is_bin ? (i_d + PM * Ib) : I;
605 const int i_Q_base = is_bin ? (i_VAC + v_d + PM * Kv)
606 : (I + V);
607
608 // Reconstruct c_i1 from bin-moment state (bin 0 = monomer only)
609 const double c_i1 = ydata[0];
610 const double c_v1 = ydata[i_VAC];
611
612 // Total SIA content Sigma n*c_n
613 double SIA_content = 0.0;
614 if (is_bin) {
615 // Discrete SIA sizes: Sigma n*c_n for n=1..i_discrete
616 for (int n = 0; n < i_d; ++n)
617 SIA_content += (n + 1.0) * ydata[n];
618 // Binned SIA: first moments mu_k^{(1)}
619 for (int k = 0; k < Ib; ++k)
620 SIA_content += ydata[i_d + PM*k + 1];
621 } else {
622 for (int n = 0; n < I; ++n)
623 SIA_content += (n + 1.0) * ydata[n];
624 }
625
626 // Total vacancy content Sigma m*c_m
627 double VAC_content = 0.0;
628 if (is_bin) {
629 // Discrete VAC sizes: Sigma m*c_m for m=1..v_discrete
630 for (int m = 0; m < v_d; ++m)
631 VAC_content += (m + 1.0) * ydata[i_VAC + m];
632 // Binned VAC: first moments (if PM>=2) or midpoint approx.
633 // Bin edges are consumed from the explicit arrays transmitted
634 // by Python (P.vac_bin_lo/hi) — NOT re-derived from r_ratio.
635 if (Kv > 0) {
636 const int vac_mom = i_VAC + v_d;
637 if (PM >= 2) {
638 for (int k = 0; k < Kv; ++k)
639 VAC_content += ydata[vac_mom + PM*k + 1];
640 } else {
641 for (int k = 0; k < Kv; ++k) {
642 int mlo = P.vac_bin_lo[k];
643 int mhi = P.vac_bin_hi[k];
644 double mid = 0.5 * (mlo + mhi - 1);
645 VAC_content += ydata[vac_mom + k] * mid;
646 }
647 }
648 }
649 } else {
650 for (int m = 0; m < V; ++m)
651 VAC_content += (m + 1.0) * ydata[I + m];
652 }
653
654 // Representative cluster sizes
655 const double c_i2 = is_bin ? ((i_d > 1) ? ydata[1] : 0.0)
656 : ((I > 1) ? ydata[1] : 0.0);
657 const double c_i5 = is_bin ? ((i_d > 4) ? ydata[4] : 0.0)
658 : ((I > 4) ? ydata[4] : 0.0);
659 const double c_v2 = is_bin ? ((v_d > 1) ? ydata[i_VAC + 1] : 0.0)
660 : ((V > 1) ? ydata[I + 1] : 0.0);
661 const double c_v5 = is_bin ? ((v_d > 4) ? ydata[i_VAC + 4] : 0.0)
662 : ((V > 4) ? ydata[I + 4] : 0.0);
663
664 // He
665 const double Q_tot = ydata[i_Q_base];
666 const double c_h = (P.he_kinetics == 0 && N_EQ > i_Q_base + 1)
667 ? ydata[i_Q_base + 1] : -1.0;
668
669 std::cerr << std::scientific << std::setprecision(3)
670 << " [diag] t=" << t_out
671 << " c_i1=" << c_i1
672 << " c_v1=" << c_v1
673 << " c_i2=" << c_i2
674 << " c_i5=" << c_i5
675 << " c_v2=" << c_v2
676 << " c_v5=" << c_v5
677 << " Q_tot=" << Q_tot;
678 if (c_h >= 0.0)
679 std::cerr << " c_h=" << c_h;
680 std::cerr << " SIA_tot=" << SIA_content
681 << " VAC_tot=" << VAC_content
682 << "\n";
683
684 // ── C_i5 reaction rate breakdown (n=5, index 4) ───────────────
685 // For bin_moment mode, c_i/c_v indices differ from full_CD
686 if (!is_bin && I > 4) {
687 const double ci4 = std::max(ydata[3], 0.0);
688 const double ci5_ = c_i5;
689 const double ci6 = (I > 5) ? std::max(ydata[5], 0.0) : 0.0;
690
691 const double rate_prod = P.Pr_SIA[4];
692 const double rate_emit_gain = (I > 5) ? P.GII[5] * ci6 : 0.0;
693 const double rate_emit_loss = -P.GII[4] * ci5_;
694 const double rate_grow_gain = P.KII[3] * c_i1 * ci4;
695 const double rate_grow_loss = -P.KII[4] * c_i1 * ci5_;
696 const double rate_shrink_gain = (I > 5) ? P.KIV[5] * c_v1 * ci6 : 0.0;
697 const double rate_shrink_loss = -P.KIV[4] * c_v1 * ci5_;
698
699 double rate_1D_loss = 0.0;
700 if (4 < P.i_mobile && P.K_1D_pref[4] > 1e-300) {
701 for (int m = 0; m < V; ++m) {
702 const double m_f = static_cast<double>(m + 1);
703 const double m13 = std::cbrt(m_f);
704 const double denom = 1.0 + P.B_rot * P.L_hat * P.L_hat / m13;
705 const double k1d = P.K_1D_pref[4] * m13 / denom;
706 rate_1D_loss -= k1d * ci5_ * std::max(ydata[i_VAC + m], 0.0);
707 }
708 }
709
710 const double rate_sink = -P.k2_SIA[4] * ci5_;
711 const double dc_i5_total = rate_prod + rate_emit_gain + rate_emit_loss
712 + rate_grow_gain + rate_grow_loss
713 + rate_shrink_gain + rate_shrink_loss
714 + rate_1D_loss + rate_sink;
715
716 std::cerr << std::scientific << std::setprecision(3)
717 << " [ci5_rates] t=" << t_out
718 << " prod=" << rate_prod
719 << " emit_in=" << rate_emit_gain
720 << " emit_out=" << rate_emit_loss
721 << " grow_in=" << rate_grow_gain
722 << " grow_out=" << rate_grow_loss
723 << " shrink_in=" << rate_shrink_gain
724 << " shrink_out=" << rate_shrink_loss
725 << " 1D_loss=" << rate_1D_loss
726 << " sink=" << rate_sink
727 << " dc_i5=" << dc_i5_total
728 << "\n";
729 }
730
731 // ── C_v5 reaction rate breakdown (m=5, index 4) ───────────────
732 if (!is_bin && V > 4) {
733 const double cv4_ = (V > 3) ? std::max(ydata[i_VAC+3], 0.0) : 0.0;
734 const double cv5_ = c_v5;
735 const double cv6_ = (V > 5) ? std::max(ydata[i_VAC+5], 0.0) : 0.0;
736
737 const double rate_prod = P.Pr_VAC[4];
738 const double rate_emit_gain = (V > 5) ? P.GVV[5] * cv6_ : 0.0;
739 const double rate_emit_loss = -P.GVV[4] * cv5_;
740 const double rate_grow_gain = P.KVV[3] * c_v1 * cv4_;
741 const double rate_grow_loss = -P.KVV[4] * c_v1 * cv5_;
742 const double rate_shrink_gain = (V > 5) ? P.KVI[5] * c_i1 * cv6_ : 0.0;
743 const double rate_shrink_loss = -P.KVI[4] * c_i1 * cv5_;
744
745 double rate_1D_loss = 0.0;
746 if (!is_bin) {
747 const double m5f = 5.0;
748 const double m513 = std::cbrt(m5f);
749 for (int n = 4; n < std::min(I, P.i_mobile); ++n) {
750 if (P.K_1D_pref[n] < 1e-300) continue;
751 const double denom = 1.0 + P.B_rot * P.L_hat * P.L_hat / m513;
752 const double k1d = P.K_1D_pref[n] * m513 / denom;
753 rate_1D_loss -= k1d * std::max(ydata[n], 0.0) * cv5_;
754 }
755 }
756
757 const double rate_sink = -P.k2_disl_v * cv5_;
758 const double dc_v5_total = rate_prod + rate_emit_gain + rate_emit_loss
759 + rate_grow_gain + rate_grow_loss
760 + rate_shrink_gain + rate_shrink_loss
761 + rate_1D_loss + rate_sink;
762
763 std::cerr << std::scientific << std::setprecision(3)
764 << " [cv5_rates] t=" << t_out
765 << " prod=" << rate_prod
766 << " emit_in=" << rate_emit_gain
767 << " emit_out=" << rate_emit_loss
768 << " grow_in=" << rate_grow_gain
769 << " grow_out=" << rate_grow_loss
770 << " shrink_in=" << rate_shrink_gain
771 << " shrink_out=" << rate_shrink_loss
772 << " 1D_loss=" << rate_1D_loss
773 << " sink=" << rate_sink
774 << " dc_v5=" << dc_v5_total
775 << "\n";
776 }
777 }
778 }
779
780 // ── Final integrator/linsol statistics ───────────────────────────────────
781 // Emit a single machine-readable line so the Python bridge can capture
782 // GMRES iteration count + preconditioner activity into provenance.
783 {
784 long nst = 0, nfe = 0, nni = 0, ncfn = 0, netf = 0;
785 long nli = 0, npe = 0, nps = 0, nlsetup = 0;
786 CVodeGetNumSteps (cvode_mem, &nst);
787 CVodeGetNumRhsEvals (cvode_mem, &nfe);
788 CVodeGetNumNonlinSolvIters (cvode_mem, &nni);
789 CVodeGetNumNonlinSolvConvFails(cvode_mem, &ncfn);
790 CVodeGetNumErrTestFails (cvode_mem, &netf);
791 CVodeGetNumLinSolvSetups (cvode_mem, &nlsetup);
792 // GMRES-only stats — return 0 / no-op for dense/band linsols
793 CVodeGetNumLinIters (cvode_mem, &nli);
794 CVodeGetNumPrecEvals(cvode_mem, &npe);
795 CVodeGetNumPrecSolves(cvode_mem, &nps);
796 const double nli_per_nni = (nni > 0) ? double(nli) / double(nni) : 0.0;
797 std::cerr << "[stats] steps=" << nst
798 << " nfe=" << nfe
799 << " nni=" << nni
800 << " nli=" << nli
801 << " nli_per_nni=" << std::fixed << std::setprecision(2)
802 << nli_per_nni
803 << " npe=" << npe
804 << " nps=" << nps
805 << " ncfn=" << ncfn
806 << " netf=" << netf
807 << " nlsetup=" << nlsetup
808 << " interrupted=" << (was_interrupted ? 1 : 0)
809 << "\n";
810 }
811
812 std::cerr << "Done: " << n_written << " time points written"
813 << (was_interrupted ? " (interrupted)" : "") << ".\n";
814
815 // ── Cleanup ───────────────────────────────────────────────────────────────
816 if (fp_bin) std::fclose(fp_bin);
817 if (fp_win) std::fclose(fp_win);
818 N_VDestroy_Serial(y);
819 CVodeFree(&cvode_mem);
820 if (sunls) SUNLinSolFree(sunls);
821 if (sunmat) SUNMatDestroy(sunmat);
822 SUNContext_Free(&sunctx);
823
824 return 0;
825}
Parameters build_parameters(const std::map< std::string, double > &p)
Definition parameters.h:398
std::map< std::string, double > parse_param_file(const std::string &path)
Definition parameters.h:368
int prec_solve(sunrealtype t, N_Vector y, N_Vector fy, N_Vector r, N_Vector z, sunrealtype gamma, sunrealtype delta, int lr, void *user_data)
int prec_setup(sunrealtype t, N_Vector y, N_Vector fy, sunbooleantype jok, sunbooleantype *jcurPtr, sunrealtype gamma, void *user_data)
int rhs_full_CD(sunrealtype t, N_Vector y, N_Vector ydot, void *user_data)
int rhs_bin_moment(sunrealtype t, N_Vector y, N_Vector ydot, void *user_data)
void build_kernel_tables(Parameters &P)
int main(int argc, char *argv[])
Definition solver.cpp:147
static BOOL WINAPI win_console_handler(DWORD ctrl_type)
Definition solver.cpp:91
static std::map< std::string, double > parse_cli_args(int argc, char *argv[])
Definition solver.cpp:120
void posix_interrupt_handler(int)
Definition solver.cpp:104
static void install_interrupt_handlers()
Definition solver.cpp:109
int sparse_fd_jac(sunrealtype, N_Vector y, N_Vector fy, SUNMatrix Jac, void *user_data, N_Vector tmp1, N_Vector tmp2, N_Vector tmp3)
void color_columns_greedy(Parameters &P)
int build_sparsity_pattern_full_CD(Parameters &P)
void build_color_groups(Parameters &P)
bool window_active
CVRhsFn_t rhs_fn
Parameters * P