// Copyright (c) 2025-2026, RTE (https://www.rte-france.com) // This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. // If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, // you can obtain one at http://mozilla.org/MPL/2.0/. // SPDX-License-Identifier: MPL-2.0 // This file is part of Co-Study4Grid a Power Grid Study tool Assistant Interface to help solve contigencies for a grid state under study. export interface ConfigRequest { network_path: string; action_file_path: string; min_line_reconnections: number; min_close_coupling: number; min_open_coupling: number; min_line_disconnections: number; min_pst?: number; min_load_shedding?: number; min_renewable_curtailment_actions?: number; min_redispatch?: number; allowed_action_types?: string[]; n_prioritized_actions: number; lines_monitoring_path?: string; monitoring_factor: number; pre_existing_overload_threshold?: number; ignore_reconnections?: boolean; pypowsybl_fast_mode?: boolean; layout_path?: string; // Pluggable recommender selection. ``model`` is the registry key // (e.g. "expert", "random", "random_overflow" or a third-party // plugin name); ``compute_overflow_graph`` toggles the (expensive) // step-2 graph build for models that don't strictly require it. model?: string; compute_overflow_graph?: boolean; } export interface AnalysisRequest { /** * Ordered list of element IDs to disconnect simultaneously. A * single-item list is the legacy N-1 case; longer lists drive * N-K (multi-element) contingency studies. */ disconnected_elements: string[]; } export interface ActionTopology { lines_ex_bus: Record; lines_or_bus: Record; gens_bus: Record; loads_bus: Record; pst_tap?: Record; substations?: Record; switches?: Record; loads_p?: Record; gens_p?: Record; // Backend-supplied VoltageLevelId hint surfaced from the // dict_action entry for pypowsybl switch-based / coupling // actions. Highest-priority signal for the Action Overview pin // anchor and the ActionCard VL chip. voltage_level_id?: string; } export interface LoadSheddingDetail { load_name: string; voltage_level_id: string | null; shedded_mw: number; } export interface CurtailmentDetail { gen_name: string; voltage_level_id: string | null; curtailed_mw: number; } export interface PstDetail { pst_name: string; tap_position: number; low_tap: number | null; high_tap: number | null; } export interface RedispatchDetail { gen_name: string; voltage_level_id: string | null; // Signed MW change applied to the generator: > 0 raises production, // < 0 lowers it. delta_mw: number; // Resulting production magnitude (MW) after the redispatch. target_mw: number; direction: 'up' | 'down'; // Current production (MW) before the redispatch. current_mw?: number; // Maximum further raise / lower (MW) bounded by the generator's // [min_p, max_p] active-power limits. Null when unknown. max_raise_mw?: number | null; max_lower_mw?: number | null; } export interface ActionDetail { description_unitaire: string; rho_before: number[] | null; rho_after: number[] | null; max_rho: number | null; max_rho_line: string; is_rho_reduction: boolean; estimated_max_rho?: number | null; estimated_max_rho_line?: string; is_manual?: boolean; is_estimated?: boolean; is_islanded?: boolean; n_components?: number; disconnected_mw?: number; non_convergence?: string | null; action_topology?: ActionTopology; lines_overloaded_after?: string[]; /** * ``{line_name: live_end_reactive_mvar}`` for still-"overloaded" lines the * action leaves open at ONE end with a loading above ~1 %. Such a line is * out of service for active power (the SLD / NAD show p = 0) but its * capacitance draws reactive charging current from the live end, so its * current-based loading stays non-zero (the reported ~33 %). The ActionCard * annotates the loading with the reactive amount so it reads as charging * current, not a residual overload. Backend: ``half_open_branch_reactive_from_obs``. */ half_open_overloads?: Record; load_shedding_details?: LoadSheddingDetail[]; curtailment_details?: CurtailmentDetail[]; redispatch_details?: RedispatchDetail[]; pst_details?: PstDetail[]; /** * Provenance of the action card — distinct from `is_manual`, which * is an overloaded UI-state flag (it's also stamped `true` when the * operator stars a recommender suggestion). `origin` records where * the action *came from* and never changes after creation: * - `"user"` — the operator simulated it themselves (manual * search dropdown, "Make a first guess"). * - `` — produced / scored by a recommender model * (e.g. `"expert"`, `"random_overflow"`); this * is the `active_model` echoed by the step-2 * `result` event, and also covers an * unsimulated pin the operator materialised * (it was scored by that model). * Optional for backward compat: legacy sessions that predate the * field get an `origin` derived from their saved status flags on * reload (see `useSession.handleRestoreSession`). */ origin?: string; } export interface CombinedAction { action1_id: string; action2_id: string; betas: number[]; p_or_combined: number[]; max_rho: number; max_rho_line: string; is_rho_reduction: boolean; description: string; rho_after: number[]; rho_before: number[]; is_islanded?: boolean; disconnected_mw?: number; estimated_max_rho?: number | null; estimated_max_rho_line?: string; target_max_rho?: number | null; target_max_rho_line?: string; error?: string; } export interface AnalysisResult { pdf_path: string | null; pdf_url: string | null; actions: Record; action_scores?: Record; mw_start?: Record; tap_start?: Record }>; lines_overloaded: string[]; combined_actions?: Record; message: string; dc_fallback: boolean; lines_we_care_about?: string[]; computed_pairs?: Record; // Identifier of the recommender model that produced the // suggestions in this result (e.g. "expert", "random", // "random_overflow", or a third-party plugin name). Echoed by // the backend in every `result` SSE event from // `/api/run-analysis-step2`; persisted in the saved session so // a reload shows which model was active for the run. active_model?: string; // Whether the step-2 overflow-graph build actually ran for this // analysis. True when the chosen model declares // `requires_overflow_graph=true` OR the operator opted in via // the Compute Overflow Graph toggle. compute_overflow_graph?: boolean; // Per-stage execution times (seconds), reported by the backend. // `overflow_graph_time` is null when the active model does not // consume the overflow graph (no time was spent building it); it is // 0.0 on a cached re-run, so a model swap can be distinguished from // a fresh run. Used by the ActionFeed reminder and the iframe's // header to show the operator how long each stage took. overflow_graph_time?: number | null; action_prediction_time?: number; assessment_time?: number; // Step 1 (contingency simulation + overload detection) ran in a // separate HTTP call before step 2. The backend echoes its // wall-clock here so the breakdown can show the full pipeline. step1_time?: number | null; // Co-Study4Grid post-processing after assessment (action enrichment // + combined-pair target_max_rho augmentation + MW-start scoring). enrichment_time?: number; // Frontend wall-clock from the "Analyze & Suggest" click until the // result event arrives ("Display N prioritized actions" appears). // Captures backend stages + network round-trip + NDJSON streaming // overhead. Difference vs. the sum of the per-stage timings above // is the "transit" overhead. wall_clock_time?: number; // Present only when the contingency islands a radial ("antenne") // pocket: the overflow graph is a synthetic downstream graph of the // disconnected zone and recommendations are injection-only (load // shedding / curtailment / redispatch). Null/absent otherwise. antenna_meta?: AntennaMeta | null; } // Describes the disconnected radial pocket of an islanded-contingency // ("antenne") analysis. Mirrors the recommender's antenna_meta dict. export interface AntennaMeta { constraint_line_name: string; root_sub_name: string; antenna_sub_names: string[]; n_subs: number; total_prod_mw: number; total_load_mw: number; net_mw: number; // "producer" (net export → curtailment / redispatch-down) or // "consumer" (net import → load shedding / redispatch-up). direction: 'producer' | 'consumer'; } export interface BranchResponse { branches: string[]; /** Optional mapping from element ID to human-readable display name. */ name_map?: Record; } /** * Mapping from element/VL ID to a human-readable display name. * Used throughout the UI to show real substation/circuit names. */ export type NameMap = Record; export interface DiagramData { svg: string | SVGSVGElement; metadata: unknown; lf_converged?: boolean; lf_status?: string; action_id?: string; flow_deltas?: Record; reactive_flow_deltas?: Record; asset_deltas?: Record; originalViewBox?: ViewBox | null; lines_overloaded?: string[]; lines_overloaded_rho?: number[]; } export interface FlowDelta { delta: number; category: 'positive' | 'negative' | 'grey'; flip_arrow?: boolean; } export interface AssetDelta { delta_p: number; delta_q: number; category: 'positive' | 'negative' | 'grey'; category_p?: 'positive' | 'negative' | 'grey'; category_q?: 'positive' | 'negative' | 'grey'; } export interface ViewBox { x: number; y: number; w: number; h: number; } export interface NodeMeta { equipmentId: string; svgId: string; x: number; y: number; legendSvgId?: string; legendEdgeSvgId?: string; [key: string]: unknown; } export interface EdgeInfoMeta { svgId: string; infoType?: string; direction?: string; externalLabel?: string; } export interface EdgeMeta { equipmentId: string; svgId: string; node1: string; node2: string; edgeInfo1?: EdgeInfoMeta; edgeInfo2?: EdgeInfoMeta; [key: string]: unknown; } export interface MetadataIndex { nodesByEquipmentId: Map; nodesBySvgId: Map; edgesByEquipmentId: Map; edgesByNode: Map; /** VL text-label (name box) svgId → its VL NodeMeta, so the label box can * be interactive (open the SLD) exactly like the VL disk. Optional because * the many hand-built MetadataIndex test fixtures omit it. */ textNodesBySvgId?: Map; } export interface DiagramPatch { patchable: boolean; reason?: string; contingency_id?: string; action_id?: string; lf_converged: boolean; lf_status: string; non_convergence?: string | null; disconnected_edges?: string[]; absolute_flows?: { p1: Record; p2: Record; q1: Record; q2: Record; vl1: Record; vl2: Record; }; lines_overloaded?: string[]; lines_overloaded_rho?: number[]; flow_deltas?: Record; reactive_flow_deltas?: Record; asset_deltas?: Record; vl_subtrees?: Record; }>; meta?: { base_state?: 'N' | 'contingency'; elapsed_ms?: number; }; } export type TabId = 'n' | 'contingency' | 'action' | 'overflow'; export interface SettingsBackup { networkPath?: string; actionPath?: string; outputFolderPath?: string; minLineReconnections: number; minCloseCoupling: number; minOpenCoupling: number; minLineDisconnections: number; minLoadShedding: number; minRenewableCurtailmentActions: number; minRedispatch: number; allowedActionTypes: string[]; nPrioritizedActions: number; linesMonitoringPath: string; monitoringFactor: number; preExistingOverloadThreshold: number; ignoreReconnections?: boolean; pypowsyblFastMode?: boolean; layoutPath?: string; } export interface RecommenderDisplayConfig { minLineReconnections: number; minCloseCoupling: number; minOpenCoupling: number; minLineDisconnections: number; minPst: number; minLoadShedding: number; minRenewableCurtailmentActions: number; minRedispatch: number; nPrioritizedActions: number; ignoreReconnections: boolean; } export interface AvailableAction { id: string; description: string; type?: string; } export type SldTab = 'n' | 'contingency' | 'action'; export interface SldFeederNode { id: string; equipmentId: string; componentType?: string; direction?: string; } /** * Baseline descriptor for one editable active-power injection (a load or * generator) on the displayed SLD. Drives the interactive injection-edit * bubble in ``useSldTopologyEdit`` / ``SldInjectionPopover``: the operator * retunes ``p`` and the user-built action sends back the absolute setpoint * as ``{loads_p|gens_p: {id: MW}}``. Backend populates this on every SLD * endpoint via ``extract_vl_injections``. */ export interface VlInjection { kind: 'generator' | 'load'; /** Current active-power setpoint (MW): ``target_p`` (gen) / ``p0`` (load). */ p: number | null; /** Generator active-power capability bounds (MW); absent for loads. */ min_p?: number | null; max_p?: number | null; /** Generator energy source (e.g. ``WIND`` / ``NUCLEAR``); absent for loads. */ energy_source?: string; } /** * Identity + display label for one branch feeder on the displayed SLD. * Backend populates this on every SLD endpoint via ``build_feeder_labels``. * * ``label`` is the **name of the voltage level at the OTHER end** of the * branch (+ a parallel-circuit index when several branches of this VL reach * the same far-end VL) — far more interpretable than pypowsybl's default raw * IIDM branch id. ``name`` carries the branch's friendly / operator name * (e.g. ``MARSIL61PRAGN``) so an overloaded line reported by friendly name * can be mapped back to its IIDM-id-keyed SLD cell for the overload halo. */ export interface FeederLabel { /** Branch friendly (operator / grid2op) name; ``null`` when none. */ name: string | null; /** Voltage-level id at the far end of the branch; ``null`` when unknown. */ other_vl: string | null; /** Display label (far-end VL name + index); ``null`` → keep the default id. */ label: string | null; } export interface VlOverlay { vlName: string; actionId: string | null; svg: string | null; sldMetadata: string | null; loading: boolean; error: string | null; tab: SldTab; flow_deltas?: Record; reactive_flow_deltas?: Record; asset_deltas?: Record; changed_switches?: Record; /** * Branch feeder identities + relabel targets for the displayed VL, keyed * by IIDM equipment id. Drives the feeder relabelling (other-end VL name) * and the friendly-name → IIDM-id resolution that lets the overload halo * land on the right cell. Backend populates via ``build_feeder_labels``. */ feeder_labels?: Record; /** * Baseline switch states for the displayed VL (``{switch_id: * is_open}``). Drives the interactive SLD-edit feature in * ``useSldTopologyEdit``: only switches present in this map are * editable, and a toggle is computed as ``user_state !== baseline``. * Backend populates this on every SLD endpoint via * ``extract_vl_switch_states``. */ switch_states?: Record; /** * Baseline active-power injections (loads + generators) for the * displayed VL, keyed by equipment id. Drives the interactive * injection-edit bubble: only equipment present here is editable, and * a staged change is computed as ``user_setpoint !== baseline.p``. * Backend populates this on every SLD endpoint via * ``extract_vl_injections``. */ injections?: Record; } // ===== Session Save ===== export interface SavedActionStatus { is_selected: boolean; is_suggested: boolean; is_rejected: boolean; is_manually_simulated: boolean; } export interface SavedActionEntry { description_unitaire: string; rho_before: number[] | null; rho_after: number[] | null; max_rho: number | null; max_rho_line: string; is_rho_reduction: boolean; is_estimated?: boolean; estimated_max_rho?: number | null; estimated_max_rho_line?: string; is_islanded?: boolean; n_components?: number; disconnected_mw?: number; non_convergence?: string | null; action_topology?: ActionTopology; lines_overloaded_after?: string[]; /** * ``{line_name: live_end_reactive_mvar}`` for still-"overloaded" lines the * action leaves open at ONE end with a loading above ~1 %. Such a line is * out of service for active power (the SLD / NAD show p = 0) but its * capacitance draws reactive charging current from the live end, so its * current-based loading stays non-zero (the reported ~33 %). The ActionCard * annotates the loading with the reactive amount so it reads as charging * current, not a residual overload. Backend: ``half_open_branch_reactive_from_obs``. */ half_open_overloads?: Record; load_shedding_details?: LoadSheddingDetail[]; curtailment_details?: CurtailmentDetail[]; redispatch_details?: RedispatchDetail[]; pst_details?: PstDetail[]; /** * Provenance of the action — `"user"` or a recommender model id. * Mirrors `ActionDetail.origin`. Optional: legacy session dumps * that predate the field get an `origin` derived from the saved * status flags + `analysis.active_model` on reload. */ origin?: string; status: SavedActionStatus; } export interface SavedCombinedAction { action1_id: string; action2_id: string; betas: number[]; max_rho: number; max_rho_line: string; is_rho_reduction: boolean; description: string; estimated_max_rho?: number | null; estimated_max_rho_line?: string; is_islanded?: boolean; disconnected_mw?: number; simulated_max_rho?: number | null; simulated_max_rho_line?: string; is_simulated: boolean; } export interface SessionResult { saved_at: string; configuration: { network_path: string; action_file_path: string; layout_path: string; min_line_reconnections: number; min_close_coupling: number; min_open_coupling: number; min_line_disconnections: number; min_pst: number; min_load_shedding: number; min_renewable_curtailment_actions?: number; min_redispatch?: number; allowed_action_types?: string[]; n_prioritized_actions: number; lines_monitoring_path: string; monitoring_factor: number; pre_existing_overload_threshold: number; ignore_reconnections: boolean; pypowsybl_fast_mode: boolean; // Registry key of the recommender model selected at the time // the session was saved (e.g. "expert", "random", // "random_overflow"). Persisted so a reload shows which model // produced the suggestions. model?: string; // Whether the operator had the Compute Overflow Graph toggle // on at save time. For models with // `requires_overflow_graph=true` this is always effectively // true; for opt-in models it captures the user choice. compute_overflow_graph?: boolean; }; contingency: { disconnected_elements: string[]; disconnected_element?: string; selected_overloads: string[]; monitor_deselected: boolean; additional_lines_to_cut?: string[]; }; overloads: { n_overloads: string[]; n1_overloads: string[]; resolved_overloads: string[]; n_overloads_rho?: number[]; n1_overloads_rho?: number[]; }; overflow_graph: { pdf_url: string | null; pdf_path: string | null; } | null; analysis: { message: string; dc_fallback: boolean; action_scores: Record> | undefined; actions: Record; combined_actions: Record; lines_we_care_about?: string[] | null; computed_pairs?: Record | null; // Model that produced this analysis result. Mirrors // `configuration.model` but captured from the result event // (= the model the backend actually executed, which may // differ from the configured one when an unknown name // silently falls back to the default). active_model?: string | null; // Whether step-2 overflow graph was actually computed for // this run. Useful when reloading a session to know whether // the Overflow Analysis tab will have content. compute_overflow_graph?: boolean | null; // Per-stage execution times (seconds), captured at run time. // Optional for backward compatibility with sessions saved before // the breakdown was added. overflow_graph_time?: number | null; action_prediction_time?: number | null; assessment_time?: number | null; step1_time?: number | null; enrichment_time?: number | null; wall_clock_time?: number | null; } | null; interaction_log?: InteractionLogEntry[]; } // ===== Interaction Logging ===== export type InteractionType = | 'config_loaded' | 'settings_opened' | 'settings_tab_changed' | 'settings_applied' | 'settings_cancelled' | 'path_picked' | 'contingency_selected' | 'contingency_confirmed' | 'contingency_element_added' | 'contingency_element_removed' | 'contingency_applied' | 'analysis_step1_started' | 'analysis_step1_completed' | 'overload_toggled' | 'additional_line_to_cut_toggled' | 'recommender_model_changed' | 'suggested_actions_cleared' | 'analysis_step2_started' | 'analysis_step2_completed' | 'prioritized_actions_displayed' | 'action_selected' | 'action_deselected' | 'action_favorited' | 'action_unfavorited' | 'action_rejected' | 'action_unrejected' | 'manual_action_simulated' | 'action_mw_resimulated' | 'pst_tap_resimulated' | 'combine_modal_opened' | 'combine_modal_closed' | 'combine_pair_toggled' | 'combine_pair_estimated' | 'combine_pair_simulated' | 'diagram_tab_changed' | 'tab_detached' | 'tab_reattached' | 'tab_tied' | 'tab_untied' | 'view_mode_changed' | 'overflow_layout_mode_toggled' | 'overflow_pins_toggled' | 'overflow_pin_clicked' | 'overflow_pin_double_clicked' | 'overflow_layer_toggled' | 'overflow_select_all_layers' | 'overflow_node_double_clicked' | 'voltage_range_changed' | 'asset_clicked' | 'zoom_in' | 'zoom_out' | 'zoom_reset' | 'inspect_query_changed' | 'vl_names_toggled' | 'sld_overlay_opened' | 'sld_overlay_tab_changed' | 'sld_overlay_closed' | 'sld_edit_mode_toggled' | 'sld_switch_toggled' | 'sld_maneuver_removed' | 'sld_maneuver_focused' | 'sld_edit_reset' | 'sld_injection_staged' | 'sld_injection_removed' | 'sld_topology_simulated' | 'overview_shown' | 'overview_hidden' | 'overview_pin_clicked' | 'overview_pin_double_clicked' | 'overview_popover_closed' | 'overview_zoom_in' | 'overview_zoom_out' | 'overview_zoom_fit' | 'overview_inspect_changed' | 'overview_filter_changed' | 'overview_unsimulated_toggled' | 'overview_unsimulated_pin_simulated' | 'session_saved' | 'session_reload_modal_opened' | 'session_reloaded' | 'sidebar_collapsed_toggled' | 'contingency_clear_requested' | 'theme_toggled' | 'smooth_pan_zoom_toggled'; export interface InteractionLogEntry { seq: number; timestamp: string; type: InteractionType; details: Record; correlation_id?: string; duration_ms?: number; } // ===== Action Overview Filters ===== export type ActionSeverityCategory = 'green' | 'orange' | 'red' | 'grey'; export type ActionTypeFilterToken = 'all' | 'disco' | 'reco' | 'ls' | 'rc' | 'redispatch' | 'open' | 'close' | 'pst'; export interface ActionOverviewFilters { categories: Record; threshold: number; showUnsimulated: boolean; actionType: ActionTypeFilterToken; showCombinedOnly: boolean; } export interface UnsimulatedActionScoreInfo { type: string; score: number; mwStart?: number | null; tapStart?: { pst_name: string; tap: number; low_tap: number | null; high_tap: number | null; } | null; rankInType: number; countInType: number; maxScoreInType: number; } // ===================================================================== // Overflow-iframe postMessage envelope // ===================================================================== export interface OverflowIframeScreenRect { left: number; top: number; width: number; height: number; } export type IframeToParentMessage = | { type: 'cs4g:overlay-ready' } | { type: 'cs4g:pin-clicked'; actionId: string; screenRect?: OverflowIframeScreenRect; } | { type: 'cs4g:pin-double-clicked'; actionId: string; substation: string; } | { type: 'cs4g:overflow-unsimulated-pin-double-clicked'; actionId: string; } | { type: 'cs4g:overflow-filter-changed'; filters: ActionOverviewFilters; } | { type: 'cs4g:overflow-layer-toggled'; key: string; label: string; visible: boolean; } | { type: 'cs4g:overflow-select-all-layers'; visible: boolean; } | { type: 'cs4g:overflow-node-double-clicked'; name: string; } | { type: 'cs4g:overflow-pins-toggled'; enabled: boolean; }; export type IframeToParentMessageType = IframeToParentMessage['type']; export type ParentToIframeMessage = | { type: 'cs4g:pins'; visible: boolean; pins: ReadonlyArray; } | { type: 'cs4g:filters'; filters: ActionOverviewFilters; } | { // Total execution time (seconds) for creating + generating the // overflow analysis graph. Posted by the React parent once the // step-2 ``result`` event arrives — the overlay JS renders it // as a subtitle just below the sidebar ``

`` so the operator // can see how long the graph took to produce. type: 'cs4g:overflow-meta'; overflowGraphTime: number | null; } | { // Light/dark theme of the host app. Posted on overlay-ready and // whenever the parent's `` flips, so the // embedded overflow viewer can match the surrounding chrome. type: 'cs4g:theme'; theme: 'light' | 'dark'; };