Skip to content

Mock Solver & REST API Decoupling Architecture ​

Table of Contents ​

  1. Overview
  2. Architecture
  3. REST API Contract
  4. Simplified PWR Model
  5. Parameter Mapping
  6. Trip Evaluation
  7. Timestep Management — The Two-Clock Architecture
  8. Checkpoint/Restore Mechanism
  9. Engine Integration — runSequence Flow
  10. MPI Parallel Execution
  11. Intervention Pipeline
  12. Key Design Decisions
  13. Verification Results
  14. Benchmarks
  15. 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:

  1. The abstract ThermalHydraulicSolver and SolverCallback interfaces isolate the engine from any specific solver implementation.
  2. 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 ​

bash
# 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=8080

3. REST API Contract ​

Session-based API. The solver manages T-H state; ADS-IDAC manages branching and crew logic.

3.1 Endpoints ​

MethodEndpointPurpose
POST/api/v1/sessionsCreate a new solver session
POST/api/v1/sessions/{id}/advanceAdvance simulation by dt seconds
POST/api/v1/sessions/{id}/interveneApply crew actions
POST/api/v1/sessions/{id}/checkpointSnapshot current state
POST/api/v1/sessions/{id}/restoreRestore from checkpoint
DELETE/api/v1/sessions/{id}Destroy session
GET/api/v1/sessions/{id}/stateQuery current state
GET/api/v1/healthHealth check

3.2 Advance Response Schema ​

The /advance response is the core data contract. It returns 310 parameters matching the r5pars_ array layout:

