cpp_bridge.py -- Python -> C++ solver bridge for RadCluster_2_1.
Responsibilities
----------------
1. Collect pre-computed rate constants and physics parameters from
InputData / ReactionRates and write them to a temporary parameter file.
2. Invoke solver.exe with --param_file=<path>.
3. Parse the binary output and reconstruct the standard results dict.
Parameter file format
---------------------
One "key=value" entry per line. Arrays use indexed entries:
KVV_0=<val>, KVV_1=<val>, ...
New fields vs. Eurofer_CD
--------------------------
- solver_mode: integer (0=full_system, 4=active_window)
- physics_option: integer (0=full_CD_fission, 1=full_CD_fusion,
2=bin_moment_fission, 3=bin_moment_fusion)
- A_sph, A_loop, A_1D, B_rot: geometric prefactors
- trap_SIA, trap_VAC, trap_loop: solute trapping sums
- K_1D_pref_k: 1D glide prefactor for SIA cluster k
- Bin-moment parameters: I_bin, V_bin, i_discrete, v_discrete, r_ratio (computed)
- i_mobile, v_mobile: mobility cutoffs
| py_utils.cpp_bridge._make_stderr_handler |
( |
|
progress_callback, |
|
|
|
info_out = None |
|
) |
| |
|
protected |
Return a callable suitable for use as a daemon-thread target that reads
proc.stderr line by line.
When progress_callback is None -> forward each line to sys.stderr verbatim.
When progress_callback is given -> also parse [diag] / [ci5_rates] /
[cv5_rates] lines and call progress_callback(row_dict) once all three
lines for a given time step have been received.
When info_out is a dict, the handler stores any solver-emitted metadata
lines (currently `[OpenMP_threads] N`) into it under a stable key.
The row_dict passed to the callback contains atom-fraction concentrations
and atom-fraction/s rates exactly as the C++ solver computed them:
t, c_i1, c_v1, c_i2, c_v2, c_i5, c_v5, Q_tot, SIA_tot, VAC_tot
ci5_prod, ci5_emit_in, ci5_emit_out, ci5_grow_in, ci5_grow_out,
ci5_shrink_in, ci5_shrink_out, ci5_1D_loss, ci5_sink
cv5_prod, cv5_emit_in, cv5_emit_out, cv5_grow_in, cv5_grow_out,
cv5_shrink_in, cv5_shrink_out, cv5_1D_loss, cv5_sink
Definition at line 680 of file cpp_bridge.py.
| py_utils.cpp_bridge._pin_child_to_cpu_group |
( |
|
pid | ) |
|
|
protected |
Keep a solver.exe on a specific Windows processor group.
A SINGLE PIN AT SPAWN DOES NOT HOLD, which is the whole reason this is a
retry loop and not one call. Measured 2026-08-18: pinning 5 already-running
solvers moved the load immediately and permanently (group 1: 3.8 % ->
14.2 %), but the identical call issued straight after Popen left all 18
solvers of a fresh stage homed to group 0. The child's own startup -- CRT
and the OpenMP runtime, which is group-aware -- re-homes the thread after
CreateProcess returns, so a pin placed before that is simply overwritten.
So pin, then re-pin over the first seconds of the row, from a daemon thread
that never blocks the row. Rows run for hours; a few seconds on the wrong
socket costs nothing measurable.
Definition at line 50 of file cpp_bridge.py.
| py_utils.cpp_bridge._pin_once |
( |
|
pid, |
|
|
|
group |
|
) |
| |
|
protected |
One pinning pass. Returns the number of threads moved/confirmed, 0 if
the process no longer exists.
NO-OP unless RADCLUSTER_CPU_GROUP is set, so every host that does not opt in
behaves exactly as before. Never raises: failing to pin must cost a little
throughput, never a row.
WHY IT EXISTS. A Windows host with more than 64 logical CPUs is split into
processor GROUPS, and the scheduler homes threads to the group their process
was assigned at creation. On MATRIX-PC2 (2x Xeon Gold 6230, two groups of 40
logical) that is group 0 for every solver, whatever the parent did: measured
2026-08-18 under a live 20-row stage, each solver.exe had exactly ONE thread,
all 20 homed to group 0, socket 0 saturated and socket 1 at 3 %. The run_
ensemble pool workers themselves DO spread across both groups -- it is only
the solver, where all the compute is, that piles up.
The fix is one call per row. Measured immediately after moving 5 of 20 live
solver threads to group 1: group 0 50.7 % -> 37.4 %, group 1 3.8 % ->
14.2 %.
NOT a numerical knob: affinity changes where a thread runs, never what it
computes. Rows produced with and without it are directly comparable.
Definition at line 81 of file cpp_bridge.py.
| py_utils.cpp_bridge._wait_with_abort_file |
( |
|
proc, |
|
|
|
timeout_s |
|
) |
| |
|
protected |
proc.wait(timeout=timeout_s), but also abortable from OUTSIDE the process.
WHY. A detached campaign worker has no console, so Ctrl+C cannot reach it,
and on Windows Stop-Process is TerminateProcess -- uncatchable. Before this,
the only way to stop such a run was to kill it, which threw away every
in-flight row: the solver never got its interrupt, so no partial trajectory
was finalized and nothing was written. That is how a 12-row batch was lost
on 2026-08-11 after ~7 h of integration.
With this, dropping the abort file named by RADCLUSTER_ABORT_FILE asks the
solver to finish its CURRENT integration step and flush -- the same graceful
path the wall-clock budget already used, and the reason budget-cut rows keep
their dose ladder instead of vanishing. The row comes back marked partial,
with every rung it reached intact.
Raises subprocess.TimeoutExpired on the wall-clock budget, exactly as
proc.wait did, so the caller's existing handling is unchanged. With no
abort file configured this is proc.wait plus a 5 s poll, i.e. a no-op.
Definition at line 183 of file cpp_bridge.py.
| py_utils.cpp_bridge.write_param_file |
( |
|
sim, |
|
|
|
solver_config, |
|
|
|
path, |
|
|
|
y0_override = None |
|
) |
| |
Write all solver parameters to a text file (key=value, one per line).
Parameters
----------
sim : RadClusterSimulation -- fully initialised
solver_config : dict -- t_span, rtol, atol, solver_method, etc.
path : str or Path
y0_override : ndarray or None -- if provided, use these initial conditions
instead of re_obj.get_initial_conditions(). Used by
adaptive continuation to resume from a mid-run state.
Definition at line 262 of file cpp_bridge.py.