82 """One pinning pass. Returns the number of threads moved/confirmed, 0 if
83 the process no longer exists.
85 NO-OP unless RADCLUSTER_CPU_GROUP is set, so every host that does not opt in
86 behaves exactly as before. Never raises: failing to pin must cost a little
87 throughput, never a row.
89 WHY IT EXISTS. A Windows host with more than 64 logical CPUs is split into
90 processor GROUPS, and the scheduler homes threads to the group their process
91 was assigned at creation. On MATRIX-PC2 (2x Xeon Gold 6230, two groups of 40
92 logical) that is group 0 for every solver, whatever the parent did: measured
93 2026-08-18 under a live 20-row stage, each solver.exe had exactly ONE thread,
94 all 20 homed to group 0, socket 0 saturated and socket 1 at 3 %. The run_
95 ensemble pool workers themselves DO spread across both groups -- it is only
96 the solver, where all the compute is, that piles up.
98 The fix is one call per row. Measured immediately after moving 5 of 20 live
99 solver threads to group 1: group 0 50.7 % -> 37.4 %, group 1 3.8 % ->
102 NOT a numerical knob: affinity changes where a thread runs, never what it
103 computes. Rows produced with and without it are directly comparable.
107 from ctypes
import wintypes
109 class _THREADENTRY32(ctypes.Structure):
110 _fields_ = [(
"dwSize", wintypes.DWORD), (
"cntUsage", wintypes.DWORD),
111 (
"th32ThreadID", wintypes.DWORD),
112 (
"th32OwnerProcessID", wintypes.DWORD),
113 (
"tpBasePri", ctypes.c_long),
114 (
"tpDeltaPri", ctypes.c_long),
115 (
"dwFlags", wintypes.DWORD)]
117 class _GROUP_AFFINITY(ctypes.Structure):
118 _fields_ = [(
"Mask", ctypes.c_size_t), (
"Group", ctypes.c_ushort),
119 (
"Reserved", ctypes.c_ushort * 3)]
121 k = ctypes.WinDLL(
"kernel32", use_last_error=
True)
122 k.OpenThread.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
123 k.OpenThread.restype = wintypes.HANDLE
124 k.SetThreadGroupAffinity.argtypes = [wintypes.HANDLE,
125 ctypes.POINTER(_GROUP_AFFINITY),
126 ctypes.POINTER(_GROUP_AFFINITY)]
127 k.GetActiveProcessorCount.argtypes = [ctypes.c_ushort]
128 k.GetActiveProcessorCount.restype = wintypes.DWORD
131 n_cpu = int(k.GetActiveProcessorCount(ctypes.c_ushort(g)))
134 mask = (1 << n_cpu) - 1
136 snap = k.CreateToolhelp32Snapshot(0x00000004, 0)
137 te = _THREADENTRY32()
138 te.dwSize = ctypes.sizeof(_THREADENTRY32)
140 ok = k.Thread32First(snap, ctypes.byref(te))
142 if te.th32OwnerProcessID == pid:
144 h = k.OpenThread(0x0020 | 0x0800,
False, te.th32ThreadID)
146 ga = _GROUP_AFFINITY(Mask=mask, Group=g)
147 if k.SetThreadGroupAffinity(h, ctypes.byref(ga),
None):
150 ok = k.Thread32Next(snap, ctypes.byref(te))
153 logging.debug(
"pinned solver pid %s (%d thread(s)) to CPU group %d",
156 except Exception
as exc:
157 logging.debug(
"CPU-group pin skipped for pid %s: %s", pid, exc)
264 Write all solver parameters to a text file (key=value, one per line).
268 sim : RadClusterSimulation -- fully initialised
269 solver_config : dict -- t_span, rtol, atol, solver_method, etc.
271 y0_override : ndarray or None -- if provided, use these initial conditions
272 instead of re_obj.get_initial_conditions(). Used by
273 adaptive continuation to resume from a mid-run state.
276 rr = sim.reaction_rates
277 re_obj = sim.rate_equations
280 method = solver_config.get(
'solver_method', {})
288 lines.append(f
"I={I}")
289 lines.append(f
"V={V}")
291 lines.append(f
"N={I}")
292 lines.append(f
"M={V}")
293 lines.append(f
"Ni={I}")
294 lines.append(f
"Nv={V}")
295 lines.append(f
"Ni_max={I}")
298 sm_int = _SOLVER_MODE_MAP.get(inp.solver_mode, 0)
299 po_int = _PHYSICS_OPTION_MAP.get(inp.physics_option, 0)
300 lines.append(f
"physics_option_int={po_int}")
301 lines.append(f
"window_mode={sm_int}")
304 lines.append(f
"A_sph={d['A_sph']:.17e}")
305 lines.append(f
"A_loop={d['A_loop']:.17e}")
306 lines.append(f
"B_rot={d['B_rot']:.17e}")
307 lines.append(f
"L_hat={d['L_hat']:.17e}")
310 lines.append(f
"i_mobile={d['i_mobile']}")
311 lines.append(f
"v_mobile={d['v_mobile']}")
313 lines.append(f
"n_max_i={d['i_mobile']}")
314 lines.append(f
"m_max_v={d['v_mobile']}")
318 bf = d.get(
'boundary_flux',
'absorption')
319 lines.append(f
"boundary_flux={1 if bf == 'reflection' else 0}")
322 for k, v
in enumerate(rr.K_VAC_grow):
323 lines.append(f
"KVV_{k}={v:.17e}")
324 for k, v
in enumerate(rr.K_VAC_shrink):
325 lines.append(f
"KVI_{k}={v:.17e}")
326 for k, v
in enumerate(rr.G_VAC):
327 lines.append(f
"GVV_{k}={v:.17e}")
328 for k, v
in enumerate(rr.K_HeV):
329 lines.append(f
"KHeV_{k}={v:.17e}")
330 for k, v
in enumerate(re_obj.Pr_VAC):
331 lines.append(f
"Pr_VAC_{k}={v:.17e}")
335 lines.append(f
"m13_{k}={m**(1.0/3.0):.17e}")
338 for k, v
in enumerate(rr.K_SIA_grow):
339 lines.append(f
"KII_{k}={v:.17e}")
340 for k, v
in enumerate(rr.K_SIA_shrink):
341 lines.append(f
"KIV_{k}={v:.17e}")
347 _zarr = getattr(rr,
"Z_i_loop_arr",
None)
348 if _zarr
is not None:
349 for k, v
in enumerate(_zarr):
350 lines.append(f
"Z_i_loop_arr_{k}={v:.17e}")
351 for k, v
in enumerate(rr.G_SIA):
352 lines.append(f
"GII_{k}={v:.17e}")
353 for k, v
in enumerate(rr.k2_SIA):
354 lines.append(f
"k2_SIA_{k}={v:.17e}")
355 for k, v
in enumerate(re_obj.Pr_SIA):
356 lines.append(f
"Pr_SIA_{k}={v:.17e}")
359 for k, v
in enumerate(rr.K_1D_pref):
360 lines.append(f
"K_1D_pref_{k}={v:.17e}")
363 for k, v
in enumerate(rr.D_SIA_eff):
364 lines.append(f
"D_SIA_eff_{k}={v:.17e}")
365 for k, v
in enumerate(rr.D_VAC_eff):
366 lines.append(f
"D_VAC_eff_{k}={v:.17e}")
371 _emission = sim.input_data.reactions.get(
'sia_emission_monomer',
'returned')
372 if _emission
not in (
'returned',
'dropped'):
373 raise ValueError(f
"sia_emission_monomer must be 'returned' or 'dropped', not {_emission!r}")
374 lines.append(f
"sia_emission_monomer={1 if _emission == 'returned' else 0}")
375 _lc = int(getattr(rr,
'loop_coal', 0))
376 lines.append(f
"loop_coal={_lc}")
378 for k, v
in enumerate(rr.D_loop_coal):
379 lines.append(f
"D_loop_coal_{k}={v:.17e}")
380 lines.append(f
"A_sph_inv_O23={rr.A_sph_inv_O23:.17e}")
381 lines.append(f
"A_loop_inv_O23={rr.A_loop_inv_O23:.17e}")
384 _Z_i = float(inp.reactions.get(
'Z_i', 1.05))
385 Z_i_loop = float(inp.reactions.get(
'Z_i_loop', _Z_i)
or _Z_i)
386 lines.append(f
"Z_i_loop={Z_i_loop:.17e}")
387 Z_ii = float(inp.reactions.get(
'Z_ii', 1.0))
388 lines.append(f
"Z_ii={Z_ii:.17e}")
394 loop_conv = int(solver_config.get(
395 'loop_conversion', inp.reactions.get(
'loop_conversion', 0)))
396 lines.append(f
"loop_conversion={loop_conv}")
398 lines.append(f
"n_loop_min={int(getattr(rr, 'n_loop_min', 4))}")
402 from .reaction_rates
import effective_n_j_min
403 lines.append(
"n_j_min_junc=%d" % int(effective_n_j_min(
404 inp.reactions.get(
'n_j_min_junc', 30),
406 inp.reactions.get(
'n_j_min_frac', 0.6))))
407 lines.append(f
"phi_max_junc={float(inp.reactions.get('phi_max_junc', 0.5)):.17e}")
408 lines.append(f
"sigma_s_junc={float(inp.reactions.get('sigma_s_junc', 0.35)):.17e}")
411 lines.append(f
"loop_conv_psuccess={float(getattr(rr, 'conv_psuccess', 1.0)):.17e}")
418 lines.append(
"loop_conv_psuccess_abs=%.17e" % float(
419 getattr(rr,
'conv_psuccess_abs',
420 getattr(rr,
'conv_psuccess', 1.0))))
424 lines.append(
"conv_absorb_boost=%.17e" % float(
425 inp.reactions.get(
'absorb_boost_100', 1.0)
or 1.0))
426 for k, v
in enumerate(rr.Gamma_uni):
427 lines.append(f
"Gamma_uni_{k}={v:.17e}")
428 for k, v
in enumerate(rr.K_100_grow):
429 lines.append(f
"K_100_grow_{k}={v:.17e}")
430 for k, v
in enumerate(rr.K_100_shrink):
431 lines.append(f
"K_100_shrink_{k}={v:.17e}")
432 for k, v
in enumerate(rr.G_100):
433 lines.append(f
"G_100_{k}={v:.17e}")
438 lam100 = getattr(rr,
'Lambda_net_100',
None)
439 if lam100
is not None:
440 for k, v
in enumerate(lam100):
441 lines.append(f
"lambda_net_100_{k}={v:.17e}")
447 lam_void = getattr(rr,
'Lambda_net_void',
None)
448 if lam_void
is not None and float(np.max(lam_void)) > 0.0:
449 for k, v
in enumerate(lam_void):
450 lines.append(f
"lambda_net_void_{k}={v:.17e}")
453 kBT = float(d[
'kBT'])
454 nu_h = float(inp.energetics.get(
'nu_h', 3.0e12))
455 E_m_h = float(inp.energetics.get(
'E_m_h', 0.06))
456 E_b_hV1 = float(inp.energetics.get(
'E_b_hV_1', 2.30))
457 beta_He = nu_h * np.exp(-(E_b_hV1 + E_m_h) / kBT)
466 from .binding_energies
import _B2, _B3
467 Omega = float(d[
'Omega'])
468 _x = np.geomspace(0.05, 2.0, 40)
469 _PdV = _x * kBT * (1.0 + _B2 * _x / Omega + _B3 * (_x / Omega) ** 2)
470 _slope, _icept = np.polyfit(np.log(_x), np.log(_PdV), 1)
471 beta_He_exp = float(_slope)
472 delta_He = float(np.exp(_icept) / beta_He_exp)
475 f
"G_He={re_obj.G_He:.17e}",
476 f
"k2_disl_v={rr.k2_vac_scalar:.17e}",
477 f
"k2_disl_i={rr.k2_SIA_scalar:.17e}",
478 f
"k2_disl_He={rr.k2_He_scalar:.17e}",
479 f
"Cv_eq={d['Cv_eq']:.17e}",
480 f
"beta_He={beta_He:.17e}",
481 f
"delta_He={delta_He:.17e}",
482 f
"beta_He_exp={beta_He_exp:.17e}",
484 f
"K_iv={rr.K_iv:.17e}",
485 f
"K_3D_cav_pref={rr.K_3D_cav_pref:.17e}",
489 I_bin = getattr(re_obj,
'I_bin', getattr(re_obj,
'K', 0))
490 V_bin = getattr(re_obj,
'V_bin', getattr(re_obj,
'K_v', 0))
491 i_discrete = getattr(re_obj,
'i_discrete', getattr(re_obj,
'n1', 1))
492 v_discrete = getattr(re_obj,
'v_discrete', 1)
493 r_ratio = getattr(re_obj,
'r', 2.0)
495 lines.append(f
"I_bin={I_bin}")
496 lines.append(f
"V_bin={V_bin}")
497 lines.append(f
"i_discrete={i_discrete}")
498 lines.append(f
"v_discrete={v_discrete}")
499 lines.append(f
"r_ratio={r_ratio:.17e}")
501 lines.append(f
"K_bins={I_bin}")
502 lines.append(f
"K_v_bins={V_bin}")
503 lines.append(f
"n1_bin={i_discrete}")
511 sia_bins = list(getattr(re_obj,
'bins', [])
or [])
512 vac_bins = list(getattr(re_obj,
'vac_bins', [])
or [])
513 for k, (nlo, nhi)
in enumerate(sia_bins):
514 lines.append(f
"sia_bin_lo_{k}={int(nlo)}")
515 lines.append(f
"sia_bin_hi_{k}={int(nhi)}")
516 for k, (mlo, mhi)
in enumerate(vac_bins):
517 lines.append(f
"vac_bin_lo_{k}={int(mlo)}")
518 lines.append(f
"vac_bin_hi_{k}={int(mhi)}")
521 _sf_map = {
'constant': 0,
'linear': 1,
'lognormal': 2}
522 sf = getattr(re_obj,
'shape_function',
'linear')
523 lines.append(f
"shape_function={_sf_map.get(sf, 1)}")
526 he_mode_int = 0
if getattr(re_obj,
'he_mode',
'case2') ==
'case2' else 1
527 lines.append(f
"he_mode={he_mode_int}")
537 n_expected = re_obj.N_eq + n_sia100
538 if y0_override
is None:
548 C_floor_ic = float(inp.reactions.get(
'C_floor', 1e-15))
549 blk = np.full(n_sia100, C_floor_ic)
550 if n_sia100
and hasattr(re_obj,
'bins'):
551 i_d, Pm = int(re_obj.i_discrete), int(re_obj.n_mom)
552 for k, (nlo, nhi)
in enumerate(re_obj.bins):
553 mid = 0.5 * (nlo + nhi - 1)
556 blk[idx + 1] = C_floor_ic * mid
558 blk[idx + 2] = C_floor_ic * mid * mid
559 y0 = np.concatenate([re_obj.get_initial_conditions(), blk])
561 y0 = np.asarray(y0_override, dtype=float)
563 if y0.size != n_expected:
565 f
"write_param_file: y0_override has {y0.size} entries but the "
566 f
"C++ state vector is {n_expected} wide (N_eq={re_obj.N_eq} + "
567 f
"{n_sia100} for the appended ⟨100⟩ block). A short y0 would "
568 "be silently filled with 1e-100, erasing the ⟨100⟩ population. "
569 "Resume via RadClusterSimulation._resume_state / _expand_state, "
570 "which re-attach the block.")
571 for k, v
in enumerate(y0):
572 lines.append(f
"y0_{k}={v:.17e}")
574 C_floor = float(inp.reactions.get(
'C_floor', 1e-15))
575 lines.append(f
"C_floor={C_floor:.17e}")
578 he_kinetics_str = str(inp.reactions.get(
'he_kinetics',
'dynamic')).lower()
579 qss_He_int = 1
if he_kinetics_str ==
'quasi_steady_state' else 0
580 lines.append(f
"qss_He={qss_He_int}")
583 t_span = solver_config.get(
't_span', (1e-8, 1e7))
584 lines.append(f
"t_begin={t_span[0]:.17e}")
585 lines.append(f
"t_end={t_span[1]:.17e}")
586 lines.append(f
"n_points={int(solver_config.get('n_points', 200))}")
587 lines.append(f
"log_time={1.0 if solver_config.get('log_time', True) else 0.0}")
588 lines.append(f
"rtol={solver_config.get('rtol', 1e-8):.17e}")
589 lines.append(f
"atol={solver_config.get('atol', 1e-50):.17e}")
593 _linsol_map = {
'dense': 0,
'band': 1,
'banded': 1,
'gmres': 2,
'klu': 3}
596 lines.append(f
"linsol={_linsol_map.get(str(method.get('linsol','dense')).lower(), 0)}")
597 lines.append(f
"mu={int(method.get('mu', N_tot - 1))}")
598 lines.append(f
"ml={int(method.get('ml', N_tot - 1))}")
599 lines.append(f
"max_order={int(method.get('max_order', 0))}")
600 lines.append(f
"hmin={float(method.get('hmin', 0.0)):.17e}")
601 lines.append(f
"hmax={float(method.get('hmax', 0.0)):.17e}")
605 lines.append(f
"window_width={int(method.get('window_width', max(I, V)))}")
606 lines.append(f
"concentration_threshold={float(method.get('concentration_threshold', 1e-18)):.17e}")
607 lines.append(f
"window_pad={int(method.get('window_pad', 10))}")
609 lines.append(f
"window_pad_v={int(method.get('window_pad_v', method.get('window_pad', 10)))}")
610 lines.append(f
"window_check_every={int(method.get('window_check_every', 1))}")
614 linsol_int = _linsol_map.get(str(method.get(
'linsol',
'dense')).lower(), 0)
615 window_mode_int = int(method.get(
'window_mode', 0))
618 prec_type_default = 1
if (linsol_int == 2
and window_mode_int == 0)
else 0
626 if 'preconditioner' in method:
627 key = str(method[
'preconditioner']).strip().lower()
628 if key
not in _prec_name_map:
630 f
"Unknown preconditioner='{method['preconditioner']}'. "
631 f
"Use 'Jacobi' or 'Woodbury'.")
632 prec_type_value = _prec_name_map[key]
634 prec_type_value = int(method.get(
'prec_type', prec_type_default))
635 lines.append(f
"prec_type={prec_type_value}")
637 prec_bw_default = max(2 * d[
'i_mobile'], 2 * d[
'v_mobile']) + 1
638 lines.append(f
"prec_bw={int(method.get('prec_bw', prec_bw_default))}")
640 prec_rank_default = d[
'i_mobile'] + d[
'v_mobile']
641 lines.append(f
"prec_rank={int(method.get('prec_rank', prec_rank_default))}")
643 verbose = 1
if solver_config.get(
'_verbose',
False)
else 0
644 lines.append(f
"verbose={verbose}")
646 with open(path,
'w')
as f:
647 f.write(
'\n'.join(lines) +
'\n')
763 timeout_s=None, y0_override=None):
765 Run the RadCluster_2_1 C++ solver and return the standard results dict.
769 sim : RadClusterSimulation
771 base_dir : Path or None
772 progress_callback : callable or None
773 If provided, called once per output time step with a dict of
774 concentrations and rate-breakdown values (all in atom fraction / s).
775 The solver's verbose mode is automatically enabled.
776 timeout_s : float or None
777 Maximum wall-clock seconds to allow for the C++ solver. If exceeded,
778 the process is killed and None is returned.
779 y0_override : ndarray or None
780 Custom initial conditions for adaptive continuation runs.
786 from .post_process
import calculate_derived_quantities
789 base_dir = Path(__file__).resolve().parent.parent
792 if progress_callback
is not None:
793 solver_config = dict(solver_config)
794 solver_config[
'_verbose'] =
True
796 exe_name =
'solver.exe' if sys.platform ==
'win32' else 'solver'
797 build_dir = Path(base_dir) /
'build'
798 exe_path = build_dir /
'Release' / exe_name
799 if not exe_path.exists():
800 exe_path = build_dir /
'Debug' / exe_name
801 if not exe_path.exists():
802 exe_path = build_dir / exe_name
804 if not exe_path.exists():
805 print(f
"C++ solver not found at {build_dir}")
806 print(
" Build with:")
807 print(f
" cd {Path(base_dir) / 'cpp_utils'}")
808 print(
" cmake -S . -B ../build -DCMAKE_BUILD_TYPE=Release")
809 print(
" cmake --build ../build --config Release")
812 with tempfile.NamedTemporaryFile(mode=
'w', suffix=
'.txt',
813 delete=
False, prefix=
'expanded_cd_')
as tf:
816 bin_path = param_path[:-4] +
'.bin'
817 re_obj = sim.rate_equations
826 _is_bin = hasattr(re_obj,
'bins')
833 print(f
"C++ solver: {exe_path.name} N_eq={N_tot}"
834 f
" solver_mode='{sim.input_data.solver_mode}'"
835 f
" physics='{sim.input_data.physics_option}'")
838 stdout=subprocess.PIPE,
839 stderr=subprocess.PIPE,
843 if sys.platform ==
'win32':
844 popen_kwargs[
'creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP
846 proc = subprocess.Popen(
847 [str(exe_path), f
'--param_file={param_path}'],
854 t_fwd = threading.Thread(target=stderr_fn, args=(proc.stderr,), daemon=
True)
862 def _drain_stdout(stream, buf):
864 buf.append(stream.read())
867 t_out = threading.Thread(target=_drain_stdout,
868 args=(proc.stdout, stdout_buf), daemon=
True)
873 except subprocess.TimeoutExpired:
874 print(f
"C++ solver hit {timeout_s}s timeout -- asking it to finalize gracefully...")
878 proc.wait(timeout=GRACEFUL_SHUTDOWN_TIMEOUT_S)
879 except subprocess.TimeoutExpired:
880 print(f
" Solver did not finalize within "
881 f
"{GRACEFUL_SHUTDOWN_TIMEOUT_S:.0f}s -- forcing kill.")
884 t_fwd.join(timeout=5)
885 t_out.join(timeout=5)
886 stdout_data = b
''.join(stdout_buf)
if stdout_buf
else b
''
887 except KeyboardInterrupt:
888 print(
"\n*** Ctrl+C -- asking C++ solver to flush and exit gracefully ***")
896 proc.wait(timeout=GRACEFUL_SHUTDOWN_TIMEOUT_S)
897 except (subprocess.TimeoutExpired, KeyboardInterrupt):
898 print(f
" Solver did not exit within "
899 f
"{GRACEFUL_SHUTDOWN_TIMEOUT_S:.0f}s -- forcing kill.")
906 t_fwd.join(timeout=5)
907 t_out.join(timeout=5)
908 stdout_data = b
''.join(stdout_buf)
if stdout_buf
else b
''
913 os.unlink(param_path)
917 if proc
is not None and not partial
and proc.returncode != 0:
918 print(f
"C++ solver failed (exit code {proc.returncode})")
926 sim._partial_results =
None
932 bin_size = os.path.getsize(bin_path)
935 print(f
"Reading bin file: {bin_path} ({bin_size} bytes)")
936 raw = np.fromfile(bin_path, dtype=np.float64)
938 n_rows = raw.size // n_cols
942 sol_arr = raw[:n_rows * n_cols].reshape(n_rows, n_cols)
944 print(f
" parsed {raw.size} doubles -> {n_rows} rows x {n_cols} cols")
947 except Exception
as exc:
949 print(f
" bin parse failed: {type(exc).__name__}: {exc}")
958 if sol_arr
is None or sol_arr.shape[0] == 0:
960 text = stdout_data.decode(
'utf-8', errors=
'replace')
963 if sol_arr
is None or sol_arr.shape[0] == 0:
964 print(
"C++ solver produced no parseable output")
971 final_stats = solver_info.get(
'solver_stats_final')
or {}
972 if final_stats.get(
'interrupted'):
975 status =
"partial" if partial
else "completed"
976 n_pts = sol_arr.shape[0]
977 print(f
"C++ solver {status} -- {n_pts} time points")
998 y_sia100 = y[-_n_sia100:, :]
999 y = y[:-_n_sia100, :]
1001 from .bin_moment_rates
import reconstruct_distribution
1002 from .post_process
import _floor_bin_moments
1003 _i_d = int(getattr(re_obj,
'i_discrete', 0))
1004 _Pm = int(getattr(re_obj,
'n_mom', 2))
1005 _sf = getattr(re_obj,
'shape_function',
'linear')
1008 _Ifull = int(sim.input_data.I)
1009 _npts = y_sia100.shape[1]
1010 _Cf = float(sim.input_data.reactions.get(
'C_floor', 1e-15))
1011 _c100 = np.zeros((_Ifull, _npts))
1034 for _j
in range(_npts):
1035 _col = np.maximum(y_sia100[:, _j], 0.0)
1036 _c = np.zeros(_Ifull)
1037 _c[:_i_d] = np.maximum(_col[:_i_d] - _Cf, 0.0)
1039 _mom = _col[_i_d:_i_d + _Pm * _Ibin]
1040 _mu0 = _mom[0::_Pm][:_Ibin].astype(float).copy()
1041 _mu1 = (_mom[1::_Pm][:_Ibin].astype(float).copy()
1042 if _Pm >= 2
else None)
1043 _mu2 = _mom[2::_Pm][:_Ibin]
if _Pm >= 3
else None
1044 for _kb, (_nlo, _nhi)
in enumerate(_bins):
1045 _m0, _m1 = _floor_bin_moments(
1046 _mu0[_kb], _mu1[_kb]
if _Pm >= 2
else 0.0,
1047 _nlo, _nhi, _Cf, _Pm)
1051 _rec = reconstruct_distribution(_sf, _mu0, _mu1, _mu2,
1053 _c[_i_d:] = _rec[_i_d:]
1055 y_sia100_full = _c100
1059 y_sia100_full = y_sia100
1061 results = calculate_derived_quantities(t, y, sim.input_data, re_obj,
1064 if y_sia100_full
is not None:
1065 results[
'y_sia100'] = y_sia100_full
1066 if y_sia100
is not None:
1075 results[
'y_sia100_raw'] = y_sia100
1082 win_path = bin_path +
'.window.csv'
1084 if os.path.exists(win_path):
1085 wdata = np.genfromtxt(win_path, delimiter=
',', skip_header=1)
1087 wdata = wdata.reshape(1, -1)
1088 n_w = min(wdata.shape[0], len(t))
1091 results[
'n_active_sia'] = (wdata[:n_w, 1].astype(int) + 1)
1092 results[
'n_active_vac'] = (wdata[:n_w, 2].astype(int) + 1)
1093 results[
'n_active'] = (results[
'n_active_sia']
1094 + results[
'n_active_vac'])
1095 except Exception
as exc:
1096 print(f
" window sidecar parse failed: {type(exc).__name__}: {exc}")
1105 sim._partial_results = results
1106 except KeyboardInterrupt:
1107 print(
"\n*** Ctrl+C during result post-processing -- "
1108 "returning whatever was rescued. ***")
1109 return sim._partial_results
1112 sm = sim.input_data.solver_mode
1113 po = sim.input_data.physics_option
1114 linsol = str(solver_config.get(
'solver_method', {}).get(
'linsol',
'dense')).upper()
1115 msg = f
'C++ CVODE BDF {sm}/{po} / {linsol}'
1117 msg +=
' (partial -- interrupted)'
1118 results[
'metadata'] = {
1122 'n_time_points': n_pts,
1123 'partial': bool(partial),
1125 'omp_threads_used': solver_info.get(
'omp_threads_used'),
1126 'solver_stats_final': final_stats
or None,
1128 print(
"Results processing complete.")
1129 except KeyboardInterrupt:
1130 print(
"\n*** Ctrl+C during metadata stamping -- "
1131 "returning rescued results without metadata. ***")