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

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 for the contract and 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:

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.

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:

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

  • 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 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 13 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 23 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 12 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