Mock Solver & REST API Decoupling Architecture
Table of Contents
- Overview
- Architecture
- REST API Contract
- Simplified PWR Model
- Parameter Mapping
- Trip Evaluation
- Timestep Management — The Two-Clock Architecture
- Checkpoint/Restore Mechanism
- Engine Integration — runSequence Flow
- MPI Parallel Execution
- Intervention Pipeline
- Key Design Decisions
- Verification Results
- Benchmarks
- Extending for Real T-H Codes
1. Overview
The engine couples to RELAP5/MOD3.3 in-process, through a shared Fortran/C++ struct (r5pars_) and Fortran callback functions (interfc_(), actonce_(), etc.). That coupling needs a Fortran compiler and the RELAP5 source code. Two abstraction layers let the engine run without them:
- The abstract
ThermalHydraulicSolverandSolverCallbackinterfaces isolate the engine from any specific solver implementation. - A standalone mock T-H solver process communicates with the engine through an HTTP REST API, so the solver runs in a separate process.
The mock solver is not a replay tool. It is a driveable lumped-parameter PWR model that responds to crew interventions and produces physically plausible transient behavior. This is necessary because ADS-IDAC's dynamic event tree creates different sequences based on crew decisions at branching points.
2. Architecture
2.1 Two-Process Design
┌─────────────────────────┐ HTTP/REST ┌──────────────────────────┐
│ ADS-IDAC Engine │ ◄─────────────────► │ Mock T-H Solver │
│ │ │ (standalone process) │
│ ┌───────────────────┐ │ POST /advance │ ┌────────────────────┐ │
│ │ RestApiSolver │───┼──────────────────────►│ │ SimplifiedPWR │ │
│ │ (TH interface) │ │ POST /intervene │ │ (state-space ODE) │ │
│ └───────┬───────────┘ │ POST /checkpoint │ ├────────────────────┤ │
│ │ │ POST /restore │ │ TripEvaluator │ │
│ ┌───────▼───────────┐ │ POST /sessions │ ├────────────────────┤ │
│ │ AdsidacCallback │ │ DELETE /sessions/{id} │ │ ParameterMapper │ │
│ │ (solver-agnostic)│ │ │ ├────────────────────┤ │
│ └───────────────────┘ │ │ │ CheckpointStore │ │
│ │ │ └────────────────────┘ │
│ r5pars_ arrays ◄────── populated from JSON │ │
└─────────────────────────┘ └──────────────────────────┘The engine process (adsidac) and the solver process (mock-solver) communicate exclusively through HTTP. The engine's RestApiSolver translates solver interface calls into REST requests, and writes the JSON response data into the same r5pars_ arrays that SystemModule::updateParametersValues() reads. The branching, crew and procedure logic is the same code whether it is driven by RELAP5 or by the mock.
2.2 Solver Interface Hierarchy
2.3 File Layout
mock-solver/ # Standalone process (separate CMake)
CMakeLists.txt
main.cpp # HTTP server entry point
src/
MockServer.h/.cpp # HTTP routing + session management
SimplifiedPWR.h/.cpp # State-space PWR model + intervention handling
TripEvaluator.h/.cpp # Threshold trip logic (51 VT + 31 LT)
ParameterMapper.h/.cpp # State → 310 output parameters
CheckpointStore.h/.cpp # In-memory checkpoint storage (global)
src/ADSIDAC/Solver/
ThermalHydraulicSolver.h # Abstract interface
RelapSolver.h/.cpp # RELAP5 Fortran wrapper
RestApiSolver.h/.cpp # REST API client
AdsidacCallback.h/.cpp # Engine-side callback implementations
third_party/
httplib.h # cpp-httplib (header-only HTTP)
json.hpp # nlohmann/json (header-only JSON)2.4 Build
# Build mock solver (standalone, no Fortran dependency)
cd mock-solver && mkdir -p build && cd build
cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release
ninja
# Build adsidac (includes RestApiSolver). On macOS use the Homebrew
# gfortran-<NN> that is installed.
cd ../../build
cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_COMPILER=clang++ -DCMAKE_C_COMPILER=clang \
-DCMAKE_Fortran_COMPILER=gfortran-16 -DENABLE_HCL=OFF
ninja
# Run (two terminals or background the mock server)
./mock-solver/build/bin/mock-solver --port 8080 &
./build/bin/adsidac --project-dir=tests/BV4 \
--input-deck=tests/BV4/BV4.i --solver=mock --solver-port=8080
# MPI parallel (mock-solver shared by all ranks)
mpirun -n 4 ./build/bin/adsidac --project-dir=tests/BV4 \
--input-deck=tests/BV4/BV4.i --solver=mock --solver-port=80803. REST API Contract
Session-based API. The solver manages T-H state; ADS-IDAC manages branching and crew logic.
3.1 Endpoints
| Method | Endpoint | Purpose |
|---|---|---|
POST | /api/v1/sessions | Create a new solver session |
POST | /api/v1/sessions/{id}/advance | Advance simulation by dt seconds |
POST | /api/v1/sessions/{id}/intervene | Apply crew actions |
POST | /api/v1/sessions/{id}/checkpoint | Snapshot current state |
POST | /api/v1/sessions/{id}/restore | Restore from checkpoint |
DELETE | /api/v1/sessions/{id} | Destroy session |
GET | /api/v1/sessions/{id}/state | Query current state |
GET | /api/v1/health | Health check |
3.2 Advance Response Schema
The /advance response is the core data contract. It returns 310 parameters matching the r5pars_ array layout:
{
"time": 5.5,
"stop_flag": 0,
"volumes": [
{ "index": 0, "liq_t": 590.2, "vap_t": 590.2, "sat_t": 589.1,
"pressure": 2250.0, "boron_d": 0.0, "quality": 0.0, "flow_reg": 1.0 },
... // 20 volumes
],
"junctions": [
{ "index": 0, "mass_fr": 100.5, "liq_vel": 15.2, "vap_vel": 0.0 },
... // 24 junctions
],
"variable_trips": [
{ "index": 0, "trip_time": -1.0 },
... // 51 variable trips
],
"logical_trips": [
{ "index": 0, "trip_time": -1.0 },
... // 31 logical trips
],
"control_variables": [
{ "index": 0, "value": 100.0 },
... // 78 control variables
]
}Total: 20×7 + 24×3 + 51 + 31 + 78 = 372 individual values per advance call.
3.3 Intervention Request Schema
{
"actions": [
{ "index": 30, "value": 1.0 },
{ "index": 1, "value": 1.0 }
]
}Indices are 0-based offsets from the IC_801 card number in RELAP5_channels.txt. Only changed values are sent (delta encoding).
3.4 Session Lifecycle
4. Simplified PWR Model
4.1 Design Philosophy
The model uses lumped-parameter ODEs — not conservation equations. Each physical subsystem is represented by 1-3 state variables with empirically tuned coupling coefficients. The goal is API validation and Fortran-free testing, not physics accuracy. The model must be:
- Driveable: respond to crew interventions (42 interactive variables)
- Plausible: produce qualitatively correct transient trends
- Fast: sub-millisecond per advance step (no Fortran, no matrix solves)
4.2 State Vector
struct PWRState {
double time = 0.0; // simulation time [s]
// --- Primary system ---
double core_power = 1.0; // normalized (1.0 = 100%)
double pzr_pressure = 2250.0; // psia
double pzr_level = 0.475; // fraction (0-1)
double loop_thot[3]; // hot leg temps [°F], loops A/B/C (init: 590)
double tcold = 545.0; // cold leg temp [°F]
double rcs_flow_frac = 1.0; // fraction of nominal RCS flow
// --- Secondary system (per SG) ---
double sg_pressure[3]; // psia (init: 850)
double sg_nr_level[3]; // NR fraction 0-1 (init: 0.50)
double fw_flow[3]; // feedwater [lbm/s] (init: 400)
double steam_flow[3]; // main steam [lbm/s] (init: 400)
// --- Safety systems ---
double hpi_flow, lpi_flow; // injection flows [lbm/s]
double break_flow[3]; // per loop [lbm/s]
// --- Component states (42 interactive variable targets) ---
bool reactor_tripped, turbine_tripped, si_actuated, mfp_tripped;
bool msiv_closed[3], porv_open, spray_valve_open;
bool rcp_tripped[3], hpi_pump_on[3], lpi_pump_on[3];
bool td_afw_pump_on, md_afw_pump_on[2], afw_pump_on[3];
// ... valve positions, throttles, IE flags
};4.3 Time Advancement
Each /advance call advances the model by dt seconds (typically 0.5s) using internal sub-stepping at 0.05s:
4.4 Physical Models
Core Power — Decay Heat
After reactor trip, core power follows a two-term fit calibrated to RELAP5 BV4 data:
P(t) = 0.240·exp(-t/0.82) + 0.136·t^(-0.2)| Time after trip | Mock | RELAP5 | ANS 5.1 |
|---|---|---|---|
| 0.5s | 28% | 28% | 9% |
| 2.0s | 14% | 14% | 7% |
| 14.0s | 7.8% | 7.8% | 5% |
The first term captures short-lived fission product decay (τ=0.82s). The second term captures long-term actinide/FP decay. A linear ramp over 0-0.1s avoids the power-law singularity at t=0.
RCS Flow — RCP Coastdown
flow_frac += (target - flow_frac) × (1 - exp(-dt/τ)) τ = 8.0sWhen all RCPs trip (LOOP), target = 0 and flow coasts down exponentially. A natural circulation floor of 5% prevents zero-flow singularities.
Hot Leg Temperatures — UA-Based SG Heat Removal
Q_removal = UA_eff × (T_hot - T_sat_SG)
UA_eff = SG_UA × (0.3 + 0.7 × flow_frac) SG_UA = 15,000 BTU/(s·°F)At natural circulation (flow_frac ≈ 0.05), UA_eff drops to 30% of nominal, giving ~500 kBTU/s heat removal — sufficient to cool down after trip but slow enough to match RELAP5 trends.
Pressurizer Pressure — Multi-Component Model
dP/dt = -break×0.5 + HPI×0.3 + LPI×0.1 - PORV_relief - spray
+ heaters (if powered and P < 2200)
+ thermal_coupling (after trip)The thermal coupling is asymmetric: cooling drives depressurization via P_eq = 2250 + (Tave - 567.5)×10, but above nominal Tave, only a slow -2 psi/s from heater loss. This prevents unphysical pressure overshoot during the initial post-trip transient.
SG Level
dL/dt = (FW_flow + AFW_flow - steam_flow) × 0.00002 - boiloffWithout feedwater (MFP tripped), boiloff depletes inventory at a rate proportional to core power.
LOOP Auto-Cascade
4.5 Physical Constants
| Constant | Value | Description |
|---|---|---|
NOMINAL_POWER_BTU_S | 8.8×10⁶ | ~2660 MWt in BTU/s |
RCS_MASS_LBM | 400,000 | Total RCS coolant mass |
CP_WATER | 1.3 BTU/(lbm·°F) | Subcooled water heat capacity |
SG_UA | 15,000 BTU/(s·°F) | SG heat transfer coefficient per SG |
RCP_COASTDOWN_TAU | 8.0 s | RCP coastdown time constant |
HPI_PUMP_FLOW | 150 lbm/s | Per HPI pump |
LPI_PUMP_FLOW | 300 lbm/s | Per LPI pump |
AFW_PUMP_FLOW | 50 lbm/s | Per AFW pump |
SUBSTEP_DT | 0.05 s | Internal ODE sub-step |
5. Parameter Mapping
5.1 Overview
The ParameterMapper translates 15 state variables into 310 output parameters matching the exact array layout in RELAP5_channels.txt. Array indices are sequential (0, 1, 2, ...) — SystemModule::updateParametersValues() reads them with i++, NOT using hydro_v_obj[].
5.2 Volumes (20)
| Index | RELAP5 Card | Description | Source |
|---|---|---|---|
| 0 | HV_204 | Loop A hot leg | liq_t=loop_thot[0], pressure=pzr_pressure-10 |
| 1 | HV_304 | Loop B hot leg | liq_t=loop_thot[1] |
| 2 | HV_404 | Loop C hot leg | liq_t=loop_thot[2] |
| 3 | HV_216 | Loop A cold leg | liq_t=tcold |
| 4 | HV_316 | Loop B cold leg | liq_t=tcold |
| 5 | HV_416 | Loop C cold leg | liq_t=tcold |
| 6 | HV_125 | Core outlet | liq_t=avg(loop_thot)+5 |
| 7 | HV_115 | Core inlet | liq_t=tcold+2 |
| 8 | HV_340 | Pressurizer | pressure=pzr_pressure, liq_t=tsat(pzr_pressure) |
| 9 | HV_360 | PZR surge line | pressure=pzr_pressure-5 |
| 10-12 | HV_550/650/750 | SG A/B/C secondary | pressure=sg_pressure[i] |
| 13-15 | HV_530/630/730 | SG A/B/C tubes | liq_t=avg(loop_thot[i], tsat_sg) |
| 16 | HV_802 | Containment | pressure=containment_pressure |
| 17-19 | HV_911/912/913 | Accumulators A/B/C | pressure=acc_pressure[i] |
All volumes include: liq_t, vap_t, sat_t, pressure, boron_d, quality, flow_reg.
5.3 Junctions (24)
| Index | RELAP5 Card | Description | Source |
|---|---|---|---|
| 0-2 | HJ_203/303/403 | Hot leg flows A/B/C | mass_fr=rcs_flow_frac×NOMINAL_RCS_FLOW |
| 3-5 | HJ_215/315/415 | Cold leg flows | (same) |
| 6-8 | HJ_527/627/727 | FW flows A/B/C | mass_fr=fw_flow[i] |
| 9-11 | HJ_543/643/743 | Steam flows A/B/C | mass_fr=steam_flow[i] |
| 12-14 | HJ_963/964/965 | HPI flows | mass_fr=hpi_flow/3 |
| 15-17 | HJ_973/974/975 | LPI flows | mass_fr=lpi_flow/3 |
| 18-20 | HJ_209/309/409 | Break flows A/B/C | mass_fr=break_flow[i] |
| 21-23 | HJ_547/647/747 | AFW flows A/B/C | mass_fr=afw_flow[i] |
5.4 Control Variables (78)
Key mappings (representative, not exhaustive):
| Index | RELAP5 CV | Description | Formula |
|---|---|---|---|
| 0 | CV_100 | Core Power % | core_power × 100 |
| 1 | CV_202 | PZR Level % | pzr_level × 100 |
| 2 | CV_204 | PZR Pressure | pzr_pressure |
| 5 | CV_480 | Tave-Tref | (avg_thot + tcold)/2 - 567.5 |
| 6-8 | CV_506/606/706 | SG NR Level A/B/C | sg_nr_level[i] × 100 |
| 12-14 | CV_508/608/708 | SG WR Level A/B/C | sg_wr_level[i] × 100 |
| 18-20 | CV_560/660/760 | SG Pressure A/B/C | sg_pressure[i] |
| 24-26 | CV_570/670/770 | SG Steam Flow A/B/C | steam_flow[i] |
| 42 | CV_900 | Total HPI Flow | hpi_flow |
| 43 | CV_910 | Total LPI Flow | lpi_flow |
| 44 | CV_984 | Total ECCS Flow | hpi_flow + lpi_flow |
| 47 | CV_977 | Heat Transfer Mode | 1 (single-phase) or 4 (boiling) |
5.5 Trips (82)
See Section 6.
6. Trip Evaluation
6.1 Variable Trips (51)
Threshold crossings evaluated each advance step. Once fired (time recorded), they stay fired for the remainder of the sequence. Key trips:
| Index | RELAP5 Trip | Condition | Purpose |
|---|---|---|---|
| 0 | VT_0465 | core_power > 1.05 | Overpower trip |
| 2-4 | VT_0503/507/508 | sg_nr_level[i] < 0.37 | Low SG level |
| 5-7 | VT_0509/510/511 | sg_nr_level[i] > 0.75 | High SG level |
| 11 | VT_0564 | pzr_pressure > 2350 | High PZR pressure trip |
| 12 | VT_0568 | pzr_pressure < 1900 | Low PZR pressure (SI signal) |
| 13 | VT_0572 | pzr_level < 0.10 | Low PZR level |
| 15-16 | VT_0580/581 | |Tave - Tref| > 5°F | Over-temperature delta-T |
| 17 | VT_0600 | rcs_flow_frac < 0.80 | Low RCS flow |
| 19 | VT_0610 | subcooling < 30°F | Low subcooling margin |
| 21 | VT_0620 | containment_pressure > 4 psig | Containment high |
| 25-27 | VT_0710/711/712 | fw_flow[i] < 100 lbm/s | Low FW flow |
6.2 Logical Trips (31)
System-level trips (OR combinations, component states):
| Index | RELAP5 Trip | Condition | Purpose |
|---|---|---|---|
| 0 | LT_1151 | core_power > 1.05 | Reactor trip signal |
| 1 | LT_1152 | rcs_flow_frac < 0.80 | Low flow reactor trip |
| 2 | LT_1153 | Any sg_pressure[i] < 600 | Low SG pressure SI |
| 3 | LT_1507 | pzr_pressure < 1800 | Low PZR pressure SI |
| 4 | LT_1510 | containment_pressure > 4 | High containment |
| 8 | LT_1663 | Any msiv_closed[i] | MSIV closure |
| 9 | LT_1665 | Any sg_nr_level[i] < 0.15 | LoLo SG level trip |
| 10 | LT_1669 | si_actuated | SI actuation |
| 11 | LT_1602 | turbine_tripped | Turbine trip |
| 12 | LT_1698 | reactor_tripped | Reactor trip (actual) |
| 13 | LT_1700 | mfp_tripped | MFP trip |
| 20-22 | LT_1800/1801/1802 | rcp_tripped[i] | RCP A/B/C tripped |
| 23-24 | LT_1810/1811 | hpi_pump_on[i] | HPI A/B on |
7. Timestep Management — The Two-Clock Architecture
Understanding timestep management is essential for any future coupling to a real thermal-hydraulic code. The original RELAP5 integration and the REST API mock solver both implement a two-clock architecture: a fast inner clock owned by the T-H solver and a slower outer clock that gates communication with the ADS-IDAC engine. This section documents both regimes, the gate mechanism that bridges them, and the design constraints that any replacement solver must satisfy.
7.1 RELAP5 Inner Clock — Adaptive Timestep Control
RELAP5/MOD3.3 uses an adaptive, semi-implicit numerical scheme to advance the conservation equations (mass, momentum, energy) for two-phase flow. The timestep dt is not fixed — it varies dynamically based on numerical stability, convergence, and Courant limits.
Timestep control logic (dtstep.f):
dt_min ≤ dt ≤ dt_max (from input deck time step cards)
On successful advancement:
dt_next = af * dt (af = 2.0, double the timestep)
On convergence failure:
dt_next = rf * dt (rf = 0.5, halve the timestep)
if dt_next < dt_min:
accept mass error, advance with dt = af * dt_min
(or terminate if error is unrecoverable)
On repeated failure:
timehy -= dt (roll back the failed step)
retry with halved dtThe key variables in dtstep.f:
| Variable | Type | Description |
|---|---|---|
timehy | real*8 | RELAP5 internal simulation clock (seconds) |
dt | real*8 | Current adaptive timestep |
dtmin(i) | real*8 | Minimum allowed timestep for time step card i |
dtmax(i) | real*8 | Maximum allowed timestep for time step card i |
af | real*8 | Advancement factor = 2.0 (double on success) |
rf | real*8 | Reduction factor = 0.5 (halve on failure) |
succes | integer | Convergence status (0 = success, >0 = failure severity) |
ncount | integer | Cumulative advancement count (restart card number) |
Typical timestep ranges for PWR transients:
| Phase | dt range | Steps per 0.5s |
|---|---|---|
| Steady-state / slow transient | 0.01–0.05 s | 10–50 |
| Break initiation / rapid depressurization | 0.001–0.005 s | 100–500 |
| Two-phase onset / void fraction oscillation | 0.0005–0.002 s | 250–1000 |
| Near-critical conditions | dt_min (~0.0001 s) | ~5000 |
A single 0.5s evaluation interval may therefore contain anywhere from 10 to 5000 RELAP5 internal steps, depending on the severity of the transient.
7.2 The 0.5-Second Evaluation Gate
The bridge between RELAP5's fast inner clock and ADS-IDAC's decision logic is a time-accumulation gate in dtstep.f (line 1334):
if ((timehy - timesno) .ge. 0.5) then
timesno = timehy
call ActTE ! execute pending crew actions at timestep
call R5ParUpdate ! copy RELAP5 internals → r5pars_ arrays
cncount = ncount ! expose restart card number to C++
call Interfc ! ← callback into ADS-IDAC C++ engine
...
endifThis gate fires when RELAP5's internal clock (timehy) has accumulated at least 0.5 seconds past the last evaluation point (timesno). Between gate firings, RELAP5 runs autonomously — ADS-IDAC has no visibility into or control over the sub-gate physics.
Key properties of the gate:
Not exactly 0.5s — the gate fires on the first RELAP5 step where
timehy - timesno ≥ 0.5. If the last step crosses the boundary,timesnois set to the actualtimehyvalue (e.g., 0.502s, not 0.500s). Over many gates, the slight overshoot accumulates but is inconsequential.Blocking — once
Interfc()is called, RELAP5 execution is suspended until the C++ callback returns. The engine can take arbitrarily long to evaluate crew logic, scan procedures, and decide branching.Bidirectional data flow — on entry,
R5ParUpdate()copies RELAP5 state intor5pars_arrays (C++ reads). On return, the engine may have setinter_val[]entries (crew actions) and flags (cgennode,csnostop,terminateall) that RELAP5 reads on the next step.Sub-gate action execution —
ActTE()(action at timestep) is called every time the gate fires, executing any pending crew actions that were queued during the previous evaluation. There is also a sub-gateActTE()call whentimehy < timesno(line 1327–1329), which handles the initial timestep before the first gate firing.
7.3 Mock Solver — Collapsing the Inner Loop
The mock solver replaces RELAP5's entire adaptive inner loop with a single REST API call:
RELAP5: ~50-5000 adaptive steps → 0.5s gate → Interfc()
Mock: POST /advance {dt: 0.5} → populate r5pars_ → onEvaluationPoint()From ADS-IDAC's perspective, the contract is identical: every ~0.5s it receives fresh r5pars_ data and runs its decision logic. The mock's SimplifiedPWR::advance(0.5) runs 10 internal sub-steps of 0.05s each (fixed, not adaptive), but this is invisible to the engine.
What the mock preserves:
- 0.5s evaluation interval (same crew decision timing resolution)
- All
r5pars_array indices and semantics (volumes, junctions, trips, control variables) - Trip evaluation at each evaluation point
- Intervention application before next advance
cncountincrementing (checkpoint numbering)
What the mock simplifies:
- No adaptive timestep (fixed 0.05s sub-step)
- No convergence failure / timestep halving
- No Courant limit enforcement
- No two-phase numerics (lumped single-node model per component)
- No
ncounttracking (RELAP5's internal restart card counter)
7.4 Constraints for Future Solver Coupling
Any thermal-hydraulic code coupled to ADS-IDAC via the REST API must satisfy these timing constraints:
Hard requirements:
Periodic evaluation points — the solver must support an external mechanism to pause execution at regular intervals (~0.5s) and return the full state. This maps to
POST /advance {dt: Δt}returning after Δt simulated seconds.State must be queryable between advances —
r5pars_arrays must be fully populated at each evaluation point. The engine reads volumes, junctions, trips, control variables, and interactive variables to make crew/branching decisions.Interventions must take effect before the next advance — crew actions (valve openings, pump starts, etc.) sent via
POST /intervenemust modify solver state before the nextPOST /advance. This mirrors theActTE()→ inner loop sequence.Checkpoint/restore must be exact — restoring a checkpoint and advancing must produce identical results to never having diverged. The dynamic event tree relies on this for branch correctness.
Configurable parameters:
| Parameter | Current value | Notes |
|---|---|---|
| Evaluation interval (gate period) | 0.5 s | Hardcoded in dtstep.f and RestApiSolver.cpp. Tightening to 0.25s doubles evaluation points but improves crew timing. Loosening to 1.0s halves overhead but delays decisions. |
| Mock sub-step dt | 0.05 s | Internal to SimplifiedPWR. No effect on API contract. |
| RELAP5 dt range | 0.0001–0.05 s | Set by input deck time step cards. Solver-internal. |
Design consideration: variable evaluation intervals. The current 0.5s gate is fixed, but the REST API naturally supports variable intervals — the engine could send POST /advance {dt: 0.1} during rapid transients and {dt: 1.0} during quasi-steady-state. This would require changes to RestApiSolver::runSequence() and the engine's evaluation logic, but the API contract already supports it. The mock solver handles any dt value transparently.
7.5 Data Flow Timing Diagram
The complete data flow for one evaluation cycle, showing both the RELAP5 and REST API paths:
Note the structural symmetry: both paths follow the same sequence of advance → update state → evaluate → branch/stop, differing only in the mechanism (Fortran calls vs HTTP requests). This symmetry is what makes the ThermalHydraulicSolver abstraction possible — the callback contract (SolverCallback) is identical regardless of which path is active.
8. Checkpoint/Restore Mechanism
8.1 Server-Side Checkpoints
The mock-solver stores checkpoints in a global (not per-session) CheckpointStore. Each checkpoint captures the full PWRState + TripResults as an in-memory copy. Checkpoint IDs are server-global (ckpt_1, ckpt_2, ...) so any session can restore any checkpoint — essential for MPI where branches cross session boundaries.
8.2 Client-Side ncount Mapping
The engine's RestApiSolver maintains a mapping from cncount (the RELAP5 restart card number written to indeck.i) to server checkpoint IDs. This is the bridge between the engine's FIFO branch traversal and the server's checkpoint store.
8.3 Checkpoint Data Flow in Serial vs MPI
Serial: RestApiSolver.checkpoint() returns a small JSON map (~100 bytes) stored in restart_buffers[]. On next sequence, restore() parses it, runSequence() sends POST /restore with the mapped checkpoint ID.
MPI: The same JSON bytes are stored in RestartNodeInfo.relap_restart_buf and serialized over MPI to peer ranks. The receiving rank's RestApiSolver.restore() parses it identically. Because CheckpointStore is global on the mock-server, checkpoint IDs created by rank 1's session are restorable by rank 2's session.
9. Engine Integration — runSequence Flow
9.1 Serial Execution
9.2 RestApiSolver.runSequence() Internal Flow
9.3 onEvaluationPoint() — The Decision Engine
This is where the ADS-IDAC crew model runs. The callback is identical for both RELAP5 and mock solver:
10. MPI Parallel Execution
10.1 Solver Creation per Rank
Each MPI client rank creates its own solver instance based on --solver-url:
// Client constructor
if (!solver_url.empty()) {
solver_ = std::make_unique<RestApiSolver>(solver_url);
} else {
solver_ = std::make_unique<RelapSolver>();
}All ranks connect to the same mock-solver process. Each rank creates its own HTTP session. The mock-server handles concurrency via a global mutex and a global checkpoint store.
10.2 MPI Branch Distribution with Mock Solver
10.3 Key MPI Adaptations
onBranchingPoint()usessolver.checkpoint()(abstract interface) instead of directads_membuf_get_()Fortran call — works for both RelapSolver and RestApiSolver.Checkpoint store is global on the mock-server — checkpoint IDs created by session_1 are accessible from session_2. Without this, MPI branch distribution would fail because receiving ranks can't access checkpoints from a different session.
Checkpoint payload is tiny (~100 bytes of JSON, against a few MB to more than 100 MB for a RELAP5 restart image). This means
--compress-queueand--memory-limithave negligible impact when using the mock solver.
11. Intervention Pipeline
11.1 Flow
11.2 Interactive Variable Index Mapping
Indices are 0-based offsets from IC_801 in RELAP5_channels.txt. 42 handlers implemented:
| IC Card | Index | Control Name | Mock Behavior |
|---|---|---|---|
| IC_801 | 0 | X_MFP_Trip | mfp_tripped = true |
| IC_802 | 1 | X_SCRAM | reactor_tripped = true, record time |
| IC_803 | 2 | X_Turb_Trip | turbine_tripped = true, record time |
| IC_804 | 3 | X_PZR_Spray_Valve | spray_valve_open, set position |
| IC_805 | 4 | X_PZR_PORV | porv_open, set position |
| IC_809-811 | 8-10 | X_SG_A/B/C_FWRV | Set fwrv_position[i] |
| IC_812-814 | 11-13 | X_SG_A/B/C_FWIV | Set mfiv_position[i] |
| IC_815-817 | 14-16 | X_SG_A/B/C_MSIV | msiv_closed[i], set position |
| IC_818 | 17 | X_SIAS | si_actuated = true, start HPI |
| IC_821-823 | 20-22 | X_RCP_A/B/C | rcp_tripped[i] |
| IC_824-825 | 23-24 | X_LPI_Pump_A/B | lpi_pump_on[i] |
| IC_826-827 | 25-26 | X_HPI_Pump_A/B | hpi_pump_on[i] |
| IC_828-829 | 27-28 | X_MD_AFW_Pump_A/B | md_afw_pump_on[i] |
| IC_830 | 29 | X_TD_AFW_Pump | td_afw_pump_on |
| IC_831 | 30 | X_LOOP | loop_active = true, record time |
| IC_834 | 33 | X_Stm_Dump | steam_dump_position |
| IC_835 | 34 | X_LOCA | loca_active = true, set break flow |
| IC_838-843 | 37-42 | AFW Throttle Valves | afw_throttle[i], md_afw_throttle[i] |
11.3 Delta Encoding
sendInterventions() compares current r5pars_.inter_val[] with a snapshot from the last call. Only changed values are sent in the POST body. This minimizes HTTP payload and avoids re-triggering one-shot events.
12. Key Design Decisions
12.1 r5pars_ Remains the Data Bridge
RestApiSolver.populateR5Pars() writes directly into the r5pars_ global struct — the same arrays that SystemModule::updateParametersValues() reads. This means zero changes to engine code. The control panel, crew model, procedures, hardware reliability, and branching logic all operate identically.
Trade-off: Tight coupling to r5pars_ layout. If the engine is ever refactored to not use r5pars_, the mock solver mapping must be updated.
12.2 One Advance = One Evaluation Point (0.5s)
Each POST /advance {dt: 0.5} corresponds to one RELAP5 interfc_() callback. No sub-timestep communication. This simplifies the REST API and matches the engine's evaluation cadence.
Trade-off: RELAP5 uses adaptive time-stepping internally (0.001-0.1s) and only calls interfc_() every ~0.5s. The mock advances in fixed 0.5s chunks. This means the mock can't detect sub-step events, but since the engine only acts at evaluation points, this is acceptable.
12.3 Interventions Are Batched
Crew actions accumulate in inter_val[] during onEvaluationPoint(). All changes are sent in a single POST /intervene before the next advance. This matches RELAP5's behavior where finteract_() is called once per evaluation point.
12.4 Session-Based API with Global Checkpoints
Sessions isolate model state (each MPI rank gets its own). Checkpoints are global to support cross-rank restore. This is the simplest design that works for both serial and MPI.
Trade-off: No checkpoint garbage collection — all checkpoints persist for the server's lifetime. For the BV4 test case (200 checkpoints at T=200), this is ~50 MB of server RAM. For longer runs, a TTL-based eviction policy would be needed.
12.5 Mock Model Is Intentionally Simple
Lumped parameters, not conservation equations. The goal is API validation and Fortran-free testing, not physics accuracy. Key simplifications:
- Single cold-leg temperature (all loops equal)
- No two-phase flow modeling
- No spatial neutronics (point kinetics only)
- No fuel conduction or gap conductance
- Approximate Tsat(P) correlation instead of steam tables
13. Verification Results
The tree sizes in this section were measured with the legacy-baseline-2026-08-18 engine (see Reference baselines, which also lists what the current engine produces with RELAP5).
13.1 Output File Validation
| Output | Mock, serial (T=15.5) | Mock, MPI n=3 (T=15.5) | RELAP5 (T=15.5) |
|---|---|---|---|
| ZESNode | 15 | 15 | 20 |
| ZPENode | 51 | ~51 | 60 |
| ZKPS/ZDIA/ZPIF | 15 each | 15 each | 20 each |
| ES_summary | 16 lines | 16 lines | 21 lines |
| Analysis files | 3 | 3 | 3 |
The mock produces fewer sequences than RELAP5 at T=15.5. The simplified model transitions faster, so some branching conditions fire at slightly different times, which gives a smaller but qualitatively similar event tree.
13.2 Parameter Comparison (LOOP Transient)
| Parameter | t=1.5s (mock) | t=1.5s (RELAP5) | Notes |
|---|---|---|---|
| Core Power | 13.7% | 13.2% | Within 1% |
| PZR Pressure | 2248 psia | 2243 psia | Within 5 psi |
| SG Pressure | 850 psia | 848 psia | Matches |
| RCS Flow | 95% | 93% | Coastdown in progress |
| Tcold | 544°F | 543°F | Within 1°F |
| Parameter | t=15s (mock) | t=15s (RELAP5) | Notes |
|---|---|---|---|
| Core Power | 7.5% | 7.8% | Within 0.5% |
| PZR Pressure | 2203 psia | 2098 psia | Simplified model |
| SG Pressure | 875 psia | 870 psia | Close |
| PZR Level | 45% | 39% | Model gap |
| Tcold | 530°F | 500°F | ~30°F offset |
14. Benchmarks
All benchmarks on macOS ARM64 (14-core Apple Silicon), Clang+gfortran Release build, measured with the legacy-baseline-2026-08-18 engine (see Reference baselines). Rerun them with scripts/mock_solver_benchmark.sh.
14.1 Wall Time (seconds) — Default Configuration
| Ranks | T=15.5 (15 seq) | T=50 (60 seq) | T=100 (165 seq) | T=150 (211 seq) | T=200 (215 seq) |
|---|---|---|---|---|---|
| Serial | 1.5 | 6.9 | 26.5 | 38.9 | 47.8 |
| n=3 | 1.5 | 5.6 | 18.4 | 29.0 | 35.5 |
| n=4 | 1.2 | 4.1 | 14.6 | 23.1 | 26.4 |
| n=6 | 1.3 | 3.3 | 10.3 | 16.9 | 20.1 |
| n=8 | 1.3 | 3.2 | 9.4 | 15.0 | 18.4 |
| n=10 | 1.4 | 3.0 | 7.9 | 16.1 | 17.0 |
| n=12 | 1.6 | 3.4 | 10.5 | 14.6 | 17.5 |
14.2 Wall Time — With --compress-queue
| Ranks | T=100 | T=150 | T=200 |
|---|---|---|---|
| n=4 | 14.4 | 22.8 | 27.4 |
| n=8 | 7.8 | 15.7 | 18.2 |
| n=10 | 7.7 | 16.2 | 17.5 |
| n=12 | 9.1 | 14.6 | 19.4 |
14.3 Wall Time — With --memory-limit 250
| Ranks | T=100 | T=150 | T=200 |
|---|---|---|---|
| n=6 | 8.7 | 18.0 | 20.6 |
| n=10 | 8.0 | 14.5 | 17.9 |
14.4 Peak RSS (MB)
| Ranks | T=15.5 | T=50 | T=100 | T=150 | T=200 |
|---|---|---|---|---|---|
| Serial | 113 | 443 | 1,432 | 1,620 | 1,667 |
| n=4 | 116 | 249 | 626 | 833 | 829 |
| n=8 | 117 | 255 | 411 | 707 | 756 |
| n=10 | 116 | 245 | 402 | 706 | 751 |
14.5 Key Findings
- Compress and memory-limit have negligible impact on the mock solver. The checkpoint payload is ~100 bytes (JSON ncount→checkpoint_id map), against a few MB to more than 100 MB for a RELAP5 restart image.
- Memory is dominated by C++ engine state (RestartNodeInfo, BranchRecord, KPS), not solver checkpoints. Serial peaks at 1.7 GB at T=200.
- MPI distributes memory well — n=10 caps at ~750 MB at T=200, a 55% reduction from serial.
- Sweet spot: n=8-10 for wall time. Best: n=10 at T=100 = 7.9s (3.4x speedup over serial).
- Mock generates more sequences than RELAP5 at longer horizons (165 vs 57 at T=100) because the simplified model transitions faster. Branching cascades more aggressively.
- Mock server RSS is constant — ~33 MB regardless of sequence count or checkpoint count. All state is per-session or in the global checkpoint store.
14.6 Comparison with RELAP5
| Config | T=100 Sequences | T=100 Wall Time | T=100 Peak RSS |
|---|---|---|---|
| RELAP5 serial | 57 | 21.4s | 1,024 MB |
| RELAP5 n=10 | 57 | 5.8s | 1,694 MB |
| Mock serial | 165 | 26.5s | 1,432 MB |
| Mock n=10 | ~170 | 7.9s | 402 MB |
The mock processes 3x more sequences in comparable wall time. Per-sequence, the mock is much faster (no Fortran physics), but generates more branches, resulting in similar total runtime.
15. Extending for Real T-H Codes
15.1 Replacing the Mock with a Real Solver
To wrap a real T-H code (e.g., RELAP5 running as a separate process, TRACE, MELCOR):
- Implement the REST API contract (Section 3) in the solver process
- Map solver outputs to the 310-parameter JSON schema
- Implement checkpoint/restore that captures the full solver state
No changes to the ADS-IDAC engine are needed — RestApiSolver connects to any server implementing the API.
15.2 What the Wrapper Must Provide
15.3 Adapter Pattern
For codes that are library-callable (not standalone processes):
class TraceApiServer : public MockServer { // inherit HTTP routing
TraceSolver trace_; // in-process TRACE instance
void handleAdvance(Request& req, Response& res) override {
double dt = parseBody(req)["dt"];
trace_.advance(dt);
json resp = mapTraceState(trace_.state()); // code-specific mapping
res.set_content(resp.dump(), "application/json");
}
};For codes that run as separate processes (e.g., RELAP5 via file I/O):
class RelapApiServer {
void handleAdvance(Request& req, Response& res) {
double dt = parseBody(req)["dt"];
writeRelapInput(dt); // write control cards
system("relap5 < input.i"); // run RELAP5
json resp = parseRelapOutput("output.o"); // parse output
res.set_content(resp.dump(), "application/json");
}
};15.4 Verification Checklist
When replacing the mock with a real solver:
- [ ] All 310 parameters populated (20 vol × 7 + 24 junc × 3 + 51 VT + 31 LT + 78 CV)
- [ ] Trip fire times are monotonic (once fired, stays fired)
- [ ] Checkpoint/restore produces identical state after round-trip
- [ ]
stop_flagcorrectly signals normal end (3) vs crash (11) - [ ] Interventions take effect on the next advance step
- [ ] BV4 runs to completion and repeated runs give the same number of sequences
- [ ] Output files generated: ZESNode, ZPENode, ZKPS, ZDIA, ZPIF, ES_summary
- [ ] MPI: sequences match serial (within ~10% from exploration order)
Appendix A: Dependencies
| Library | Version | Purpose | Integration |
|---|---|---|---|
| cpp-httplib | 0.15+ | HTTP server (mock) + client (RestApiSolver) | Header-only, third_party/httplib.h |
| nlohmann/json | 3.11+ | JSON serialization/parsing | Header-only, third_party/json.hpp |
No external dependencies beyond the C++17 standard library and pthreads.
Appendix B: File Manifest
| File | Purpose |
|---|---|
mock-solver/main.cpp | Server entry point, signal handling |
mock-solver/src/MockServer.h | Session struct, route declarations |
mock-solver/src/MockServer.cpp | HTTP handlers, session and checkpoint logic |
mock-solver/src/SimplifiedPWR.h | PWRState struct, SimplifiedPWR class |
mock-solver/src/SimplifiedPWR.cpp | ODE substep, intervention handlers |
mock-solver/src/TripEvaluator.h | TripEvaluator and TripResults declarations |
mock-solver/src/TripEvaluator.cpp | 51 variable trips + 31 logical trips |
mock-solver/src/ParameterMapper.h | Mapper class declaration |
mock-solver/src/ParameterMapper.cpp | State → 310 parameters (20V + 24J + 78CV + 82T) |
mock-solver/src/CheckpointStore.h | Global checkpoint store |
mock-solver/src/CheckpointStore.cpp | Save/restore/size/remove |
src/ADSIDAC/Solver/RestApiSolver.h | REST client declarations |
src/ADSIDAC/Solver/RestApiSolver.cpp | HTTP calls, r5pars_ population, ncount mapping |
src/ADSIDAC/Solver/ThermalHydraulicSolver.h | Abstract solver and callback interfaces |
src/ADSIDAC/Solver/AdsidacCallback.h/.cpp | Engine callbacks |