Co-Study4Grid / docs /architecture /simulation-pipeline.md
github-actions[bot]
Deploy 7688ef1
13d4e44
|
Raw
History Blame Contribute Delete
32.3 kB
# Simulation pipeline — Co-Study4Grid backend
This document explains what happens between the moment an operator
clicks **"Analyze & Suggest"** (or stars a single action) in the React
UI and the moment a result lands back in their browser. It focuses on
the simulation mechanics, the parameters that are exposed, the
hypotheses that are baked in, and the modes the system supports.
> **Start here for the simulation deep-dive.** Anything below that
> mentions `obs.simulate`, `run_load_flow`, a load-flow mode, voltage
> initialisation, variant management, the `DC_VALUES` fallback or
> the `maxOuterLoopIterations` cap is **implemented in the lib** — the
> backend just wires the right knobs. The full mechanism, the four
> orthogonal LF knobs and every retry branch are documented in the
> sister doc:
> **[`Expert_op4grid_recommender/docs/architecture/simulation-pipeline.md`](https://github.com/marota/Expert_op4grid_recommender/blob/main/docs/architecture/simulation-pipeline.md)**.
> The present file describes the **backend-side glue** that exposes
> that machinery through the FastAPI endpoints and the React UI.
> Sections 4.4 and 5.2 below also surface the **fast vs slow mode**
> cheat-sheet inline for quick reference — for the rationale and the
> retry tree, keep the lib doc handy.
## 1. High-level flow
```
React UI FastAPI RecommenderService expert_op4grid_recommender
│ │ │ │
├─ POST /api/config ───▶│── update_config(settings) ▶│── SimulationEnvironment(network) ───▶│ pn.load(.xiidm)
│ │ │ prefetch base NAD │ LF (DC_VALUES seed)
│ │ │ │
├─ POST /api/run-analysis-step1 ───▶│ run_analysis_step1 │
│ {disconnected_elements} │ simulate contingency → obs_N1 │ obs.simulate(disconnect)
│ │ │ detect overloads (rho > 1) │
│ │ │ cache (obs_N, obs_N1) on _analysis_context │
│ │◀──────────│ lines_overloaded_names │
│ │ │ │
├─ POST /api/run-analysis-step2 ───▶│ run_analysis_step2_discovery │
│ {selected_overloads} │ RecommenderModel.recommend(inputs) │
│ │ │ reassess_prioritized_actions │ ×N obs.simulate(action)
│ │ │ compute_combined_pairs (superposition) │
│ │◀── stream {type:"pdf"} ──◀ overflow_graph_path
│ │◀── stream {type:"result", │
│ │ prioritized_actions, ...} ──◀ │
│ │ │ │
├─ POST /api/simulate-manual-action ▶│ simulate_manual_action │
│ {action_id} │ resolve action vs _dict_action │
│ │ │ obs_simu_defaut.simulate(action, │
│ │ │ keep_variant=True, fast_mode=…) │ obs.simulate(action)
│ │◀───────────│ serialize result (NumPy → JSON) │
```
Every UI gesture (`branch select`, `action star`, `pair compute`) ends
up at one of the five endpoints above. The backend is **stateless from
the client's perspective** — every request carries the contingency
and the action ids it needs — but **stateful internally**: a single
`RecommenderService` singleton holds the loaded network, the action
dictionary, observation caches, and analysis context across requests.
## 2. Architectural layout
```
expert_backend/services/
├── network_service.py # raw pypowsybl Network + metadata
├── recommender_service.py # state lifecycle, composes the 3 mixins
├── diagram_mixin.py + diagram/ # NAD/SLD rendering (not in this doc)
├── analysis_mixin.py + analysis/ # run_analysis_step1, step2
├── simulation_mixin.py # simulate_manual_action, compute_superposition
└── simulation_helpers.py # 14 pure helpers (metrics, setpoints, …)
```
### 2.1 Two module-level singletons
`network_service` owns the `pp.network.Network` object loaded by
`pn.load(...)`. Read-only consumers (`/api/branches`,
`/api/voltage-levels`, …) hit it directly. It does NOT switch
variants.
`recommender_service` owns the analysis state:
| Attribute | Purpose |
|---|---|
| `_base_network` | shared `pp.network.Network` (re-used from `network_service`, no second `pn.load`). |
| `_simulation_env` | `SimulationEnvironment` (lib) wrapping `_base_network`. |
| `_cached_env_context` | bundle of (env, action_space, name_sub, …) reused across step1 calls. |
| `_dict_action` | full action dictionary from `action_file_path` + auto-generated `disco_*`. |
| `_analysis_context` | `{obs, obs_simu_defaut, lines_overloaded_names, …}` captured at step1, reused at step2 / manual / superposition. |
| `_cached_obs_n1` | post-contingency observation built once per (network, contingency). Drives the `prebuilt_obs_simu_defaut` fast path of `run_analysis_step1`. |
| `_last_result` | last `{prioritized_actions, action_scores, …}` payload. Survives reload via the session JSON. |
| `_last_step2_signature` | identity of (network, contingency, lines_overloaded, model). Reused step2 → step2 skips overflow-graph rebuild on a model swap. |
The two singletons share **the same Network** because
`recommender_service._get_base_network()` mutualises
`network_service.network`. Safe because:
- `network_service` only reads.
- `recommender_service` always restores the working variant in a
`try/finally` (see `_ensure_n_state_ready`, `_ensure_n1_state_ready`,
`_ensure_contingency_state_ready`).
### 2.2 Mixin composition
`RecommenderService` inherits from `DiagramMixin`, `AnalysisMixin`,
`SimulationMixin`. They all operate on the same `self`. State
lifecycle (`__init__`, `reset`, `update_config`) stays in
`recommender_service.py`. Read the three mixins as one class split
across files for readability.
Pure numerics (no `self` access) live in helper packages
(`diagram/`, `analysis/`, `simulation_helpers.py`) so they can be
unit-tested without booting a FastAPI app.
## 3. State lifecycle
### 3.1 First load (`POST /api/config`)
1. `network_service.load_network(path)``pn.load(.xiidm)`.
2. `recommender_service.update_config(settings)`:
- `expert_op4grid_recommender.config` globals are written from the
pydantic `ConfigRequest` payload:
`ENV_PATH`, `LAYOUT_FILE_PATH`,
`PYPOWSYBL_FAST_MODE`, `MONITORING_FACTOR_THERMAL_LIMITS`,
`PRE_EXISTING_OVERLOAD_WORSENING_THRESHOLD`,
`MIN_LINE_RECONNECTIONS`, `MIN_CLOSE_COUPLING`,
`MIN_OPEN_COUPLING`, `MIN_LINE_DISCONNECTIONS`, `MIN_PST`,
`MIN_LOAD_SHEDDING`, `MIN_RENEWABLE_CURTAILMENT_ACTIONS`,
`N_PRIORITIZED_ACTIONS`, `IGNORE_RECONNECTIONS`,
`VISUALIZATION_FORMAT` (= `"html"`).
- `prefetch_base_nad_async()` spawns a daemon thread that
pre-computes the base NAD so the next `/api/network-diagram` is
near-instant.
- The action dictionary is loaded (`load_actions`) and enriched
(`enrich_actions_lazy`). Missing `disco_*` entries are auto-built.
- A `SimulationEnvironment` is constructed and cached on
`_cached_env_context`. Its constructor runs an **initial LF
seeded with `DC_VALUES`** (see § 3 of the lib doc) so the working
variant is in a valid state before the first user action.
### 3.2 Reload (any subsequent `POST /api/config`)
`recommender_service.reset()` is invoked **before** the new network is
loaded. It must clear every per-study cache. The exhaustive list is
documented in
[`docs/features/state-reset-and-confirmation-dialogs.md`](../features/state-reset-and-confirmation-dialogs.md).
Drain order matters: `_drain_pending_base_nad_prefetch()` runs first
so a still-running prefetch thread cannot race-write into the next
study's cache.
### 3.3 Session reload (`POST /api/load-session`)
Restores a previously saved `session.json` (config + contingency +
prioritized actions with status tags + combined pairs). Calls
`restore_analysis_context()` to repopulate `_analysis_context`,
`_last_result`, `_dict_action` injections, `_saved_computed_pairs`.
Hypothesis: session reload **does not re-simulate**. The `rho_after`,
`max_rho`, `non_convergence` values stored in JSON are trusted. Re-
simulation happens only when the operator explicitly clicks an action
again (`/api/simulate-manual-action`).
## 4. The two-step analysis flow
### 4.1 Step 1 — overload detection
`POST /api/run-analysis-step1 {disconnected_elements: [contingency_id]}`
Inside `AnalysisMixin.run_analysis_step1`:
1. `_ensure_contingency_state_ready(contingency)` — joins the NAD
prefetch (so it can't race) and pins the working variant on the
contingency variant.
2. **Decision tree** for getting `obs_simu_defaut` (the N-1 obs):
- If `_cached_obs_n1` exists for this contingency → use it
directly (`prebuilt_obs_simu_defaut=...`). Saves ~1–3 s.
- Otherwise call
`lib.run_analysis_step1(prebuilt_obs_simu_defaut=None)` which
runs `simulate_contingency_pypowsybl` internally
(`fast_mode=config.PYPOWSYBL_FAST_MODE`).
3. Library returns `obs_simu_defaut`, `lines_overloaded_names`,
`lines_overloaded_ids`, `pre_existing_rho`.
4. Backend captures the result in `_analysis_context` for re-use by
step2 / manual / superposition.
5. Returns `{lines_overloaded_names, lines_overloaded, pre_existing_rho}`.
Hypotheses surfaced in step 1:
- A branch is **overloaded** when `rho > 1` *after* the monitoring
factor (default 0.95) has been applied to the thermal limits.
- A pre-existing overload (already > 1 in N-state) is **kept in the
list** but flagged for downstream filtering (`pre_existing_rho`).
### 4.2 Step 2 — action discovery + reassessment
`POST /api/run-analysis-step2 {selected_overloads, all_overloads, monitor_deselected, additional_lines_to_cut}`
NDJSON streaming response. Two events:
1. `{type:"pdf", pdf_url, pdf_path}` emitted as soon as the
overflow-graph file is written. Field name `pdf` is legacy — the
actual file is now an `.html` interactive viewer
(`config.VISUALIZATION_FORMAT="html"`).
2. `{type:"result", prioritized_actions, action_scores, …}` emitted at
the end.
Inside `analysis_runner`:
1. **Signature check**: `_last_step2_signature` = hash of
`(network_path, contingency, selected_overloads, monitor_deselected,
additional_lines_to_cut, active_model)`. If unchanged, the
overflow-graph is reused from cache; only model recommendation +
reassessment re-runs (model-swap fast path).
2. Build the overflow graph (graphviz `dot`) via the library's
`overflow_distribution_graph` + write it to `Overflow_Graph/`
following the canonical filename pattern. Emit the `pdf` event.
3. Call `lib.run_analysis_step2_discovery(context, recommender, params)`
which:
- Filters the candidate action set by the expert-rule chain
(whenever an overflow graph is available, **regardless** of the
active model — sampling models inherit the same filter).
- Calls `recommender.recommend(inputs, params)` to get
`{action_id: action_object}` (raw scores).
- Reassesses every action via `reassess_prioritized_actions`:
each candidate gets its own `obs.simulate(action,
fast_mode=actual_fast_mode, keep_variant=True)`.
- Computes combined pairs via `compute_combined_pairs`
(superposition theorem over the top-K reassessed actions).
4. Backend serialises the result (NumPy → native via
`sanitize_for_json`) and emits the `result` event.
Hypotheses in step 2:
- The expert rule filter assumes the overflow graph is "trusted"
(i.e. step 1 converged). When the graph is missing or stale the
filter is silently bypassed and the model sees the whole action
dictionary.
- `monitor_deselected=False` (default) means **only the selected
overloads count toward the model's objective**. When `True`, all
detected overloads are monitored — typically slower because the
candidate set is larger.
- The reassessment phase **does not** mutate the candidate ranking —
it only enriches each action with `max_rho`, `rho_after`,
`is_rho_reduction`, `is_islanded`, `n_components`, `non_convergence`,
plus type-specific details for LS / curtail / redispatch / PST.
- `compute_combined_pairs` uses the **superposition theorem** on the
per-line rho impacts. It is an estimate, not a full re-simulation
— see [`docs/features/combined-actions.md`](../features/combined-actions.md)
for the contract and [`docs/superposition_module.md`](https://github.com/marota/Expert_op4grid_recommender/blob/main/docs/superposition_module.md)
in the lib for the math.
### 4.3 Pre-existing overload threshold
`config.PRE_EXISTING_OVERLOAD_WORSENING_THRESHOLD` (default `0.02`,
UI field `pre_existing_overload_threshold`) controls when a remedial
action's impact on a pre-existing overloaded line is counted as
"worsening". If the line was already at rho=4.34 and the action moves
it to rho=4.345 (+0.005), the action is **not** flagged as
"worsening" because the delta is below 2 %.
### 4.4 Load-flow modes — inline cheat-sheet
Every `obs.simulate(...)` call inside step 1 and step 2 forwards
`fast_mode=actual_fast_mode` down to
`NetworkManager.run_load_flow(fast=…)`. The same applies to manual
simulations (§ 5) and combined-pair re-simulations (§ 6). Resolved at
the top of `lib.run_analysis`:
```python
if fast_mode: # explicit override
actual_fast_mode = True
else:
actual_fast_mode = (
config.PYPOWSYBL_FAST_MODE if fast_mode is None else fast_mode
)
```
Backend default for `config.PYPOWSYBL_FAST_MODE` is **`True`**, set
from the UI Settings checkbox `pypowsybl_fast_mode` and persisted in
`user_config.json`. So every analysis / simulation runs in **fast
AC** unless the user toggles it off.
Reproduced from the lib doc § 9 — keep this open when reading the
flows below:
| Mode | Knobs | When the backend uses it | Trade-off |
|---|---|---|---|
| **AC fast (default)** | `dc=False, fast=True, init=PREVIOUS_VALUES` | every `obs.simulate(...)` triggered by step 1 / step 2 / `simulate-manual-action` / `compute-superposition` while the UI setting `pypowsybl_fast_mode` is on (default). | quickest AC; ignores tap / shunt re-regulation. |
| **AC slow** | `dc=False, fast=False, init=PREVIOUS_VALUES` | (a) UI setting `pypowsybl_fast_mode=false` propagates through `config.PYPOWSYBL_FAST_MODE` to `actual_fast_mode=False`; (b) automatic retry by the lib when fast didn't converge. | full physics; ~2× slower; converges more cases. |
| **AC + DC seed** | `dc=False, init=DC_VALUES` | implicit retry inside the lib's `_run_ac_with_init_fallback` whenever the initial `PREVIOUS_VALUES` attempt raises OR returns a non-`CONVERGED` status (`FAILED` "Unrealistic state", `MAX_ITERATION_REACHED`). Not directly exposed to the UI. | robust to topology perturbations; +1 internal DC LF. |
| **DC LF** | `dc=True` | only used internally by the lib for screening (`use_dc=True` flag of `run_analysis_step2_discovery`) and by the recommender models that declare a DC scoring path. Never the path picked by the regular analysis endpoints. | linear, no reactive, ignores tap ratios; converges almost always; **not** suitable for thermal-overload arbitration close to limits. |
| **Initial LF** | `dc=False, init=DC_VALUES` (forced) | inside `SimulationEnvironment._ensure_valid_state` — called once at network load and again on `reset()`. | avoids the spurious "Voltage magnitude is undefined" warning + retry on cold load. |
What `fast=True` actually disables (mirrors lib doc § 3.3):
| OpenLoadFlow outer loop | `fast=True` |
|---|---|
| `IncrementalTransformerVoltageControl` (tap-changer voltage regulation) | **OFF** |
| `IncrementalShuntVoltageControl` (shunt section switching) | **OFF** |
| Phase-shifter regulation (`phase_shifter_regulation_on`) | unchanged (ON) |
| Reactive limits enforcement (`use_reactive_limits`) | unchanged (ON) |
| Distributed slack (`distributed_slack`) | unchanged (ON) |
Physical meaning: in `fast` mode the network is still a valid AC
solution **assuming taps and shunt sections stay at their input
values**. Voltages and reactive flows are slightly off but the
thermal-overload signal (rho on each branch) is usually within a few
percent of the slow-mode answer — good enough for the recommender's
ranking step. The retry tree (lib doc § 3.7) escalates to slow / DC
automatically when this approximation breaks down.
When to flip the UI checkbox off:
- A specific action keeps coming back with `non_convergence`
in fast mode on a small or medium grid (slow mode often resolves
it, at the cost of analysis runtime — count ~1.5–3× longer on the
French grid).
- The operator wants reference-grade `max_rho` for an action under
comparison (e.g. exporting to a third-party study). Slow + the
`DC_VALUES` fallback is the closest you can get to the lib's
reference `run_load_flow` semantics.
> **Caveat — the diagram / overload path on large grids.** The N-1
> *diagram* path (`recommender_service._get_contingency_variant` →
> `_run_ac_with_fallback`) runs `create_olf_rte_parameter()` from the
> lib's `make_env_utils`, which keeps OpenLoadFlow's **stock 20
> `maxOuterLoopIterations`** — unlike the analysis path's
> `_create_default_lf_parameters` (raised to **100**, lib doc § 3.6).
> On the full French grid this made **slow mode diverge**
> (`MAX_ITERATION_REACHED` → null flows) for `P.SAOL31RONCI`, while
> fast mode converges at 95.4 %. **`_run_ac_with_fallback` now forces
> `maxOuterLoopIterations=100`** so the diagram path converges in slow
> mode too (95.84 %). The exact Hades2 reference (96.1 %) still needs
> `pypowsybl-rte` / the `parameters_hades2` recipe and
> `SecurityAnalysis`. Full write-up + reproduction:
> [`ronci-beon-reproducibility.md`](ronci-beon-reproducibility.md).
## 5. Manual action simulation
`POST /api/simulate-manual-action {action_id, disconnected_elements, action_content?, lines_overloaded?, target_mw?, target_tap?}`
Used when the operator stars an action that wasn't in the prioritized
list, or applies an LS / curtail / redispatch / PST action with an
explicit setpoint. Detailed flow in
`SimulationMixin.simulate_manual_action` (orchestrator) +
`simulation_helpers.py` (14 pure helpers).
Pipeline:
1. **Normalise** `disconnected_elements` (canonical contingency id
list).
2. `_ensure_contingency_state_ready(contingency)` — same guard as
step 1.
3. **Resolve the action**:
- Split combined ids (`action1+action2+…`).
- Inject restored topology entries from `action_content` if any
(session reload path).
- Build dynamic LS / curtail / redispatch / PST / reconnect actions
if the id is not already in `_dict_action` (heuristic action
generation). Redispatch is an **injection** action (signed
`set_gen_p` delta — raise *or* lower a dispatchable generator,
setpoint via `compute_redispatch_setpoint`), distinct from
curtailment, which only reduces generation to a target MW.
4. **Pick the obs**: prefers `_analysis_context.obs_simu_defaut` to
keep numerical alignment with step 1 and the library's
`compute_superposition`. Falls back to a fresh
`simulate_contingency_pypowsybl` if the context is empty.
5. **Apply setpoint overrides**: `target_mw` rewrites
`set_load_p[load_id]` / `set_gen_p[gen_id]`; `target_tap` rewrites
`pst_tap[transformer_id]`. The promoted action is reinjected into
`_dict_action` so future simulations see the updated setpoint.
6. **Build the combined action object** (via the lib's
`ActionSpace`).
7. **Simulate**: `obs_simu_defaut.simulate(action, keep_variant=True,
fast_mode=config.PYPOWSYBL_FAST_MODE)`. The kept variant is owned
by the library; the backend does NOT clean it up — see the
variant-cleanup note in `simulation_mixin.py`.
8. **Compute metrics** (`compute_action_metrics`): `rho_before`,
`rho_after`, `max_rho`, `max_rho_line`, `is_rho_reduction`,
`is_islanded`, `disconnected_mw`, `n_components`,
`lines_overloaded_after`.
9. **Normalise non-convergence**: `info_action["exception"]` is
inspected by `normalise_non_convergence` — produces a readable
string like `"Load flow did not converge: ComponentStatus.MAX_ITERATION_REACHED"`
that the UI surfaces on the action card.
10. **Enrich** with curtailment / load-shedding / PST details, register
on `_dict_action` (for subsequent superposition computations),
serialise with `serialize_action_result`.
Same `fast_mode` resolution rule as § 4.4: the simulate call uses
`fast_mode=config.PYPOWSYBL_FAST_MODE` (UI checkbox), which is `True`
by default. The lib's `_run_ac_with_init_fallback` handles the
retry-with-DC_VALUES escalation transparently — the backend just
surfaces the final status (CONVERGED or one of the failure modes
listed in § 9) on the action card.
### 5.1 `_analysis_context` re-use — why
The library's pypowsybl backend caches an N-1 variant on
`obs_simu_defaut._variant_id` when `keep_variant=True`. The backend's
`simulate_manual_action` MUST use the **same** obs, otherwise a fresh
`env.get_obs()` returns an N-state observation (the grid2op ↔ pypowsybl
bridge doesn't re-sync `get_obs()` to the current `working_variant`),
and the subsequent `obs.simulate(...)` would branch from N — not from
N-1. Result: `max_rho` would drift from the library's own simulation,
making session reload / superposition diverge.
The "used_context_obs" branch documents this:
```python
ctx_obs_n1 = self._obs_n1_from_context()
ctx_obs_n = ctx.get("obs")
used_context_obs = ctx_obs_n1 is not None and ctx_obs_n is not None
if used_context_obs:
obs, obs_simu_defaut = ctx_obs_n, ctx_obs_n1 # numerically aligned with step1
```
## 6. Superposition (`POST /api/compute-superposition`)
Given two prioritized actions `(action1, action2)`, estimate the
combined `max_rho` without simulating `action1+action2` explicitly.
Uses the per-line rho deltas captured during step 2 reassessment and
the superposition theorem:
```
rho_combined[line] ≈ rho_N1[line]
+ (rho_action1[line] - rho_N1[line])
+ (rho_action2[line] - rho_N1[line])
```
Hypotheses:
- Linear superposition holds well in DC, and **approximately** in AC
for small perturbations.
- The two actions must be reassessed against the **same** N-1
baseline. The backend's `compute_superposition` re-runs the per-
action simulation via `simulate_manual_action` if either member is
missing from `_last_result` — same `_analysis_context` re-use
semantics as § 5.
- **Injection-action pairs are now estimable.** Pairs that mix a
topology action with an **injection** action (load shedding,
curtailment, redispatch) — previously simulation-only — are handled
through the **Generalized Superposition Theorem (GST)** path. The
backend tags each member via
`simulation_helpers.is_injection_action(action_id, dict_action,
classifier)` and forwards `act1_is_injection` / `act2_is_injection`
to the library's `compute_combined_pair_gst` (plumbed through
`compute_combined_pair_superposition`); the injection action is
returned with `beta = 1.0`.
The full math + caveats are documented at
[`docs/features/combined-actions.md`](../features/combined-actions.md)
+ the lib's `docs/superposition_module.md`.
## 7. Modes exposed to the UI
The Settings modal surfaces these knobs (mapped to `ConfigRequest`):
| UI field | Backend field | Lib config global | Default | Effect |
|---|---|---|---|---|
| **Network path** | `network_path` | — | — | `.xiidm` file to load. |
| **Action file path** | `action_file_path` | — | — | JSON action dictionary. |
| **Layout path** | `layout_path` | `LAYOUT_FILE_PATH` | — | `grid_layout.json` for NAD `fixed_positions`. |
| **Output folder** | `output_folder_path` | — | `./sessions` | session save target. |
| **Lines monitoring path** | `lines_monitoring_path` | `LINES_TO_MONITOR_PATH` | — | optional whitelist of branches to monitor (empty = all). |
| **Recommender model** | `model` | — | `"expert"` | active `RecommenderModel`. See `/api/models`. |
| **Compute overflow graph** | `compute_overflow_graph` | `DO_CONSOLIDATE_GRAPH` | `True` | toggles the overflow-graph build in step 2. Needed by Expert + every model declaring `requires_overflow_graph=True`. |
| **Use pypowsybl fast mode** | `pypowsybl_fast_mode` | `PYPOWSYBL_FAST_MODE` | `True` | drives `obs.simulate(..., fast_mode=...)` across every analysis / manual / superposition call. See § 4.4 above for the inline cheat-sheet and the lib doc [§ 3.3](https://github.com/marota/Expert_op4grid_recommender/blob/main/docs/architecture/simulation-pipeline.md#33-fast-vs-slow--what-gets-disabled) for what `fast=True` actually disables in OpenLoadFlow. |
| **Monitoring factor** | `monitoring_factor` | `MONITORING_FACTOR_THERMAL_LIMITS` | `0.95` | thermal-limit multiplier; 0.95 means "alarm at 95 % of `permanent_limit`". |
| **Pre-existing overload threshold** | `pre_existing_overload_threshold` | `PRE_EXISTING_OVERLOAD_WORSENING_THRESHOLD` | `0.02` | min. delta on rho to count as "worsening" a pre-existing overload. |
| **n_prioritized_actions** | `n_prioritized_actions` | `N_PRIORITIZED_ACTIONS` | `10` | cap on the action list returned by the model. |
| **min_line_reconnections** | `min_line_reconnections` | `MIN_LINE_RECONNECTIONS` | `2` | min. number of reconnection actions in the prioritized list. |
| **min_close_coupling** | `min_close_coupling` | `MIN_CLOSE_COUPLING` | `1``3` | min. number of node-merging (close-coupler) actions. |
| **min_open_coupling** | `min_open_coupling` | `MIN_OPEN_COUPLING` | `2` | min. number of node-splitting (open-coupler) actions. |
| **min_line_disconnections** | `min_line_disconnections` | `MIN_LINE_DISCONNECTIONS` | `2``3` | min. number of disconnection actions. |
| **min_pst** | `min_pst` | `MIN_PST` | `1` | min. number of phase-shifter tap actions. |
| **min_load_shedding** | `min_load_shedding` | `MIN_LOAD_SHEDDING` | `1`–`2` | min. number of load-shedding actions. |
| **min_renewable_curtailment_actions** | `min_renewable_curtailment_actions` | `MIN_RENEWABLE_CURTAILMENT_ACTIONS` | `1` | min. number of renewable curtailment actions. |
| **Ignore reconnections** | `ignore_reconnections` | `IGNORE_RECONNECTIONS` | `False` | when `True`, suppress all reco actions from the candidate set. |
### 7.1 Selecting a recommender model
`POST /api/recommender-model {model: "expert" | "random_overflow" | …}` swaps
the active `RecommenderModel` without reloading the network or the
action dictionary. Preserves `_last_step2_signature` so the overflow
graph cache is reused — a model swap re-runs only the recommendation
+ reassessment phases.
Available models depend on the lib registry: `expert` (default,
rule-based, `requires_overflow_graph=True`), `random_overflow`
(uniform sampling among the candidates surfaced by the overflow
graph), and any third-party model that implements the
`RecommenderModel` ABC.
## 8. Overflow graph modes
Two layouts are available, switchable on the fly via
`POST /api/regenerate-overflow-graph {mode}`:
- **`hierarchical`** (default) — graphviz `dot` layout produced by
the lib during `run_analysis_step2`. Topological, no geographic
meaning.
- **`geo`** — pure SVG transform (`services/analysis/overflow_geo_transform.py`)
that repositions node groups using `grid_layout.json` coordinates
and redraws edges as straight lines. Computed on first click, then
cached. Cache is cleared on every fresh `run_analysis_step2` and on
`reset()`.
The interactive HTML viewer (`services/overflow_overlay.py`) is
injected on top of the upstream `expert_op4grid_recommender` SVG
viewer at serve time: it adds the Co-Study4Grid action-pin overlay,
the layer-toggle bar, and the `cs4g:filters` postMessage bridge.
## 9. Convergence & failure semantics
| Symptom | Where it surfaces | Meaning |
|---|---|---|
| `non_convergence: "Load flow did not converge: ComponentStatus.MAX_ITERATION_REACHED"` | action card `non_convergence` field | OpenLoadFlow ran out of outer-loop iterations (cap raised to 100 in `0.2.2.post2`). Usually means a numerical instability on the post-action topology. |
| `non_convergence: "Load flow did not converge: ComponentStatus.FAILED"` | action card | OpenLoadFlow's voltage-control consistency check tripped on "Unrealistic state". The DC_VALUES fallback in `_run_ac_with_init_fallback` should now catch this. |
| `is_islanded: true` | action card | the action disconnected part of the network from the slack-bus component. `disconnected_mw` reports the dropped active load. |
| `is_rho_reduction: false` | action card | the action did NOT reduce `max_rho` on at least one of the monitored lines. |
| Empty `prioritized_actions` | step 2 result | every candidate either didn't converge or didn't reduce rho. Check the analysis logs for per-candidate `non_convergence` reasons. |
## 10. What the backend does NOT do
- It does not run dynamic simulations — every LF is steady-state.
- It does not enforce voltage-magnitude or reactive-reserve limits as
failure conditions. Only `rho > 1` triggers the overload flag.
- It does not retry an action with a different `fast_mode` or
`voltage_init_mode` on its own — the retry is implemented at the
library's `run_load_flow` level (see lib doc §§ 3.5–3.7).
- It does not parallelise the per-action reassessment (the library
runs candidates sequentially over the shared `pp.network.Network`).
Parallelism is bounded by the FastAPI thread pool and the
`allow_variant_multi_thread_access=False` choice in
`network_service` (see `network_service.py:30-43`).
## 11. Cross-references
- `expert_backend/services/analysis_mixin.py``run_analysis_step1`,
`run_analysis_step2`.
- `expert_backend/services/simulation_mixin.py``simulate_manual_action`,
`compute_superposition`.
- `expert_backend/services/recommender_service.py` — singleton +
lifecycle (`__init__`, `reset`, `update_config`).
- `expert_backend/services/simulation_helpers.py` — 14 pure helpers
used by the simulation mixin.
- [`docs/features/save-results.md`](../features/save-results.md) —
session save / reload contract.
- [`docs/features/combined-actions.md`](../features/combined-actions.md)
— combined-action UI + superposition contract.
- [`docs/features/interactive-overflow-analysis.md`](../features/interactive-overflow-analysis.md)
— overflow viewer + layer toggles.
- [`docs/features/state-reset-and-confirmation-dialogs.md`](../features/state-reset-and-confirmation-dialogs.md)
`reset()` cache list + confirmation-dialog policy.
- [`Expert_op4grid_recommender/docs/architecture/simulation-pipeline.md`](https://github.com/marota/Expert_op4grid_recommender/blob/main/docs/architecture/simulation-pipeline.md)
— physical / numerical layer (LF modes, voltage init, variants,
retry strategy).
- [`Expert_op4grid_recommender/docs/release-notes/v0.2.2.post2.md`](https://github.com/marota/Expert_op4grid_recommender/blob/main/docs/release-notes/v0.2.2.post2.md)
— rationale for the non-converged-status fallback + outer-loop bump.