json
{
  "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 ​

json
{
  "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 ​

cpp
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 tripMockRELAP5ANS 5.1
0.5s28%28%9%
2.0s14%14%7%
14.0s7.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.0s

When 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 - boiloff

Without feedwater (MFP tripped), boiloff depletes inventory at a rate proportional to core power.

LOOP Auto-Cascade ​

4.5 Physical Constants ​

ConstantValueDescription
NOMINAL_POWER_BTU_S8.8×10⁶~2660 MWt in BTU/s
RCS_MASS_LBM400,000Total RCS coolant mass
CP_WATER1.3 BTU/(lbm·°F)Subcooled water heat capacity
SG_UA15,000 BTU/(s·°F)SG heat transfer coefficient per SG
RCP_COASTDOWN_TAU8.0 sRCP coastdown time constant
HPI_PUMP_FLOW150 lbm/sPer HPI pump
LPI_PUMP_FLOW300 lbm/sPer LPI pump
AFW_PUMP_FLOW50 lbm/sPer AFW pump
SUBSTEP_DT0.05 sInternal 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) ​

IndexRELAP5 CardDescriptionSource
0HV_204Loop A hot legliq_t=loop_thot[0], pressure=pzr_pressure-10
1HV_304Loop B hot legliq_t=loop_thot[1]
2HV_404Loop C hot legliq_t=loop_thot[2]
3HV_216Loop A cold legliq_t=tcold
4HV_316Loop B cold legliq_t=tcold
5HV_416Loop C cold legliq_t=tcold
6HV_125Core outletliq_t=avg(loop_thot)+5
7HV_115Core inletliq_t=tcold+2
8HV_340Pressurizerpressure=pzr_pressure, liq_t=tsat(pzr_pressure)
9HV_360PZR surge linepressure=pzr_pressure-5
10-12HV_550/650/750SG A/B/C secondarypressure=sg_pressure[i]
13-15HV_530/630/730SG A/B/C tubesliq_t=avg(loop_thot[i], tsat_sg)
16HV_802Containmentpressure=containment_pressure
17-19HV_911/912/913Accumulators A/B/Cpressure=acc_pressure[i]

All volumes include: liq_t, vap_t, sat_t, pressure, boron_d, quality, flow_reg.

5.3 Junctions (24) ​

IndexRELAP5 CardDescriptionSource
0-2HJ_203/303/403Hot leg flows A/B/Cmass_fr=rcs_flow_frac×NOMINAL_RCS_FLOW
3-5HJ_215/315/415Cold leg flows(same)
6-8HJ_527/627/727FW flows A/B/Cmass_fr=fw_flow[i]
9-11HJ_543/643/743Steam flows A/B/Cmass_fr=steam_flow[i]
12-14HJ_963/964/965HPI flowsmass_fr=hpi_flow/3
15-17HJ_973/974/975LPI flowsmass_fr=lpi_flow/3
18-20HJ_209/309/409Break flows A/B/Cmass_fr=break_flow[i]
21-23HJ_547/647/747AFW flows A/B/Cmass_fr=afw_flow[i]

5.4 Control Variables (78) ​

Key mappings (representative, not exhaustive):

IndexRELAP5 CVDescriptionFormula
0CV_100Core Power %core_power × 100
1CV_202PZR Level %pzr_level × 100
2CV_204PZR Pressurepzr_pressure
5CV_480Tave-Tref(avg_thot + tcold)/2 - 567.5
6-8CV_506/606/706SG NR Level A/B/Csg_nr_level[i] × 100
12-14CV_508/608/708SG WR Level A/B/Csg_wr_level[i] × 100
18-20CV_560/660/760SG Pressure A/B/Csg_pressure[i]
24-26CV_570/670/770SG Steam Flow A/B/Csteam_flow[i]
42CV_900Total HPI Flowhpi_flow
43CV_910Total LPI Flowlpi_flow
44CV_984Total ECCS Flowhpi_flow + lpi_flow
47CV_977Heat Transfer Mode1 (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:

IndexRELAP5 TripConditionPurpose
0VT_0465core_power > 1.05Overpower trip
2-4VT_0503/507/508sg_nr_level[i] < 0.37Low SG level
5-7VT_0509/510/511sg_nr_level[i] > 0.75High SG level
11VT_0564pzr_pressure > 2350High PZR pressure trip
12VT_0568pzr_pressure < 1900Low PZR pressure (SI signal)
13VT_0572pzr_level < 0.10Low PZR level
15-16VT_0580/581|Tave - Tref| > 5°FOver-temperature delta-T
17VT_0600rcs_flow_frac < 0.80Low RCS flow
19VT_0610subcooling < 30°FLow subcooling margin
21VT_0620containment_pressure > 4 psigContainment high
25-27VT_0710/711/712fw_flow[i] < 100 lbm/sLow FW flow

6.2 Logical Trips (31) ​

System-level trips (OR combinations, component states):

IndexRELAP5 TripConditionPurpose
0LT_1151core_power > 1.05Reactor trip signal
1LT_1152rcs_flow_frac < 0.80Low flow reactor trip
2LT_1153Any sg_pressure[i] < 600Low SG pressure SI
3LT_1507pzr_pressure < 1800Low PZR pressure SI
4LT_1510containment_pressure > 4High containment
8LT_1663Any msiv_closed[i]MSIV closure
9LT_1665Any sg_nr_level[i] < 0.15LoLo SG level trip
10LT_1669si_actuatedSI actuation
11LT_1602turbine_trippedTurbine trip
12LT_1698reactor_trippedReactor trip (actual)
13LT_1700mfp_trippedMFP trip
20-22LT_1800/1801/1802rcp_tripped[i]RCP A/B/C tripped
23-24LT_1810/1811hpi_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 dt

The key variables in dtstep.f:

VariableTypeDescription
timehyreal*8RELAP5 internal simulation clock (seconds)
dtreal*8Current adaptive timestep
dtmin(i)real*8Minimum allowed timestep for time step card i
dtmax(i)real*8Maximum allowed timestep for time step card i
afreal*8Advancement factor = 2.0 (double on success)
rfreal*8Reduction factor = 0.5 (halve on failure)
succesintegerConvergence status (0 = success, >0 = failure severity)
ncountintegerCumulative advancement count (restart card number)

Typical timestep ranges for PWR transients:

Phasedt rangeSteps per 0.5s
Steady-state / slow transient0.01–0.05 s10–50
Break initiation / rapid depressurization0.001–0.005 s100–500
Two-phase onset / void fraction oscillation0.0005–0.002 s250–1000
Near-critical conditionsdt_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):

fortran
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
    ...
endif

This 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:

  1. Not exactly 0.5s — the gate fires on the first RELAP5 step where timehy - timesno ≥ 0.5. If the last step crosses the boundary, timesno is set to the actual timehy value (e.g., 0.502s, not 0.500s). Over many gates, the slight overshoot accumulates but is inconsequential.

  2. 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.

  3. Bidirectional data flow — on entry, R5ParUpdate() copies RELAP5 state into r5pars_ arrays (C++ reads). On return, the engine may have set inter_val[] entries (crew actions) and flags (cgennode, csnostop, terminateall) that RELAP5 reads on the next step.

  4. 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-gate ActTE() call when timehy < 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
  • cncount incrementing (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 ncount tracking (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:

  1. 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.

  2. 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.

  3. Interventions must take effect before the next advance — crew actions (valve openings, pump starts, etc.) sent via POST /intervene must modify solver state before the next POST /advance. This mirrors the ActTE() → inner loop sequence.

  4. 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:

ParameterCurrent valueNotes
Evaluation interval (gate period)0.5 sHardcoded 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 dt0.05 sInternal to SimplifiedPWR. No effect on API contract.
RELAP5 dt range0.0001–0.05 sSet 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:

cpp
// 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 ​

  1. onBranchingPoint() uses solver.checkpoint() (abstract interface) instead of direct ads_membuf_get_() Fortran call — works for both RelapSolver and RestApiSolver.

  2. 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.

  3. 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-queue and --memory-limit have 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 CardIndexControl NameMock Behavior
IC_8010X_MFP_Tripmfp_tripped = true
IC_8021X_SCRAMreactor_tripped = true, record time
IC_8032X_Turb_Tripturbine_tripped = true, record time
IC_8043X_PZR_Spray_Valvespray_valve_open, set position
IC_8054X_PZR_PORVporv_open, set position
IC_809-8118-10X_SG_A/B/C_FWRVSet fwrv_position[i]
IC_812-81411-13X_SG_A/B/C_FWIVSet mfiv_position[i]
IC_815-81714-16X_SG_A/B/C_MSIVmsiv_closed[i], set position
IC_81817X_SIASsi_actuated = true, start HPI
IC_821-82320-22X_RCP_A/B/Crcp_tripped[i]
IC_824-82523-24X_LPI_Pump_A/Blpi_pump_on[i]
IC_826-82725-26X_HPI_Pump_A/Bhpi_pump_on[i]
IC_828-82927-28X_MD_AFW_Pump_A/Bmd_afw_pump_on[i]
IC_83029X_TD_AFW_Pumptd_afw_pump_on
IC_83130X_LOOPloop_active = true, record time
IC_83433X_Stm_Dumpsteam_dump_position
IC_83534X_LOCAloca_active = true, set break flow
IC_838-84337-42AFW Throttle Valvesafw_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 ​

OutputMock, serial (T=15.5)Mock, MPI n=3 (T=15.5)RELAP5 (T=15.5)
ZESNode151520
ZPENode51~5160
ZKPS/ZDIA/ZPIF15 each15 each20 each
ES_summary16 lines16 lines21 lines
Analysis files333

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) ​

Parametert=1.5s (mock)t=1.5s (RELAP5)Notes
Core Power13.7%13.2%Within 1%
PZR Pressure2248 psia2243 psiaWithin 5 psi
SG Pressure850 psia848 psiaMatches
RCS Flow95%93%Coastdown in progress
Tcold544°F543°FWithin 1°F
Parametert=15s (mock)t=15s (RELAP5)Notes
Core Power7.5%7.8%Within 0.5%
PZR Pressure2203 psia2098 psiaSimplified model
SG Pressure875 psia870 psiaClose
PZR Level45%39%Model gap
Tcold530°F500°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 ​

RanksT=15.5 (15 seq)T=50 (60 seq)T=100 (165 seq)T=150 (211 seq)T=200 (215 seq)
Serial1.56.926.538.947.8
n=31.55.618.429.035.5
n=41.24.114.623.126.4
n=61.33.310.316.920.1
n=81.33.29.415.018.4
n=101.43.07.916.117.0
n=121.63.410.514.617.5

14.2 Wall Time — With --compress-queue ​

RanksT=100T=150T=200
n=414.422.827.4
n=87.815.718.2
n=107.716.217.5
n=129.114.619.4

14.3 Wall Time — With --memory-limit 250 ​

RanksT=100T=150T=200
n=68.718.020.6
n=108.014.517.9

14.4 Peak RSS (MB) ​

RanksT=15.5T=50T=100T=150T=200
Serial1134431,4321,6201,667
n=4116249626833829
n=8117255411707756
n=10116245402706751

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 ​

ConfigT=100 SequencesT=100 Wall TimeT=100 Peak RSS
RELAP5 serial5721.4s1,024 MB
RELAP5 n=10575.8s1,694 MB
Mock serial16526.5s1,432 MB
Mock n=10~1707.9s402 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):

  1. Implement the REST API contract (Section 3) in the solver process
  2. Map solver outputs to the 310-parameter JSON schema
  3. 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):

