Spaces:
Sleeping
Sleeping
| // 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. | |
| import type { AnalysisResult, SessionResult, SavedActionEntry, SavedCombinedAction, InteractionLogEntry } from '../types'; | |
| /** | |
| * All pieces of App state required to build a SavedSessionResult JSON snapshot. | |
| * Kept as a plain-data interface so the logic is easily unit-testable without | |
| * mounting the full React component tree. | |
| */ | |
| export interface SessionInput { | |
| // Configuration / paths | |
| networkPath: string; | |
| actionPath: string; | |
| layoutPath: string; | |
| minLineReconnections: number; | |
| minCloseCoupling: number; | |
| minOpenCoupling: number; | |
| minLineDisconnections: number; | |
| minPst: number; | |
| minLoadShedding: number; | |
| minRenewableCurtailmentActions: number; | |
| minRedispatch: number; | |
| allowedActionTypes: string[]; | |
| nPrioritizedActions: number; | |
| linesMonitoringPath: string; | |
| monitoringFactor: number; | |
| preExistingOverloadThreshold: number; | |
| ignoreReconnections: boolean; | |
| pypowsyblFastMode: boolean; | |
| // Pluggable-recommender selection. Captured at save time so a | |
| // reloaded session shows which model was active. Optional for | |
| // backwards compatibility with builders that haven't been updated | |
| // yet. | |
| recommenderModel?: string; | |
| computeOverflowGraph?: boolean; | |
| // Contingency | |
| selectedContingency?: string[]; | |
| selectedBranch?: string; | |
| selectedOverloads: Set<string>; | |
| monitorDeselected: boolean; | |
| committedAdditionalLinesToCut?: Set<string>; | |
| // Overload lists from diagrams | |
| nOverloads: string[]; | |
| n1Overloads: string[]; | |
| nOverloadsRho?: number[]; | |
| n1OverloadsRho?: number[]; | |
| // Analysis result (already merged from pendingAnalysisResult → result) | |
| result: AnalysisResult | null; | |
| // Action status tracking | |
| selectedActionIds: Set<string>; | |
| rejectedActionIds: Set<string>; | |
| manuallyAddedIds: Set<string>; | |
| suggestedByRecommenderIds: Set<string>; | |
| // Interaction log | |
| interactionLog: InteractionLogEntry[]; | |
| } | |
| /** | |
| * Builds a serialisable SessionResult snapshot from the current App state. | |
| */ | |
| export function buildSessionResult(input: SessionInput): SessionResult { | |
| const { | |
| networkPath, actionPath, layoutPath, | |
| minLineReconnections, minCloseCoupling, minOpenCoupling, minLineDisconnections, minPst, minLoadShedding, minRenewableCurtailmentActions, minRedispatch, allowedActionTypes, | |
| nPrioritizedActions, linesMonitoringPath, monitoringFactor, | |
| preExistingOverloadThreshold, ignoreReconnections, pypowsyblFastMode, | |
| recommenderModel, computeOverflowGraph, | |
| selectedContingency: contingencyInput, | |
| selectedBranch: legacyBranch, | |
| selectedOverloads, monitorDeselected, | |
| committedAdditionalLinesToCut, | |
| nOverloads, n1Overloads, | |
| nOverloadsRho, n1OverloadsRho, | |
| result, | |
| selectedActionIds, rejectedActionIds, manuallyAddedIds, suggestedByRecommenderIds, | |
| interactionLog, | |
| } = input; | |
| // Build combined_actions from the analysis result | |
| const savedCombinedActions: Record<string, SavedCombinedAction> = {}; | |
| if (result?.combined_actions) { | |
| for (const [id, ca] of Object.entries(result.combined_actions)) { | |
| const canonicalId = id.includes('+') ? id.split('+').map(p => p.trim()).sort().join('+') : id; | |
| const simData = result.actions[id] || result.actions[canonicalId]; | |
| const isSimulated = !!simData && !simData.is_estimated && simData.rho_after != null && simData.rho_after.length > 0; | |
| savedCombinedActions[id] = { | |
| action1_id: ca.action1_id, | |
| action2_id: ca.action2_id, | |
| betas: ca.betas, | |
| max_rho: ca.max_rho, | |
| max_rho_line: ca.max_rho_line, | |
| is_rho_reduction: ca.is_rho_reduction, | |
| description: ca.description, | |
| estimated_max_rho: ca.estimated_max_rho ?? ca.max_rho, | |
| estimated_max_rho_line: ca.estimated_max_rho_line ?? ca.max_rho_line, | |
| is_islanded: ca.is_islanded, | |
| disconnected_mw: ca.disconnected_mw, | |
| simulated_max_rho: isSimulated ? simData.max_rho : null, | |
| simulated_max_rho_line: isSimulated ? simData.max_rho_line : undefined, | |
| is_simulated: isSimulated, | |
| }; | |
| } | |
| } | |
| const analysis: SessionResult['analysis'] = result | |
| ? { | |
| message: result.message, | |
| dc_fallback: result.dc_fallback, | |
| action_scores: result.action_scores, | |
| lines_we_care_about: result.lines_we_care_about ?? null, | |
| computed_pairs: result.computed_pairs ?? null, | |
| // Persist the model the BACKEND actually executed (echoed in | |
| // the result event). Differs from `configuration.model` | |
| // (= what was requested) when an unknown name silently fell | |
| // back to the default — the active_model is the ground truth. | |
| active_model: result.active_model ?? null, | |
| // Whether step-2 overflow graph was computed for this run. | |
| // True for any model that declares requires_overflow_graph, | |
| // OR when the operator opted in. Persisted so a reload knows | |
| // whether the Overflow Analysis tab will have content. | |
| compute_overflow_graph: result.compute_overflow_graph ?? null, | |
| // Per-stage execution times (seconds). Persisted so a reload | |
| // shows the same "Suggestions produced by …" breakdown the | |
| // operator saw at run time. | |
| overflow_graph_time: result.overflow_graph_time ?? null, | |
| action_prediction_time: result.action_prediction_time ?? null, | |
| assessment_time: result.assessment_time ?? null, | |
| step1_time: result.step1_time ?? null, | |
| enrichment_time: result.enrichment_time ?? null, | |
| wall_clock_time: result.wall_clock_time ?? null, | |
| actions: Object.fromEntries( | |
| Object.entries(result.actions).map(([id, detail]): [string, SavedActionEntry] => [ | |
| id, | |
| { | |
| description_unitaire: detail.description_unitaire, | |
| rho_before: detail.rho_before, | |
| rho_after: detail.rho_after, | |
| max_rho: detail.max_rho, | |
| max_rho_line: detail.max_rho_line, | |
| is_rho_reduction: detail.is_rho_reduction, | |
| is_estimated: detail.is_estimated, | |
| non_convergence: detail.non_convergence, | |
| action_topology: detail.action_topology, | |
| estimated_max_rho: detail.estimated_max_rho, | |
| estimated_max_rho_line: detail.estimated_max_rho_line, | |
| is_islanded: detail.is_islanded, | |
| n_components: detail.n_components, | |
| disconnected_mw: detail.disconnected_mw, | |
| lines_overloaded_after: detail.lines_overloaded_after, | |
| half_open_overloads: detail.half_open_overloads, | |
| load_shedding_details: detail.load_shedding_details, | |
| curtailment_details: detail.curtailment_details, | |
| pst_details: detail.pst_details, | |
| redispatch_details: detail.redispatch_details, | |
| // Provenance: "user" or a recommender model id. | |
| // Restored verbatim on reload (with a derived | |
| // fallback for legacy dumps that lack the field). | |
| origin: detail.origin, | |
| status: { | |
| is_selected: selectedActionIds.has(id), | |
| is_suggested: suggestedByRecommenderIds.has(id), | |
| is_rejected: rejectedActionIds.has(id), | |
| is_manually_simulated: manuallyAddedIds.has(id), | |
| }, | |
| }, | |
| ]) | |
| ), | |
| combined_actions: savedCombinedActions, | |
| } | |
| : null; | |
| return { | |
| saved_at: new Date().toISOString(), | |
| configuration: { | |
| network_path: networkPath, | |
| action_file_path: actionPath, | |
| layout_path: layoutPath, | |
| min_line_reconnections: minLineReconnections, | |
| min_close_coupling: minCloseCoupling, | |
| min_open_coupling: minOpenCoupling, | |
| min_line_disconnections: minLineDisconnections, | |
| min_pst: minPst, | |
| min_load_shedding: minLoadShedding, | |
| min_renewable_curtailment_actions: minRenewableCurtailmentActions, | |
| min_redispatch: minRedispatch, | |
| allowed_action_types: allowedActionTypes, | |
| n_prioritized_actions: nPrioritizedActions, | |
| lines_monitoring_path: linesMonitoringPath, | |
| monitoring_factor: monitoringFactor, | |
| pre_existing_overload_threshold: preExistingOverloadThreshold, | |
| ignore_reconnections: ignoreReconnections, | |
| pypowsybl_fast_mode: pypowsyblFastMode, | |
| // Pluggable-recommender selection at save time. Optional — | |
| // older session builders that didn't pass these fields | |
| // emit `undefined`, which `JSON.stringify` drops from the | |
| // output (preserves backwards compatibility for the | |
| // existing session-reader code paths). | |
| ...(recommenderModel !== undefined ? { model: recommenderModel } : {}), | |
| ...(computeOverflowGraph !== undefined ? { compute_overflow_graph: computeOverflowGraph } : {}), | |
| }, | |
| contingency: (() => { | |
| const elements: string[] = contingencyInput && contingencyInput.length > 0 | |
| ? [...contingencyInput] | |
| : (legacyBranch ? [legacyBranch] : []); | |
| return { | |
| disconnected_elements: elements, | |
| ...(elements.length === 1 | |
| ? { disconnected_element: elements[0] } | |
| : {}), | |
| selected_overloads: Array.from(selectedOverloads), | |
| monitor_deselected: monitorDeselected, | |
| ...(committedAdditionalLinesToCut && committedAdditionalLinesToCut.size > 0 | |
| ? { additional_lines_to_cut: Array.from(committedAdditionalLinesToCut) } | |
| : {}), | |
| }; | |
| })(), | |
| overloads: { | |
| n_overloads: nOverloads, | |
| n1_overloads: n1Overloads, | |
| resolved_overloads: result?.lines_overloaded ?? [], | |
| ...(nOverloadsRho && nOverloadsRho.length === nOverloads.length ? { n_overloads_rho: nOverloadsRho } : {}), | |
| ...(n1OverloadsRho && n1OverloadsRho.length === n1Overloads.length ? { n1_overloads_rho: n1OverloadsRho } : {}), | |
| }, | |
| overflow_graph: result?.pdf_url | |
| ? { pdf_url: result.pdf_url, pdf_path: result.pdf_path ?? null } | |
| : null, | |
| analysis, | |
| interaction_log: interactionLog, | |
| }; | |
| } | |