Spaces:
Sleeping
Sleeping
| # 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. | |