cpp
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):

cpp
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_flag correctly 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 ​

LibraryVersionPurposeIntegration
cpp-httplib0.15+HTTP server (mock) + client (RestApiSolver)Header-only, third_party/httplib.h
nlohmann/json3.11+JSON serialization/parsingHeader-only, third_party/json.hpp

No external dependencies beyond the C++17 standard library and pthreads.

Appendix B: File Manifest ​

FilePurpose
mock-solver/main.cppServer entry point, signal handling
mock-solver/src/MockServer.hSession struct, route declarations
mock-solver/src/MockServer.cppHTTP handlers, session and checkpoint logic
mock-solver/src/SimplifiedPWR.hPWRState struct, SimplifiedPWR class
mock-solver/src/SimplifiedPWR.cppODE substep, intervention handlers
mock-solver/src/TripEvaluator.hTripEvaluator and TripResults declarations
mock-solver/src/TripEvaluator.cpp51 variable trips + 31 logical trips
mock-solver/src/ParameterMapper.hMapper class declaration
mock-solver/src/ParameterMapper.cppState → 310 parameters (20V + 24J + 78CV + 82T)
mock-solver/src/CheckpointStore.hGlobal checkpoint store
mock-solver/src/CheckpointStore.cppSave/restore/size/remove
src/ADSIDAC/Solver/RestApiSolver.hREST client declarations
src/ADSIDAC/Solver/RestApiSolver.cppHTTP calls, r5pars_ population, ncount mapping
src/ADSIDAC/Solver/ThermalHydraulicSolver.hAbstract solver and callback interfaces
src/ADSIDAC/Solver/AdsidacCallback.h/.cppEngine callbacks

ADS-IDAC Engine Documentation