RadCluster
Graph-based cluster dynamics for irradiated materials
Loading...
Searching...
No Matches
Functions | Variables
py_utils.cpp_bridge Namespace Reference

Functions

 _pin_child_to_cpu_group (pid)
 
 _pin_once (pid, group)
 
 _send_graceful_interrupt (proc)
 
 _wait_with_abort_file (proc, timeout_s)
 
 sia100_block_length (sim, solver_config)
 
 write_param_file (sim, solver_config, path, y0_override=None)
 
 _parse_stdout (text, N_eq)
 
 _parse_kv_line (line)
 
 _make_stderr_handler (progress_callback, info_out=None)
 
 run_cpp_solver (sim, solver_config, base_dir=None, progress_callback=None, timeout_s=None, y0_override=None)
 

Variables

 log = logging.getLogger(__name__)
 
float GRACEFUL_SHUTDOWN_TIMEOUT_S = 60.0
 
float _ABORT_POLL_S = 5.0
 
dict _SOLVER_MODE_MAP
 
dict _PHYSICS_OPTION_MAP
 

Detailed Description

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

Function Documentation

◆ _make_stderr_handler()

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.

◆ _parse_kv_line()

py_utils.cpp_bridge._parse_kv_line (   line)
protected
Parse a C++ diagnostic stderr line of the form  key=value  key=value ...
Returns a dict of {str: float} for every key=value token found.

Definition at line 666 of file cpp_bridge.py.

◆ _parse_stdout()

py_utils.cpp_bridge._parse_stdout (   text,
  N_eq 
)
protected

Definition at line 652 of file cpp_bridge.py.

◆ _pin_child_to_cpu_group()

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.

◆ _pin_once()

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.

◆ _send_graceful_interrupt()

py_utils.cpp_bridge._send_graceful_interrupt (   proc)
protected
Ask the C++ subprocess to finish its current integration step and
exit cleanly via its own signal handler.  On Windows this requires the
process to have been launched with CREATE_NEW_PROCESS_GROUP.

Definition at line 161 of file cpp_bridge.py.

◆ _wait_with_abort_file()

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.

◆ run_cpp_solver()

py_utils.cpp_bridge.run_cpp_solver (   sim,
  solver_config,
  base_dir = None,
  progress_callback = None,
  timeout_s = None,
  y0_override = None 
)
Run the RadCluster_2_1 C++ solver and return the standard results dict.

Parameters
----------
sim               : RadClusterSimulation
solver_config     : dict
base_dir          : Path or None
progress_callback : callable or None
    If provided, called once per output time step with a dict of
    concentrations and rate-breakdown values (all in atom fraction / s).
    The solver's verbose mode is automatically enabled.
timeout_s         : float or None
    Maximum wall-clock seconds to allow for the C++ solver.  If exceeded,
    the process is killed and None is returned.
y0_override       : ndarray or None
    Custom initial conditions for adaptive continuation runs.

Returns
-------
dict or None

Definition at line 762 of file cpp_bridge.py.

◆ sia100_block_length()

py_utils.cpp_bridge.sia100_block_length (   sim,
  solver_config 
)
Length of the appended ⟨100⟩ SIA block in the C++ state vector.

The ⟨100⟩ block carries the SAME size reduction as the ½⟨111⟩ population:
full per-size (length I) in discrete modes, and the discrete-prefix +
bin-moment vector (i_discrete + n_mom·I_bin) in bin_moment modes.  Zero when
loop conversion is off.

Shared by ``write_param_file`` (which must emit a y0 of the full width) and
``run_cpp_solver`` (which splits the block back off).  Keeping one rule in
one place is deliberate: when the two disagreed, the writer emitted a short
y0 and the reader still found a block, so the mismatch was invisible.

Definition at line 238 of file cpp_bridge.py.

◆ write_param_file()

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.

Variable Documentation

◆ _ABORT_POLL_S

float py_utils.cpp_bridge._ABORT_POLL_S = 5.0
protected

Definition at line 180 of file cpp_bridge.py.

◆ _PHYSICS_OPTION_MAP

dict py_utils.cpp_bridge._PHYSICS_OPTION_MAP
protected
Initial value:
1= {
2 'full_CD_fission': 0,
3 'full_CD_fusion': 1,
4 'bin_moment_CD_fission': 2,
5 'bin_moment_CD_fusion': 3,
6}

Definition at line 230 of file cpp_bridge.py.

◆ _SOLVER_MODE_MAP

dict py_utils.cpp_bridge._SOLVER_MODE_MAP
protected
Initial value:
1= {
2 'full_system': 0,
3 'active_window': 4,
4 # Backward-compat aliases (legacy names)
5 'cpp_full': 0,
6 'sliding_OpenMP': 4,
7}

Definition at line 223 of file cpp_bridge.py.

◆ GRACEFUL_SHUTDOWN_TIMEOUT_S

float py_utils.cpp_bridge.GRACEFUL_SHUTDOWN_TIMEOUT_S = 60.0

Definition at line 47 of file cpp_bridge.py.

◆ log

py_utils.cpp_bridge.log = logging.getLogger(__name__)

Definition at line 40 of file cpp_bridge.py.