From 98f07a8146880d1d05aeab2fddb33eed049443a6 Mon Sep 17 00:00:00 2001 From: Thang PHAM Date: Thu, 15 Jan 2026 20:01:12 +0100 Subject: [PATCH 1/6] Dynamic Margin Calculation: Functional implementation Signed-off-by: Thang PHAM --- .../use-all-computing-status.ts | 29 ++- .../single-line-diagram-content.tsx | 8 +- src/components/run-button-container.jsx | 65 ++++++- src/components/run-button.jsx | 9 + src/components/study-container.jsx | 9 +- src/components/utils/optional-services.ts | 1 + src/components/utils/running-status.ts | 15 ++ .../computation-debug-utils.ts | 42 ++++ .../computation-debug/use-debug-download.ts | 64 +++++++ .../use-debug-notification.ts | 58 ++++++ .../use-debug-subscription.ts | 46 +++++ src/hooks/use-computation-debug.ts | 180 ------------------ src/hooks/use-computation-results-count.ts | 9 + src/redux/reducer.ts | 12 +- src/services/dynamic-margin-calculation.ts | 34 ++++ .../study/dynamic-margin-calculation.ts | 177 +++++++++++++++++ .../study/dynamic-margin-calculation.type.ts | 47 +++++ src/translations/en.json | 14 ++ src/translations/fr.json | 14 ++ src/types/notification-types.ts | 3 + 20 files changed, 643 insertions(+), 193 deletions(-) create mode 100644 src/hooks/computation-debug/computation-debug-utils.ts create mode 100644 src/hooks/computation-debug/use-debug-download.ts create mode 100644 src/hooks/computation-debug/use-debug-notification.ts create mode 100644 src/hooks/computation-debug/use-debug-subscription.ts delete mode 100644 src/hooks/use-computation-debug.ts create mode 100644 src/services/dynamic-margin-calculation.ts create mode 100644 src/services/study/dynamic-margin-calculation.ts create mode 100644 src/services/study/dynamic-margin-calculation.type.ts diff --git a/src/components/computing-status/use-all-computing-status.ts b/src/components/computing-status/use-all-computing-status.ts index 594e203977..e7a5c88626 100644 --- a/src/components/computing-status/use-all-computing-status.ts +++ b/src/components/computing-status/use-all-computing-status.ts @@ -7,6 +7,7 @@ import { useComputingStatus } from './use-computing-status'; import { + getDynamicMarginCalculationRunningStatus, getDynamicSecurityAnalysisRunningStatus, getDynamicSimulationRunningStatus, getLoadFlowRunningStatus, @@ -28,11 +29,12 @@ import { fetchShortCircuitAnalysisStatus, } from '../../services/study/short-circuit-analysis'; import { fetchVoltageInitStatus } from '../../services/study/voltage-init'; -import { fetchLoadFlowStatus, fetchLoadFlowComputationInfos } from '../../services/study/loadflow'; +import { fetchLoadFlowComputationInfos, fetchLoadFlowStatus } from '../../services/study/loadflow'; import { OptionalServicesNames } from '../utils/optional-services'; import { useOptionalServiceStatus } from '../../hooks/use-optional-service-status'; import { fetchStateEstimationStatus } from '../../services/study/state-estimation'; import { fetchDynamicSecurityAnalysisStatus } from '../../services/study/dynamic-security-analysis'; +import { fetchDynamicMarginCalculationStatus } from '../../services/study/dynamic-margin-calculation'; import { NotificationType } from 'types/notification-types'; import { fetchPccMinStatus } from 'services/study/pcc-min'; @@ -62,6 +64,10 @@ const dynamicSecurityAnalysisStatusInvalidations = [ NotificationType.DYNAMIC_SECURITY_ANALYSIS_STATUS, NotificationType.DYNAMIC_SECURITY_ANALYSIS_FAILED, ]; +const dynamicMarginCalculationStatusInvalidations = [ + NotificationType.DYNAMIC_MARGIN_CALCULATION_STATUS, + NotificationType.DYNAMIC_MARGIN_CALCULATION_FAILED, +]; const voltageInitStatusInvalidations = [NotificationType.VOLTAGE_INIT_STATUS, NotificationType.VOLTAGE_INIT_FAILED]; const stateEstimationStatusInvalidations = [ NotificationType.STATE_ESTIMATION_STATUS, @@ -95,6 +101,10 @@ const dynamicSecurityAnalysisStatusCompletions = [ NotificationType.DYNAMIC_SECURITY_ANALYSIS_RESULT, NotificationType.DYNAMIC_SECURITY_ANALYSIS_FAILED, ]; +const dynamicMarginCalculationStatusCompletions = [ + NotificationType.DYNAMIC_MARGIN_CALCULATION_RESULT, + NotificationType.DYNAMIC_MARGIN_CALCULATION_FAILED, +]; const voltageInitStatusCompletions = [NotificationType.VOLTAGE_INIT_RESULT, NotificationType.VOLTAGE_INIT_FAILED]; const stateEstimationStatusCompletions = [ NotificationType.STATE_ESTIMATION_RESULT, @@ -107,6 +117,7 @@ export const loadflowResultInvalidations = [NotificationType.LOADFLOW_RESULT]; export const securityAnalysisResultInvalidations = [NotificationType.SECURITY_ANALYSIS_RESULT]; export const dynamicSimulationResultInvalidations = [NotificationType.DYNAMIC_SIMULATION_RESULT]; export const dynamicSecurityAnalysisResultInvalidations = [NotificationType.DYNAMIC_SECURITY_ANALYSIS_RESULT]; +export const dynamicSecurityMarginCalculationInvalidations = [NotificationType.DYNAMIC_MARGIN_CALCULATION_RESULT]; export const voltageInitResultInvalidations = [NotificationType.VOLTAGE_INIT_RESULT]; export const stateEstimationResultInvalidations = [NotificationType.STATE_ESTIMATION_RESULT]; export const pccMinResultInvalidations = [NotificationType.PCC_MIN_RESULT]; @@ -117,6 +128,9 @@ export const useAllComputingStatus = (studyUuid: UUID, currentNodeUuid: UUID, cu const sensitivityAnalysisAvailability = useOptionalServiceStatus(OptionalServicesNames.SensitivityAnalysis); const dynamicSimulationAvailability = useOptionalServiceStatus(OptionalServicesNames.DynamicSimulation); const dynamicSecurityAnalysisAvailability = useOptionalServiceStatus(OptionalServicesNames.DynamicSecurityAnalysis); + const dynamicMarginCalculationAvailability = useOptionalServiceStatus( + OptionalServicesNames.DynamicMarginCalculation + ); const voltageInitAvailability = useOptionalServiceStatus(OptionalServicesNames.VoltageInit); const shortCircuitAvailability = useOptionalServiceStatus(OptionalServicesNames.ShortCircuit); const stateEstimationAvailability = useOptionalServiceStatus(OptionalServicesNames.StateEstimation); @@ -212,6 +226,19 @@ export const useAllComputingStatus = (studyUuid: UUID, currentNodeUuid: UUID, cu dynamicSecurityAnalysisAvailability ); + useComputingStatus( + studyUuid, + currentNodeUuid, + currentRootNetworkUuid, + fetchDynamicMarginCalculationStatus, + dynamicMarginCalculationStatusInvalidations, + dynamicMarginCalculationStatusCompletions, + getDynamicMarginCalculationRunningStatus, + ComputingType.DYNAMIC_MARGIN_CALCULATION, + undefined, + dynamicMarginCalculationAvailability + ); + useComputingStatus( studyUuid, currentNodeUuid, diff --git a/src/components/grid-layout/cards/diagrams/singleLineDiagram/single-line-diagram-content.tsx b/src/components/grid-layout/cards/diagrams/singleLineDiagram/single-line-diagram-content.tsx index 2605d94505..a8cfd3ce8f 100644 --- a/src/components/grid-layout/cards/diagrams/singleLineDiagram/single-line-diagram-content.tsx +++ b/src/components/grid-layout/cards/diagrams/singleLineDiagram/single-line-diagram-content.tsx @@ -5,7 +5,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { useCallback, useLayoutEffect, useRef, useState, memo } from 'react'; +import { memo, useCallback, useLayoutEffect, useRef, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { RunningStatus } from '../../../../utils/running-status'; import { @@ -33,9 +33,9 @@ import { ComputingType, EquipmentType, mergeSx, + PARAM_DEVELOPER_MODE, snackWithFallback, useSnackMessage, - PARAM_DEVELOPER_MODE, } from '@gridsuite/commons-ui'; import Box from '@mui/material/Box'; import LinearProgress from '@mui/material/LinearProgress'; @@ -50,11 +50,11 @@ import { useParameterState } from 'components/dialogs/parameters/use-parameters- import { DiagramType, type SubstationDiagramParams, type VoltageLevelDiagramParams } from '../diagram.type'; import { useEquipmentMenu } from '../../../../../hooks/use-equipment-menu'; import useEquipmentDialogs from 'hooks/use-equipment-dialogs'; -import useComputationDebug from '../../../../../hooks/use-computation-debug'; import GenericEquipmentPopover from 'components/tooltips/generic-equipment-popover'; import { GenericEquipmentInfos } from 'components/tooltips/equipment-popover-type'; import { GenericPopoverContent } from 'components/tooltips/generic-popover-content'; +import useDebugSubscription from '../../../../../hooks/computation-debug/use-debug-subscription'; interface SingleLineDiagramContentProps { readonly showInSpreadsheet: (menu: { equipmentId: string | null; equipmentType: EquipmentType | null }) => void; @@ -217,7 +217,7 @@ const SingleLineDiagramContent = memo(function SingleLineDiagramContent(props: S ); // --- for running in debug mode --- // - const subscribeDebug = useComputationDebug({ + const subscribeDebug = useDebugSubscription({ studyUuid: studyUuid, nodeUuid: currentNode?.id!, rootNetworkUuid: currentRootNetworkUuid!, diff --git a/src/components/run-button-container.jsx b/src/components/run-button-container.jsx index eb23b761db..60faefaf40 100644 --- a/src/components/run-button-container.jsx +++ b/src/components/run-button-container.jsx @@ -49,11 +49,17 @@ import { } from '../services/study/dynamic-security-analysis'; import { useParameterState } from './dialogs/parameters/use-parameters-state'; import { isSecurityModificationNode } from './graph/tree-node.type'; -import useComputationDebug from '../hooks/use-computation-debug'; import { PaginationType } from 'types/custom-aggrid-types'; import { usePaginationReset } from 'hooks/use-pagination-selector'; import { useLogsPaginationResetByType } from './report-viewer/use-logs-pagination'; import { startPccMin, stopPccMin } from 'services/study/pcc-min'; +import { + fetchDynamicMarginCalculationProvider, + startDynamicMarginCalculation, + stopDynamicMarginCalculation, +} from '../services/study/dynamic-margin-calculation.ts'; +import useDebugSubscription from '../hooks/computation-debug/use-debug-subscription.ts'; +import useDebugNotification from '../hooks/computation-debug/use-debug-notification.ts'; const checkDynamicSimulationParameters = (studyUuid) => { return fetchDynamicSimulationParameters(studyUuid).then((params) => { @@ -97,6 +103,9 @@ export function RunButtonContainer({ studyUuid, currentNode, currentRootNetworkU const dynamicSecurityAnalysisStatus = useSelector( (state) => state.computingStatus[ComputingType.DYNAMIC_SECURITY_ANALYSIS] ); + const dynamicMarginCalculationStatus = useSelector( + (state) => state.computingStatus[ComputingType.DYNAMIC_MARGIN_CALCULATION] + ); const voltageInitStatus = useSelector((state) => state.computingStatus[ComputingType.VOLTAGE_INITIALIZATION]); const stateEstimationStatus = useSelector((state) => state.computingStatus[ComputingType.STATE_ESTIMATION]); const pccMinStatus = useSelector((state) => state.computingStatus[ComputingType.PCC_MIN]); @@ -123,6 +132,9 @@ export function RunButtonContainer({ studyUuid, currentNode, currentRootNetworkU const dynamicSimulationAvailability = useOptionalServiceStatus(OptionalServicesNames.DynamicSimulation); const dynamicSecurityAnalysisAvailability = useOptionalServiceStatus(OptionalServicesNames.DynamicSecurityAnalysis); + const dynamicMarginCalculationAvailability = useOptionalServiceStatus( + OptionalServicesNames.DynamicMarginCalculation + ); const voltageInitAvailability = useOptionalServiceStatus(OptionalServicesNames.VoltageInit); const shortCircuitAvailability = useOptionalServiceStatus(OptionalServicesNames.ShortCircuit); const stateEstimationAvailability = useOptionalServiceStatus(OptionalServicesNames.StateEstimation); @@ -162,8 +174,11 @@ export function RunButtonContainer({ studyUuid, currentNode, currentRootNetworkU ] ); + // --- for listening to debug notifications then perform download debug file --- // + useDebugNotification(); + // --- for running in debug mode --- // - const subscribeDebug = useComputationDebug({ + const subscribeDebug = useDebugSubscription({ studyUuid: studyUuid, nodeUuid: currentNode?.id, rootNetworkUuid: currentRootNetworkUuid, @@ -442,6 +457,45 @@ export function RunButtonContainer({ studyUuid, currentNode, currentRootNetworkU ); }, }, + [ComputingType.DYNAMIC_MARGIN_CALCULATION]: { + messageId: 'DynamicMarginCalculation', + async startComputation(debug) { + try { + const isProviderValid = await checkForbiddenProvider( + studyUuid, + ComputingType.DYNAMIC_MARGIN_CALCULATION, + fetchDynamicMarginCalculationProvider, + [PARAM_PROVIDER_DYNAWO] + ); + + if (!isProviderValid) { + return; + } + + startComputationAsync( + ComputingType.DYNAMIC_MARGIN_CALCULATION, + null, + () => + startDynamicMarginCalculation( + studyUuid, + currentNode?.id, + currentRootNetworkUuid, + debug + ), + () => debug && subscribeDebug(ComputingType.DYNAMIC_MARGIN_CALCULATION), + null, + 'startDynamicMarginCalculationError' + ); + } catch (error) { + snackWithFallback(snackError, error, { headerId: 'startDynamicMarginCalculationError' }); + } + }, + actionOnRunnable() { + actionOnRunnables(ComputingType.DYNAMIC_MARGIN_CALCULATION, () => + stopDynamicMarginCalculation(studyUuid, currentNode?.id, currentRootNetworkUuid) + ); + }, + }, [ComputingType.VOLTAGE_INITIALIZATION]: { messageId: 'VoltageInit', @@ -533,6 +587,8 @@ export function RunButtonContainer({ studyUuid, currentNode, currentRootNetworkU return dynamicSimulationStatus; case ComputingType.DYNAMIC_SECURITY_ANALYSIS: return dynamicSecurityAnalysisStatus; + case ComputingType.DYNAMIC_MARGIN_CALCULATION: + return dynamicMarginCalculationStatus; case ComputingType.VOLTAGE_INITIALIZATION: return voltageInitStatus; case ComputingType.STATE_ESTIMATION: @@ -551,6 +607,7 @@ export function RunButtonContainer({ studyUuid, currentNode, currentRootNetworkU allBusesShortCircuitAnalysisStatus, dynamicSimulationStatus, dynamicSecurityAnalysisStatus, + dynamicMarginCalculationStatus, voltageInitStatus, stateEstimationStatus, pccMinStatus, @@ -573,6 +630,9 @@ export function RunButtonContainer({ studyUuid, currentNode, currentRootNetworkU ...(dynamicSecurityAnalysisAvailability === OptionalServicesStatus.Up && isDeveloperMode ? [ComputingType.DYNAMIC_SECURITY_ANALYSIS] : []), + ...(dynamicMarginCalculationAvailability === OptionalServicesStatus.Up && isDeveloperMode + ? [ComputingType.DYNAMIC_MARGIN_CALCULATION] + : []), ...(voltageInitAvailability === OptionalServicesStatus.Up ? [ComputingType.VOLTAGE_INITIALIZATION] : []), ...(stateEstimationAvailability === OptionalServicesStatus.Up && isDeveloperMode ? [ComputingType.STATE_ESTIMATION] @@ -586,6 +646,7 @@ export function RunButtonContainer({ studyUuid, currentNode, currentRootNetworkU dynamicSimulationAvailability, isDeveloperMode, dynamicSecurityAnalysisAvailability, + dynamicMarginCalculationAvailability, voltageInitAvailability, stateEstimationAvailability, pccMinAvailability, diff --git a/src/components/run-button.jsx b/src/components/run-button.jsx index 0d21179c5b..975a1e34cf 100644 --- a/src/components/run-button.jsx +++ b/src/components/run-button.jsx @@ -80,6 +80,15 @@ const RunButton = ({ runnables, activeRunnables, getStatus, computationStopped, ); } + if (selectedRunnable === ComputingType.DYNAMIC_MARGIN_CALCULATION) { + // Load flow button's status must be "SUCCEED" + return ( + getRunningStatus() === RunningStatus.RUNNING || + (getStatus('LOAD_FLOW_WITHOUT_RATIO_TAP_CHANGERS') !== RunningStatus.SUCCEED && + getStatus('LOAD_FLOW_WITH_RATIO_TAP_CHANGERS') !== RunningStatus.SUCCEED) + ); + } + // We can run only 1 computation at a time return getRunningStatus() === RunningStatus.RUNNING; } diff --git a/src/components/study-container.jsx b/src/components/study-container.jsx index a0e158af7d..468df45355 100644 --- a/src/components/study-container.jsx +++ b/src/components/study-container.jsx @@ -28,14 +28,14 @@ import { fetchRootNetworks } from 'services/root-network'; import WaitingLoader from './utils/waiting-loader'; import { + convertToCustomError, hasElementPermission, - PermissionType, NotificationsUrlKeys, + PermissionType, snackWithFallback, useIntlRef, useNotificationsListener, useSnackMessage, - convertToCustomError, } from '@gridsuite/commons-ui'; import NetworkModificationTreeModel from './graph/network-modification-tree-model'; import { getFirstNodeOfType } from './graph/util/model-functions'; @@ -207,6 +207,11 @@ export function StudyContainer() { headerId: 'DynamicSecurityAnalysisRunError', }); } + if (updateTypeHeader === NotificationType.DYNAMIC_MARGIN_CALCULATION_FAILED) { + snackWithFallback(snackError, convertToCustomError(errorMessage), { + headerId: 'DynamicMarginCalculationRunError', + }); + } if (updateTypeHeader === NotificationType.VOLTAGE_INIT_FAILED) { snackWithFallback(snackError, convertToCustomError(errorMessage), { headerId: 'voltageInitError', diff --git a/src/components/utils/optional-services.ts b/src/components/utils/optional-services.ts index b990f60e1a..9e727c2c01 100644 --- a/src/components/utils/optional-services.ts +++ b/src/components/utils/optional-services.ts @@ -10,6 +10,7 @@ export enum OptionalServicesNames { SensitivityAnalysis = 'SensitivityAnalysis', DynamicSimulation = 'DynamicSimulation', DynamicSecurityAnalysis = 'DynamicSecurityAnalysis', + DynamicMarginCalculation = 'DynamicMarginCalculation', ShortCircuit = 'ShortCircuit', VoltageInit = 'VoltageInit', StateEstimation = 'StateEstimation', diff --git a/src/components/utils/running-status.ts b/src/components/utils/running-status.ts index 6d1da5a497..cfcbce99e2 100644 --- a/src/components/utils/running-status.ts +++ b/src/components/utils/running-status.ts @@ -106,6 +106,21 @@ export function getDynamicSecurityAnalysisRunningStatus(dynamicSecurityAnalysisS } } +export function getDynamicMarginCalculationRunningStatus(dynamicMarginCalculationStatus: string | null): RunningStatus { + switch (dynamicMarginCalculationStatus) { + case 'SUCCEED': + return RunningStatus.SUCCEED; + case 'FAILED': + return RunningStatus.FAILED; + case 'RUNNING': + return RunningStatus.RUNNING; + case 'NOT_DONE': + return RunningStatus.IDLE; + default: + return RunningStatus.IDLE; + } +} + export function getVoltageInitRunningStatus(voltageInitStatus: string | null): RunningStatus { switch (voltageInitStatus) { case 'OK': diff --git a/src/hooks/computation-debug/computation-debug-utils.ts b/src/hooks/computation-debug/computation-debug-utils.ts new file mode 100644 index 0000000000..796c074319 --- /dev/null +++ b/src/hooks/computation-debug/computation-debug-utils.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +import type { UUID } from 'node:crypto'; +import { getDebugState, saveDebugState } from '../../redux/session-storage/debug-state'; +import { ComputingType } from '@gridsuite/commons-ui'; + +export function buildDebugIdentifier({ + studyUuid, + nodeUuid, + rootNetworkUuid, + computingType, +}: { + studyUuid: UUID; + nodeUuid: UUID; + rootNetworkUuid: UUID; + computingType: ComputingType; +}) { + return `${studyUuid}|${rootNetworkUuid}|${nodeUuid}|${computingType}`; +} + +export function setDebug(identifier: string) { + const debugState = getDebugState() ?? new Set(); + debugState.add(identifier); + saveDebugState(debugState); +} + +export function isDebug(identifier: string) { + const debugState = getDebugState(); + return debugState?.has(identifier); +} + +export function unsetDebug(identifier: string) { + const debugState = getDebugState(); + if (debugState) { + debugState.delete(identifier); + saveDebugState(debugState); + } +} diff --git a/src/hooks/computation-debug/use-debug-download.ts b/src/hooks/computation-debug/use-debug-download.ts new file mode 100644 index 0000000000..61c02b1e80 --- /dev/null +++ b/src/hooks/computation-debug/use-debug-download.ts @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +import { useCallback } from 'react'; +import type { UUID } from 'node:crypto'; +import { ComputingType, formatComputingTypeLabel, snackWithFallback, useSnackMessage } from '@gridsuite/commons-ui'; +import { downloadZipFile } from '../../services/utils'; +import { HttpStatusCode } from '../../utils/http-status-code'; +import { downloadDebugFileDynamicSimulation } from '../../services/dynamic-simulation'; +import { downloadDebugFileDynamicSecurityAnalysis } from '../../services/dynamic-security-analysis'; +import { downloadDebugFileDynamicMarginCalculation } from '../../services/dynamic-margin-calculation'; +import { downloadDebugFileVoltageInit } from '../../services/voltage-init'; +import { downloadDebugFileShortCircuitAnalysis } from '../../services/short-circuit-analysis'; + +const downloadDebugFileFetchers = { + [ComputingType.DYNAMIC_SIMULATION]: downloadDebugFileDynamicSimulation, + [ComputingType.DYNAMIC_SECURITY_ANALYSIS]: downloadDebugFileDynamicSecurityAnalysis, + [ComputingType.DYNAMIC_MARGIN_CALCULATION]: downloadDebugFileDynamicMarginCalculation, + [ComputingType.VOLTAGE_INITIALIZATION]: downloadDebugFileVoltageInit, + [ComputingType.SHORT_CIRCUIT]: downloadDebugFileShortCircuitAnalysis, + [ComputingType.SHORT_CIRCUIT_ONE_BUS]: downloadDebugFileShortCircuitAnalysis, +} as Record Promise) | null>; + +export default function useDebugDownload() { + const { snackWarning, snackError } = useSnackMessage(); + const downloadDebugFile = useCallback( + (resultUuid: UUID, computingType: ComputingType) => { + downloadDebugFileFetchers[computingType]?.(resultUuid) // download a debug file from a specific computation server + .then(async (response) => { + // Get the filename + const contentDisposition = response.headers.get('Content-Disposition'); + let filename = `${formatComputingTypeLabel(computingType)}.zip`; + if (contentDisposition?.includes('filename=')) { + const regex = /filename="?([^"]+)"?/; + const match = regex.exec(contentDisposition); + if (match?.[1]) { + filename = match[1]; + } + } + + const blob = await response.blob(); + downloadZipFile(blob, filename); + }) + .catch((responseError) => { + const error = responseError as Error & { status: number }; + if (error.status === HttpStatusCode.NOT_FOUND) { + // not found + snackWarning({ + headerId: 'debug.header.fileNotFound', + }); + } else { + // or whatever error + snackWithFallback(snackError, error, { headerId: 'debug.header.fileError' }); + } + }); + }, + [snackWarning, snackError] + ); + + return downloadDebugFile; +} diff --git a/src/hooks/computation-debug/use-debug-notification.ts b/src/hooks/computation-debug/use-debug-notification.ts new file mode 100644 index 0000000000..29d4e17bb5 --- /dev/null +++ b/src/hooks/computation-debug/use-debug-notification.ts @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +import { useCallback } from 'react'; +import { NotificationType } from '../../types/notification-types'; +import { NotificationsUrlKeys, useNotificationsListener, useSnackMessage } from '@gridsuite/commons-ui'; +import { buildDebugIdentifier, isDebug, unsetDebug } from './computation-debug-utils'; +import { useSelector } from 'react-redux'; +import { AppState } from '../../redux/reducer'; +import useDebugDownload from './use-debug-download'; + +export default function useDebugNotification() { + const { snackWarning } = useSnackMessage(); + const userId = useSelector((state: AppState) => state.user?.profile.sub); + const downloadDebugFile = useDebugDownload(); + const onDebugNotification = useCallback( + (event: MessageEvent) => { + const eventData = JSON.parse(event.data); + const updateTypeHeader = eventData.headers.updateType; + if (updateTypeHeader === NotificationType.COMPUTATION_DEBUG_FILE_STATUS) { + const { + studyUuid, + node: nodeUuid, + rootNetworkUuid, + computationType: computingType, + userId: userIdNotif, + resultUuid, + error, + } = eventData.headers; + const debugIdentifierNotif = buildDebugIdentifier({ + studyUuid, + nodeUuid, + rootNetworkUuid, + computingType, + }); + const debug = isDebug(debugIdentifierNotif); + if (debug && userIdNotif === userId) { + // download by notif once, so unset the debug identifier + unsetDebug(debugIdentifierNotif); + if (error) { + snackWarning({ + messageTxt: error, + }); + } else { + // perform download debug file once + resultUuid && downloadDebugFile(resultUuid, computingType); + } + } + } + }, + [downloadDebugFile, userId, snackWarning] + ); + + useNotificationsListener(NotificationsUrlKeys.STUDY, { listenerCallbackMessage: onDebugNotification }); +} diff --git a/src/hooks/computation-debug/use-debug-subscription.ts b/src/hooks/computation-debug/use-debug-subscription.ts new file mode 100644 index 0000000000..40f6192e2c --- /dev/null +++ b/src/hooks/computation-debug/use-debug-subscription.ts @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +import type { UUID } from 'node:crypto'; +import { useCallback } from 'react'; +import { ComputingType, formatComputingTypeLabel, useSnackMessage } from '@gridsuite/commons-ui'; +import { buildDebugIdentifier, setDebug } from './computation-debug-utils'; +import { useIntl } from 'react-intl'; + +export default function useDebugSubscription({ + studyUuid, + nodeUuid, + rootNetworkUuid, +}: { + studyUuid: UUID; + nodeUuid: UUID; + rootNetworkUuid: UUID; +}) { + const intl = useIntl(); + const { snackInfo } = useSnackMessage(); + const subscribeDebug = useCallback( + (computingType: ComputingType) => { + // set debug true in the session storage + setDebug( + buildDebugIdentifier({ + studyUuid: studyUuid, + nodeUuid: nodeUuid, + rootNetworkUuid: rootNetworkUuid, + computingType: computingType, + }) + ); + snackInfo({ + headerTxt: intl.formatMessage({ + id: formatComputingTypeLabel(computingType), + }), + messageTxt: intl.formatMessage({ id: 'debug.message.downloadFile' }), + }); + }, + [studyUuid, nodeUuid, rootNetworkUuid, snackInfo, intl] + ); + + return subscribeDebug; +} diff --git a/src/hooks/use-computation-debug.ts b/src/hooks/use-computation-debug.ts deleted file mode 100644 index fb13041a68..0000000000 --- a/src/hooks/use-computation-debug.ts +++ /dev/null @@ -1,180 +0,0 @@ -/** - * Copyright (c) 2025, RTE (http://www.rte-france.com) - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -import { useCallback } from 'react'; -import type { UUID } from 'node:crypto'; -import { getDebugState, saveDebugState } from '../redux/session-storage/debug-state'; -import { downloadZipFile } from '../services/utils'; -import { HttpStatusCode } from '../utils/http-status-code'; -import { - ComputingType, - formatComputingTypeLabel, - NotificationsUrlKeys, - snackWithFallback, - useNotificationsListener, - useSnackMessage, -} from '@gridsuite/commons-ui'; -import { downloadDebugFileDynamicSimulation } from '../services/dynamic-simulation'; -import { useIntl } from 'react-intl'; -import { downloadDebugFileDynamicSecurityAnalysis } from '../services/dynamic-security-analysis'; -import { NotificationType } from '../types/notification-types'; -import { useSelector } from 'react-redux'; -import { AppState } from '../redux/reducer'; -import { downloadDebugFileVoltageInit } from '../services/voltage-init'; -import { downloadDebugFileShortCircuitAnalysis } from '../services/short-circuit-analysis'; - -const downloadDebugFileFetchers = { - [ComputingType.DYNAMIC_SIMULATION]: downloadDebugFileDynamicSimulation, - [ComputingType.DYNAMIC_SECURITY_ANALYSIS]: downloadDebugFileDynamicSecurityAnalysis, - [ComputingType.VOLTAGE_INITIALIZATION]: downloadDebugFileVoltageInit, - [ComputingType.SHORT_CIRCUIT]: downloadDebugFileShortCircuitAnalysis, - [ComputingType.SHORT_CIRCUIT_ONE_BUS]: downloadDebugFileShortCircuitAnalysis, -} as Record Promise) | null>; - -export function buildDebugIdentifier({ - studyUuid, - nodeUuid, - rootNetworkUuid, - computingType, -}: { - studyUuid: UUID; - nodeUuid: UUID; - rootNetworkUuid: UUID; - computingType: ComputingType; -}) { - return `${studyUuid}|${rootNetworkUuid}|${nodeUuid}|${computingType}`; -} - -export function setDebug(identifier: string) { - const debugState = getDebugState() ?? new Set(); - debugState.add(identifier); - saveDebugState(debugState); -} - -export function isDebug(identifier: string) { - const debugState = getDebugState(); - return debugState?.has(identifier); -} - -export function unsetDebug(identifier: string) { - const debugState = getDebugState(); - if (debugState) { - debugState.delete(identifier); - saveDebugState(debugState); - } -} - -export default function useComputationDebug({ - studyUuid, - nodeUuid, - rootNetworkUuid, -}: { - studyUuid: UUID; - nodeUuid: UUID; - rootNetworkUuid: UUID; -}) { - const intl = useIntl(); - const { snackWarning, snackInfo, snackError } = useSnackMessage(); - const userId = useSelector((state: AppState) => state.user?.profile.sub); - - const downloadDebugFile = useCallback( - (resultUuid: UUID, computingType: ComputingType) => { - downloadDebugFileFetchers[computingType]?.(resultUuid) // download a debug file from a specific computation server - .then(async (response) => { - // Get the filename - const contentDisposition = response.headers.get('Content-Disposition'); - let filename = `${formatComputingTypeLabel(computingType)}.zip`; - if (contentDisposition?.includes('filename=')) { - const regex = /filename="?([^"]+)"?/; - const match = regex.exec(contentDisposition); - if (match?.[1]) { - filename = match[1]; - } - } - - const blob = await response.blob(); - downloadZipFile(blob, filename); - }) - .catch((responseError) => { - const error = responseError as Error & { status: number }; - if (error.status === HttpStatusCode.NOT_FOUND) { - // not found - snackWarning({ - headerId: 'debug.header.fileNotFound', - }); - } else { - // or whatever error - snackWithFallback(snackError, error, { headerId: 'debug.header.fileError' }); - } - }); - }, - [snackWarning, snackError] - ); - - const onDebugNotification = useCallback( - (event: MessageEvent) => { - const eventData = JSON.parse(event.data); - const updateTypeHeader = eventData.headers.updateType; - if (updateTypeHeader === NotificationType.COMPUTATION_DEBUG_FILE_STATUS) { - const { - studyUuid, - node: nodeUuid, - rootNetworkUuid, - computationType: computingType, - userId: userIdNotif, - resultUuid, - error, - } = eventData.headers; - const debugIdentifierNotif = buildDebugIdentifier({ - studyUuid, - nodeUuid, - rootNetworkUuid, - computingType, - }); - const debug = isDebug(debugIdentifierNotif); - if (debug && userIdNotif === userId) { - // download by notif once, so unset the debug identifier - unsetDebug(debugIdentifierNotif); - if (error) { - snackWarning({ - messageTxt: error, - }); - } else { - // perform download debug file once - resultUuid && downloadDebugFile(resultUuid, computingType); - } - } - } - }, - [downloadDebugFile, userId, snackWarning] - ); - - useNotificationsListener(NotificationsUrlKeys.STUDY, { listenerCallbackMessage: onDebugNotification }); - - const subscribeDebug = useCallback( - (computingType: ComputingType) => { - // set debug true in the session storage - setDebug( - buildDebugIdentifier({ - studyUuid: studyUuid, - nodeUuid: nodeUuid, - rootNetworkUuid: rootNetworkUuid, - computingType: computingType, - }) - ); - snackInfo({ - headerTxt: intl.formatMessage({ - id: formatComputingTypeLabel(computingType), - }), - messageTxt: intl.formatMessage({ id: 'debug.message.downloadFile' }), - }); - }, - [studyUuid, nodeUuid, rootNetworkUuid, snackInfo, intl] - ); - - return subscribeDebug; -} diff --git a/src/hooks/use-computation-results-count.ts b/src/hooks/use-computation-results-count.ts index fb43e6b796..c38e80d91b 100644 --- a/src/hooks/use-computation-results-count.ts +++ b/src/hooks/use-computation-results-count.ts @@ -42,6 +42,10 @@ export const useComputationResultsCount = () => { (state: AppState) => state.computingStatus[ComputingType.DYNAMIC_SECURITY_ANALYSIS] ); + const dynamicMarginComputationStatus = useSelector( + (state: AppState) => state.computingStatus[ComputingType.DYNAMIC_MARGIN_CALCULATION] + ); + const voltageInitStatus = useSelector( (state: AppState) => state.computingStatus[ComputingType.VOLTAGE_INITIALIZATION] ); @@ -75,6 +79,10 @@ export const useComputationResultsCount = () => { dynamicSecurityAnalysisStatus === RunningStatus.SUCCEED || dynamicSecurityAnalysisStatus === RunningStatus.FAILED; // Can be failed for technical reasons (e.g., server not responding or computation divergence) + const dynamicMarginComputationResultPresent = + dynamicMarginComputationStatus === RunningStatus.SUCCEED || + dynamicMarginComputationStatus === RunningStatus.FAILED; // Can be failed for technical reasons (e.g., server not responding or computation divergence) + const stateEstimationResultPresent = isDeveloperMode && (stateEstimationStatus === RunningStatus.SUCCEED || stateEstimationStatus === RunningStatus.FAILED); // Can be failed for technical reasons (e.g., server not responding or computation divergence) @@ -90,6 +98,7 @@ export const useComputationResultsCount = () => { voltageInitResultPresent, dynamicSimulationResultPresent, dynamicSecurityAnalysisResultPresent, + dynamicMarginComputationResultPresent, stateEstimationResultPresent, pccMinResultPresent, ].filter(Boolean).length; diff --git a/src/redux/reducer.ts b/src/redux/reducer.ts index 948ac596f1..dcddd66b22 100644 --- a/src/redux/reducer.ts +++ b/src/redux/reducer.ts @@ -56,6 +56,8 @@ import { CLOSE_STUDY, type CloseStudyAction, CONFIRM_LEAVE_PARAMETERS_TAB, + COPIED_NETWORK_MODIFICATIONS, + type CopiedNetworkModificationsAction, CURRENT_ROOT_NETWORK_UUID, CURRENT_TREE_NODE, type CurrentRootNetworkUuidAction, @@ -101,9 +103,7 @@ import { type NetworkModificationTreeNodesReorderAction, type NetworkModificationTreeNodesUpdatedAction, NODE_SELECTION_FOR_COPY, - COPIED_NETWORK_MODIFICATIONS, type NodeSelectionForCopyAction, - type CopiedNetworkModificationsAction, OPEN_STUDY, type OpenStudyAction, type ParameterizedComputingType, @@ -217,6 +217,8 @@ import { type SpreadsheetFilterAction, STATEESTIMATION_RESULT_FILTER, type StateEstimationResultFilterAction, + STORE_NAD_VIEW_BOX, + StoreNadViewBoxAction, STUDY_UPDATED, type StudyUpdatedAction, TABLE_SORT, @@ -237,8 +239,6 @@ import { type UpdateTableDefinitionAction, USE_NAME, type UseNameAction, - STORE_NAD_VIEW_BOX, - StoreNadViewBoxAction, } from './actions'; import { getLocalStorageComputedLanguage, @@ -448,6 +448,7 @@ export interface ComputingStatus { [ComputingType.SHORT_CIRCUIT_ONE_BUS]: RunningStatus; [ComputingType.DYNAMIC_SIMULATION]: RunningStatus; [ComputingType.DYNAMIC_SECURITY_ANALYSIS]: RunningStatus; + [ComputingType.DYNAMIC_MARGIN_CALCULATION]: RunningStatus; [ComputingType.VOLTAGE_INITIALIZATION]: RunningStatus; [ComputingType.STATE_ESTIMATION]: RunningStatus; [ComputingType.PCC_MIN]: RunningStatus; @@ -631,6 +632,7 @@ const initialLogsFilterState: LogsFilterState = { [COMPUTING_AND_NETWORK_MODIFICATION_TYPE.SHORT_CIRCUIT_ONE_BUS]: [], [COMPUTING_AND_NETWORK_MODIFICATION_TYPE.DYNAMIC_SIMULATION]: [], [COMPUTING_AND_NETWORK_MODIFICATION_TYPE.DYNAMIC_SECURITY_ANALYSIS]: [], + [COMPUTING_AND_NETWORK_MODIFICATION_TYPE.DYNAMIC_MARGIN_CALCULATION]: [], [COMPUTING_AND_NETWORK_MODIFICATION_TYPE.VOLTAGE_INITIALIZATION]: [], [COMPUTING_AND_NETWORK_MODIFICATION_TYPE.STATE_ESTIMATION]: [], [COMPUTING_AND_NETWORK_MODIFICATION_TYPE.PCC_MIN]: [], @@ -645,6 +647,7 @@ const initialLogsPaginationState: LogsPaginationState = { [COMPUTING_AND_NETWORK_MODIFICATION_TYPE.SHORT_CIRCUIT_ONE_BUS]: { ...DEFAULT_LOGS_PAGINATION }, [COMPUTING_AND_NETWORK_MODIFICATION_TYPE.DYNAMIC_SIMULATION]: { ...DEFAULT_LOGS_PAGINATION }, [COMPUTING_AND_NETWORK_MODIFICATION_TYPE.DYNAMIC_SECURITY_ANALYSIS]: { ...DEFAULT_LOGS_PAGINATION }, + [COMPUTING_AND_NETWORK_MODIFICATION_TYPE.DYNAMIC_MARGIN_CALCULATION]: { ...DEFAULT_LOGS_PAGINATION }, [COMPUTING_AND_NETWORK_MODIFICATION_TYPE.VOLTAGE_INITIALIZATION]: { ...DEFAULT_LOGS_PAGINATION }, [COMPUTING_AND_NETWORK_MODIFICATION_TYPE.STATE_ESTIMATION]: { ...DEFAULT_LOGS_PAGINATION }, [COMPUTING_AND_NETWORK_MODIFICATION_TYPE.PCC_MIN]: { ...DEFAULT_LOGS_PAGINATION }, @@ -779,6 +782,7 @@ const initialState: AppState = { [ComputingType.SHORT_CIRCUIT_ONE_BUS]: RunningStatus.IDLE, [ComputingType.DYNAMIC_SIMULATION]: RunningStatus.IDLE, [ComputingType.DYNAMIC_SECURITY_ANALYSIS]: RunningStatus.IDLE, + [ComputingType.DYNAMIC_MARGIN_CALCULATION]: RunningStatus.IDLE, [ComputingType.VOLTAGE_INITIALIZATION]: RunningStatus.IDLE, [ComputingType.STATE_ESTIMATION]: RunningStatus.IDLE, [ComputingType.PCC_MIN]: RunningStatus.IDLE, diff --git a/src/services/dynamic-margin-calculation.ts b/src/services/dynamic-margin-calculation.ts new file mode 100644 index 0000000000..5307b37924 --- /dev/null +++ b/src/services/dynamic-margin-calculation.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +import { backendFetch, backendFetchJson } from '@gridsuite/commons-ui'; +import type { UUID } from 'node:crypto'; + +const PREFIX_DYNAMIC_MARGIN_CALCULATION_SERVER_QUERIES = + import.meta.env.VITE_API_GATEWAY + '/dynamic-margin-calculation'; + +function getDynamicMarginCalculationUrl() { + return `${PREFIX_DYNAMIC_MARGIN_CALCULATION_SERVER_QUERIES}/v1/`; +} + +export function fetchDynamicMarginCalculationProviders() { + console.info('fetch dynamic margin calculation providers'); + const url = getDynamicMarginCalculationUrl() + 'providers'; + console.debug(url); + return backendFetchJson(url); +} + +export function downloadDebugFileDynamicMarginCalculation(resultUuid: UUID): Promise { + console.info(`Download dynamic margin calculation debug file of '${resultUuid}' ...`); + + const url = getDynamicMarginCalculationUrl() + `results/${resultUuid}/download-debug-file`; + + console.debug(url); + return backendFetch(url, { + method: 'get', + headers: { 'Content-Type': 'application/json' }, + }); +} diff --git a/src/services/study/dynamic-margin-calculation.ts b/src/services/study/dynamic-margin-calculation.ts new file mode 100644 index 0000000000..a39a9a318c --- /dev/null +++ b/src/services/study/dynamic-margin-calculation.ts @@ -0,0 +1,177 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +import { getStudyUrl, getStudyUrlWithNodeUuidAndRootNetworkUuid, PREFIX_STUDY_QUERIES } from './index'; +import { backendFetch, backendFetchJson, backendFetchText } from '@gridsuite/commons-ui'; +import type { UUID } from 'node:crypto'; +import { + DynamicMarginCalculationParametersFetchReturn, + DynamicMarginCalculationParametersInfos, +} from './dynamic-margin-calculation.type'; +import { fetchContingencyAndFiltersLists } from '../directory'; + +export function startDynamicMarginCalculation( + studyUuid: UUID, + currentNodeUuid: UUID, + currentRootNetworkUuid: UUID, + debug: boolean +): Promise { + console.info( + `Running dynamic margin calculation on study '${studyUuid}', on root network '${currentRootNetworkUuid}' and node '${currentNodeUuid}' ...` + ); + + const urlParams = new URLSearchParams(); + + if (debug) { + urlParams.append('debug', `${debug}`); + } + + const startDynamicMarginCalculationUrl = `${getStudyUrlWithNodeUuidAndRootNetworkUuid( + studyUuid, + currentNodeUuid, + currentRootNetworkUuid + )}/dynamic-margin-calculation/run?${urlParams}`; + + console.debug({ startDynamicMarginCalculationUrl }); + + return backendFetch(startDynamicMarginCalculationUrl, { + method: 'post', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + }); +} + +export function stopDynamicMarginCalculation(studyUuid: UUID, currentNodeUuid: UUID, currentRootNetworkUuid: UUID) { + console.info( + `Stopping dynamic margin calculation on study '${studyUuid}', on root network '${currentRootNetworkUuid}' and node '${currentNodeUuid}' ...` + ); + const stopDynamicMarginCalculationUrl = + getStudyUrlWithNodeUuidAndRootNetworkUuid(studyUuid, currentNodeUuid, currentRootNetworkUuid) + + '/dynamic-margin-calculation/stop'; + console.debug(stopDynamicMarginCalculationUrl); + return backendFetch(stopDynamicMarginCalculationUrl, { method: 'put' }); +} + +export function fetchDynamicMarginCalculationStatus( + studyUuid: UUID, + currentNodeUuid: UUID, + currentRootNetworkUuid: UUID +): Promise { + console.info( + `Fetching dynamic margin calculation status on study '${studyUuid}', on root network '${currentRootNetworkUuid}' and node '${currentNodeUuid}' ...` + ); + const url = + getStudyUrlWithNodeUuidAndRootNetworkUuid(studyUuid, currentNodeUuid, currentRootNetworkUuid) + + '/dynamic-margin-calculation/status'; + console.debug(url); + return backendFetchText(url); +} + +export function fetchDynamicMarginCalculationProvider(studyUuid: UUID) { + console.info(`Fetching dynamic margin calculation provider on study '${studyUuid}' ...`); + const url = getStudyUrl(studyUuid) + '/dynamic-margin-calculation/provider'; + console.debug(url); + return backendFetchText(url); +} + +export function fetchDefaultDynamicMarginCalculationProvider() { + console.info('Fetching default dynamic margin calculation provider'); + const url = PREFIX_STUDY_QUERIES + '/v1/dynamic-margin-calculation-default-provider'; + console.debug(url); + return backendFetchText(url); +} + +export function updateDynamicMarginCalculationProvider(studyUuid: UUID, newProvider: string) { + console.info(`Updating dynamic margin calculation provider on study '${studyUuid}' ...`); + const url = getStudyUrl(studyUuid) + '/dynamic-margin-calculation/provider'; + console.debug(url); + return backendFetch(url, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: newProvider, + }); +} + +export function fetchDynamicMarginCalculationParameters( + studyUuid: UUID +): Promise { + console.info(`Fetching dynamic margin calculation parameters on study '${studyUuid}' ...`); + const url = getStudyUrl(studyUuid) + '/dynamic-margin-calculation/parameters'; + console.debug(url); + const parametersPromise: Promise = backendFetchJson(url); + + // enrich LoadsVariationInfos by LoadsVariationFetchReturn with id and name infos + return parametersPromise.then((parameters) => { + if (parameters?.loadsVariations) { + const loadsVariations = parameters?.loadsVariations; + const allLoadFilterUuids = loadsVariations.flatMap((loadVariation) => loadVariation.loadFilterUuids ?? []); + return fetchContingencyAndFiltersLists(allLoadFilterUuids).then((loadFilterInfos) => { + delete parameters.loadsVariations; + const loadFilterInfosMap = Object.fromEntries( + loadFilterInfos.map((info) => [info.elementUuid, info.elementName]) + ); + return { + ...parameters, + loadsVariationsInfos: loadsVariations?.map((infos) => { + const newLoadVariationInfos = { + ...infos, + loadFiltersInfos: infos.loadFilterUuids?.map((loadFilterUuid) => ({ + id: loadFilterUuid, + name: loadFilterInfosMap[loadFilterUuid], + })), + }; + delete newLoadVariationInfos.loadFilterUuids; + return newLoadVariationInfos; + }), + }; + }); + } + delete parameters.loadsVariations; + return { + ...parameters, + loadsVariationsInfos: [], + }; + }); +} + +export function updateDynamicMarginCalculationParameters( + studyUuid: UUID, + newParams: DynamicMarginCalculationParametersFetchReturn | null +): Promise { + console.info(`Setting dynamic margin calculation parameters on study '${studyUuid}' ...`); + const url = getStudyUrl(studyUuid) + '/dynamic-margin-calculation/parameters'; + console.debug(url); + + // send to back raw LoadsVariations instead of LoadsVariationsInfos + const newParameters = + newParams != null + ? { + ...newParams, + loadsVariations: newParams?.loadsVariationsInfos?.map((infos) => { + const newLoadsVariationInfos = { + ...infos, + loadFilterUuids: infos.loadFiltersInfos?.map((loadFilterInfos) => loadFilterInfos.id), + }; + delete newLoadsVariationInfos.loadFiltersInfos; + return newLoadsVariationInfos; + }), + } + : newParams; + delete newParameters?.loadsVariationsInfos; + return backendFetch(url, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(newParameters), + }); +} diff --git a/src/services/study/dynamic-margin-calculation.type.ts b/src/services/study/dynamic-margin-calculation.type.ts new file mode 100644 index 0000000000..f538da20dc --- /dev/null +++ b/src/services/study/dynamic-margin-calculation.type.ts @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +import type { UUID } from 'node:crypto'; + +export enum CalculationType { + GLOBAL_MARGIN = 'GLOBAL_MARGIN', + LOCAL_MARGIN = 'LOCAL_MARGIN', +} + +export enum LoadModelsRule { + ALL_LOADS = 'ALL_LOADS', + TARGETED_LOADS = 'TARGETED_LOADS', +} + +export type LoadsVariationInfos = { + id?: UUID; // persisted id of the info to be modified + loadFilterUuids?: UUID[]; + variation: number; +}; + +export type LoadsVariationFetchReturn = Exclude & { + loadFiltersInfos?: { id: string; name: string }[]; +}; + +export type DynamicMarginCalculationParametersInfos = { + provider?: string; + startTime?: number; + stopTime?: number; + marginCalculationStartTime?: number; + loadIncreaseStartTime?: number; + loadIncreaseStopTime?: number; + calculationType?: CalculationType; + accuracy?: number; // integer + loadModelsRule?: LoadModelsRule; + loadsVariations?: LoadsVariationInfos[]; +}; + +export type DynamicMarginCalculationParametersFetchReturn = Exclude< + DynamicMarginCalculationParametersInfos, + 'loadsVariations' +> & { + loadsVariationsInfos?: LoadsVariationFetchReturn[]; +}; diff --git a/src/translations/en.json b/src/translations/en.json index 10a3548723..2915f04d61 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -366,6 +366,20 @@ "DynamicSecurityAnalysisContingenciesStartTime": "Contingencies start time", "DynamicSecurityAnalysisTabStatus": "Status", + "startDynamicMarginCalculationError": "An error occurred while starting the dynamic margin calculation", + "DynamicMarginCalculation": "Dynamic margin calculation", + "DynamicMarginCalculationRunError": "An error occurred while executing the dynamic margin calculation", + "DynamicMarginCalculationStartTime": "Start time", + "DynamicMarginCalculationStopTime": "Stop time", + "DynamicMarginCalculationMarginCalculationStartTime": "Margin calculation start time", + "DynamicMarginCalculationLoadIncreaseStartTime": "Load increase start time", + "DynamicMarginCalculationLoadIncreaseStopTime": "Load increase stop time", + "DynamicMarginCalculationCalculationType": "Calculation type", + "DynamicMarginCalculationAccuracy": "Accuracy", + "DynamicMarginCalculationLoadModelsRule": "Load models rule", + "DynamicMarginCalculationLoadsVariations": "Load variations", + "DynamicMarginCalculationTabStatus": "Status", + "TwoSides.ONE": "Origin side", "TwoSides.TWO": "Extremity side", diff --git a/src/translations/fr.json b/src/translations/fr.json index f600572e69..3d0cf6d8c2 100644 --- a/src/translations/fr.json +++ b/src/translations/fr.json @@ -364,6 +364,20 @@ "DynamicSecurityAnalysisContingenciesStartTime": "Temps de début des aléas", "DynamicSecurityAnalysisTabStatus": "Statut", + "startDynamicMarginCalculationError": "Une erreur est survenue lors du lancement de la calculation de marge dynamique", + "DynamicMarginCalculation": "Calculation de marge dynamique", + "DynamicMarginCalculationRunError": "Une erreur est survenue lors de l'exécution de la calculation de marge dynamique", + "DynamicMarginCalculationStartTime": "Temps de début", + "DynamicMarginCalculationStopTime": "Temps d'arrêt", + "DynamicMarginCalculationMarginCalculationStartTime": "Temps de début de la calculation de marge", + "DynamicMarginCalculationLoadIncreaseStartTime": "Temps de début de l'augmentation de charge", + "DynamicMarginCalculationLoadIncreaseStopTime": "Temps d'arrêt de l'augmentation de charge", + "DynamicMarginCalculationCalculationType": "Type de calculation", + "DynamicMarginCalculationAccuracy": "Précision", + "DynamicMarginCalculationLoadModelsRule": "Règle des modèles de charge", + "DynamicMarginCalculationLoadsVariations": "Variations de charge", + "DynamicMarginCalculationTabStatus": "Status", + "TwoSides.ONE": "Côté 1", "TwoSides.TWO": "Côté 2", diff --git a/src/types/notification-types.ts b/src/types/notification-types.ts index b22cd0e769..9db62e05ed 100644 --- a/src/types/notification-types.ts +++ b/src/types/notification-types.ts @@ -73,6 +73,9 @@ export enum NotificationType { DYNAMIC_SECURITY_ANALYSIS_RESULT = 'dynamicSecurityAnalysisResult', DYNAMIC_SECURITY_ANALYSIS_FAILED = 'dynamicSecurityAnalysis_failed', DYNAMIC_SECURITY_ANALYSIS_STATUS = 'dynamicSecurityAnalysis_status', + DYNAMIC_MARGIN_CALCULATION_RESULT = 'dynamicMarginCalculationResult', + DYNAMIC_MARGIN_CALCULATION_FAILED = 'dynamicMarginCalculation_failed', + DYNAMIC_MARGIN_CALCULATION_STATUS = 'dynamicMarginCalculation_status', VOLTAGE_INIT_RESULT = 'voltageInitResult', VOLTAGE_INIT_FAILED = 'voltageInit_failed', VOLTAGE_INIT_CANCEL_FAILED = 'voltageInit_cancel_failed', From 832449ca85aa4b5766b53430c8240d799e2ea3b4 Mon Sep 17 00:00:00 2001 From: Thang PHAM Date: Thu, 22 Jan 2026 19:32:21 +0100 Subject: [PATCH 2/6] correct labels Signed-off-by: Thang PHAM --- .../computing-status/use-all-computing-status.ts | 2 +- src/translations/en.json | 9 --------- src/translations/fr.json | 15 +++------------ 3 files changed, 4 insertions(+), 22 deletions(-) diff --git a/src/components/computing-status/use-all-computing-status.ts b/src/components/computing-status/use-all-computing-status.ts index e7a5c88626..034ad7bfe0 100644 --- a/src/components/computing-status/use-all-computing-status.ts +++ b/src/components/computing-status/use-all-computing-status.ts @@ -117,7 +117,7 @@ export const loadflowResultInvalidations = [NotificationType.LOADFLOW_RESULT]; export const securityAnalysisResultInvalidations = [NotificationType.SECURITY_ANALYSIS_RESULT]; export const dynamicSimulationResultInvalidations = [NotificationType.DYNAMIC_SIMULATION_RESULT]; export const dynamicSecurityAnalysisResultInvalidations = [NotificationType.DYNAMIC_SECURITY_ANALYSIS_RESULT]; -export const dynamicSecurityMarginCalculationInvalidations = [NotificationType.DYNAMIC_MARGIN_CALCULATION_RESULT]; +export const dynamicMarginCalculationInvalidations = [NotificationType.DYNAMIC_MARGIN_CALCULATION_RESULT]; export const voltageInitResultInvalidations = [NotificationType.VOLTAGE_INIT_RESULT]; export const stateEstimationResultInvalidations = [NotificationType.STATE_ESTIMATION_RESULT]; export const pccMinResultInvalidations = [NotificationType.PCC_MIN_RESULT]; diff --git a/src/translations/en.json b/src/translations/en.json index 2915f04d61..69a83d6e1e 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -369,15 +369,6 @@ "startDynamicMarginCalculationError": "An error occurred while starting the dynamic margin calculation", "DynamicMarginCalculation": "Dynamic margin calculation", "DynamicMarginCalculationRunError": "An error occurred while executing the dynamic margin calculation", - "DynamicMarginCalculationStartTime": "Start time", - "DynamicMarginCalculationStopTime": "Stop time", - "DynamicMarginCalculationMarginCalculationStartTime": "Margin calculation start time", - "DynamicMarginCalculationLoadIncreaseStartTime": "Load increase start time", - "DynamicMarginCalculationLoadIncreaseStopTime": "Load increase stop time", - "DynamicMarginCalculationCalculationType": "Calculation type", - "DynamicMarginCalculationAccuracy": "Accuracy", - "DynamicMarginCalculationLoadModelsRule": "Load models rule", - "DynamicMarginCalculationLoadsVariations": "Load variations", "DynamicMarginCalculationTabStatus": "Status", "TwoSides.ONE": "Origin side", diff --git a/src/translations/fr.json b/src/translations/fr.json index 3d0cf6d8c2..6b3f0a4bb2 100644 --- a/src/translations/fr.json +++ b/src/translations/fr.json @@ -364,19 +364,10 @@ "DynamicSecurityAnalysisContingenciesStartTime": "Temps de début des aléas", "DynamicSecurityAnalysisTabStatus": "Statut", - "startDynamicMarginCalculationError": "Une erreur est survenue lors du lancement de la calculation de marge dynamique", + "startDynamicMarginCalculationError": "Une erreur est survenue lors du lancement du calcul de marge dynamique", "DynamicMarginCalculation": "Calculation de marge dynamique", - "DynamicMarginCalculationRunError": "Une erreur est survenue lors de l'exécution de la calculation de marge dynamique", - "DynamicMarginCalculationStartTime": "Temps de début", - "DynamicMarginCalculationStopTime": "Temps d'arrêt", - "DynamicMarginCalculationMarginCalculationStartTime": "Temps de début de la calculation de marge", - "DynamicMarginCalculationLoadIncreaseStartTime": "Temps de début de l'augmentation de charge", - "DynamicMarginCalculationLoadIncreaseStopTime": "Temps d'arrêt de l'augmentation de charge", - "DynamicMarginCalculationCalculationType": "Type de calculation", - "DynamicMarginCalculationAccuracy": "Précision", - "DynamicMarginCalculationLoadModelsRule": "Règle des modèles de charge", - "DynamicMarginCalculationLoadsVariations": "Variations de charge", - "DynamicMarginCalculationTabStatus": "Status", + "DynamicMarginCalculationRunError": "Une erreur est survenue lors de l'exécution du calcul de marge dynamique", + "DynamicMarginCalculationTabStatus": "Statut", "TwoSides.ONE": "Côté 1", "TwoSides.TWO": "Côté 2", From 5989be5ed033192eb4f08ab0caae27c6148fc1d7 Mon Sep 17 00:00:00 2001 From: Thang PHAM Date: Wed, 28 Jan 2026 14:30:45 +0100 Subject: [PATCH 3/6] Parameters and result Signed-off-by: Thang PHAM --- .../use-all-computing-status.ts | 2 +- src/components/parameters-tabs.tsx | 52 +++++++- src/components/result-view-tab.tsx | 24 ++++ ...dynamic-margin-calculation-result-logs.tsx | 46 +++++++ ...ic-margin-calculation-result-synthesis.tsx | 115 ++++++++++++++++++ .../dynamic-margin-calculation-result-tab.tsx | 79 ++++++++++++ src/services/dynamic-margin-calculation.ts | 9 +- .../study/dynamic-margin-calculation.ts | 96 +++------------ .../study/dynamic-margin-calculation.type.ts | 47 ------- 9 files changed, 331 insertions(+), 139 deletions(-) create mode 100644 src/components/results/dynamic-margin-calculation/dynamic-margin-calculation-result-logs.tsx create mode 100644 src/components/results/dynamic-margin-calculation/dynamic-margin-calculation-result-synthesis.tsx create mode 100644 src/components/results/dynamic-margin-calculation/dynamic-margin-calculation-result-tab.tsx delete mode 100644 src/services/study/dynamic-margin-calculation.type.ts diff --git a/src/components/computing-status/use-all-computing-status.ts b/src/components/computing-status/use-all-computing-status.ts index 034ad7bfe0..008846af77 100644 --- a/src/components/computing-status/use-all-computing-status.ts +++ b/src/components/computing-status/use-all-computing-status.ts @@ -117,7 +117,7 @@ export const loadflowResultInvalidations = [NotificationType.LOADFLOW_RESULT]; export const securityAnalysisResultInvalidations = [NotificationType.SECURITY_ANALYSIS_RESULT]; export const dynamicSimulationResultInvalidations = [NotificationType.DYNAMIC_SIMULATION_RESULT]; export const dynamicSecurityAnalysisResultInvalidations = [NotificationType.DYNAMIC_SECURITY_ANALYSIS_RESULT]; -export const dynamicMarginCalculationInvalidations = [NotificationType.DYNAMIC_MARGIN_CALCULATION_RESULT]; +export const dynamicMarginCalculationResultInvalidations = [NotificationType.DYNAMIC_MARGIN_CALCULATION_RESULT]; export const voltageInitResultInvalidations = [NotificationType.VOLTAGE_INIT_RESULT]; export const stateEstimationResultInvalidations = [NotificationType.STATE_ESTIMATION_RESULT]; export const pccMinResultInvalidations = [NotificationType.PCC_MIN_RESULT]; diff --git a/src/components/parameters-tabs.tsx b/src/components/parameters-tabs.tsx index 10c400f305..c7bbd02109 100644 --- a/src/components/parameters-tabs.tsx +++ b/src/components/parameters-tabs.tsx @@ -47,6 +47,8 @@ import { cancelLeaveParametersTab, confirmLeaveParametersTab, setDirtyComputatio import type { UUID } from 'node:crypto'; import { ComputingType, + DynamicMarginCalculationInline, + fetchDynamicMarginCalculationProviders, fetchSecurityAnalysisProviders, getSecurityAnalysisDefaultLimitReductions, LoadFlowParametersInline, @@ -69,6 +71,11 @@ import { } from 'services/study/short-circuit-analysis'; import { useGetPccMinParameters } from './dialogs/parameters/use-get-pcc-min-parameters'; import { useWorkspacePanelActions } from './workspace/hooks/use-workspace-panel-actions'; +import { fetchDefaultDynamicSecurityAnalysisProvider } from '../services/study/dynamic-security-analysis'; +import { + fetchDynamicMarginCalculationParameters, + updateDynamicMarginCalculationParameters, +} from '../services/study/dynamic-margin-calculation'; enum TAB_VALUES { lfParamsTabValue = 'LOAD_FLOW', @@ -77,6 +84,7 @@ enum TAB_VALUES { shortCircuitParamsTabValue = 'SHORT_CIRCUIT', dynamicSimulationParamsTabValue = 'DYNAMIC_SIMULATION', dynamicSecurityAnalysisParamsTabValue = 'DYNAMIC_SECURITY_ANALYSIS', + dynamicMarginCalculationParamsTabValue = 'DYNAMIC_MARGIN_CALCULATION', voltageInitParamsTabValue = 'VOLTAGE_INITIALIZATION', stateEstimationTabValue = 'STATE_ESTIMATION', pccMinTabValue = 'PCC_MIN', @@ -106,6 +114,9 @@ const ParametersTabs: FunctionComponent = () => { const sensitivityAnalysisAvailability = useOptionalServiceStatus(OptionalServicesNames.SensitivityAnalysis); const dynamicSimulationAvailability = useOptionalServiceStatus(OptionalServicesNames.DynamicSimulation); const dynamicSecurityAnalysisAvailability = useOptionalServiceStatus(OptionalServicesNames.DynamicSecurityAnalysis); + const dynamicMarginCalculationAvailability = useOptionalServiceStatus( + OptionalServicesNames.DynamicMarginCalculation + ); const voltageInitAvailability = useOptionalServiceStatus(OptionalServicesNames.VoltageInit); const shortCircuitAvailability = useOptionalServiceStatus(OptionalServicesNames.ShortCircuit); const stateEstimationAvailability = useOptionalServiceStatus(OptionalServicesNames.StateEstimation); @@ -192,7 +203,7 @@ const ParametersTabs: FunctionComponent = () => { ComputingType.SHORT_CIRCUIT, OptionalServicesStatus.Up, null, - null, + fetchDefaultDynamicSecurityAnalysisProvider, null, null, getShortCircuitParameters, @@ -201,6 +212,24 @@ const ParametersTabs: FunctionComponent = () => { ); useParametersNotification(ComputingType.SHORT_CIRCUIT, OptionalServicesStatus.Up, shortCircuitParametersBackend); + const dynamicMarginCalculationParametersBackend = useParametersBackend( + user, + studyUuid, + ComputingType.DYNAMIC_MARGIN_CALCULATION, + dynamicMarginCalculationAvailability, + fetchDynamicMarginCalculationProviders, + null, + null, + null, + fetchDynamicMarginCalculationParameters, + updateDynamicMarginCalculationParameters + ); + useParametersNotification( + ComputingType.DYNAMIC_MARGIN_CALCULATION, + dynamicMarginCalculationAvailability, + dynamicMarginCalculationParametersBackend + ); + const pccMinParameters = useGetPccMinParameters(); const voltageInitParameters = useGetVoltageInitParameters(); const useStateEstimationParameters = useGetStateEstimationParameters(); @@ -269,7 +298,8 @@ const ParametersTabs: FunctionComponent = () => { oldValue === TAB_VALUES.shortCircuitParamsTabValue || oldValue === TAB_VALUES.pccMinTabValue || oldValue === TAB_VALUES.dynamicSimulationParamsTabValue || - oldValue === TAB_VALUES.dynamicSecurityAnalysisParamsTabValue)) || + oldValue === TAB_VALUES.dynamicSecurityAnalysisParamsTabValue || + oldValue === TAB_VALUES.dynamicMarginCalculationParamsTabValue)) || oldValue === TAB_VALUES.stateEstimationTabValue ) { return TAB_VALUES.securityAnalysisParamsTabValue; @@ -331,6 +361,16 @@ const ParametersTabs: FunctionComponent = () => { return ; case TAB_VALUES.dynamicSecurityAnalysisParamsTabValue: return ; + case TAB_VALUES.dynamicMarginCalculationParamsTabValue: + return ( + + ); + case TAB_VALUES.voltageInitParamsTabValue: return ( { shortCircuitParametersBackend, pccMinParameters, user, + dynamicMarginCalculationParametersBackend, voltageInitParameters, useStateEstimationParameters, networkVisualizationsParameters, @@ -435,6 +476,13 @@ const ParametersTabs: FunctionComponent = () => { value={TAB_VALUES.dynamicSecurityAnalysisParamsTabValue} /> ) : null} + {isDeveloperMode ? ( + } + value={TAB_VALUES.dynamicMarginCalculationParamsTabValue} + /> + ) : null} } diff --git a/src/components/result-view-tab.tsx b/src/components/result-view-tab.tsx index 1a2e422444..e1229e3365 100644 --- a/src/components/result-view-tab.tsx +++ b/src/components/result-view-tab.tsx @@ -29,6 +29,7 @@ import { useParameterState } from './dialogs/parameters/use-parameters-state'; import { IService } from './result-view-tab.type'; import { CurrentTreeNode } from './graph/tree-node.type'; import { PccMinResultTab } from './results/pccmin/pcc-min-result-tab'; +import DynamicMarginCalculationResultTab from './results/dynamic-margin-calculation/dynamic-margin-calculation-result-tab'; const styles = { table: { @@ -77,6 +78,9 @@ export const ResultViewTab: FunctionComponent = ({ const sensitivityAnalysisUnavailability = useOptionalServiceStatus(OptionalServicesNames.SensitivityAnalysis); const dynamicSimulationAvailability = useOptionalServiceStatus(OptionalServicesNames.DynamicSimulation); const dynamicSecurityAnalysisAvailability = useOptionalServiceStatus(OptionalServicesNames.DynamicSecurityAnalysis); + const dynamicMarginCalculationAvailability = useOptionalServiceStatus( + OptionalServicesNames.DynamicMarginCalculation + ); const voltageInitAvailability = useOptionalServiceStatus(OptionalServicesNames.VoltageInit); const shortCircuitAvailability = useOptionalServiceStatus(OptionalServicesNames.ShortCircuit); const stateEstimationAvailability = useOptionalServiceStatus(OptionalServicesNames.StateEstimation); @@ -166,6 +170,18 @@ export const ResultViewTab: FunctionComponent = ({ ); }, [studyUuid, currentNode, currentRootNetworkUuid]); + const renderDynamicMarginCalculationResult = useMemo(() => { + return ( + + + + ); + }, [studyUuid, currentNode, currentRootNetworkUuid]); + const renderStateEstimationResult = useMemo(() => { return ( @@ -228,6 +244,12 @@ export const ResultViewTab: FunctionComponent = ({ displayed: isDeveloperMode && dynamicSecurityAnalysisAvailability === OptionalServicesStatus.Up, renderResult: renderDynamicSecurityAnalysisResult, }, + { + id: 'DynamicMarginCalculation', + computingType: [ComputingType.DYNAMIC_MARGIN_CALCULATION], + displayed: isDeveloperMode && dynamicMarginCalculationAvailability === OptionalServicesStatus.Up, + renderResult: renderDynamicMarginCalculationResult, + }, { id: 'VoltageInit', computingType: [ComputingType.VOLTAGE_INITIALIZATION], @@ -260,6 +282,8 @@ export const ResultViewTab: FunctionComponent = ({ renderDynamicSimulationResult, dynamicSecurityAnalysisAvailability, renderDynamicSecurityAnalysisResult, + dynamicMarginCalculationAvailability, + renderDynamicMarginCalculationResult, voltageInitAvailability, renderVoltageInitResult, stateEstimationAvailability, diff --git a/src/components/results/dynamic-margin-calculation/dynamic-margin-calculation-result-logs.tsx b/src/components/results/dynamic-margin-calculation/dynamic-margin-calculation-result-logs.tsx new file mode 100644 index 0000000000..930c2ec33d --- /dev/null +++ b/src/components/results/dynamic-margin-calculation/dynamic-margin-calculation-result-logs.tsx @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { ComputationReportViewer } from '../common/computation-report-viewer'; +import { memo, useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { AppState } from '../../../redux/reducer'; +import { ComputingType } from '@gridsuite/commons-ui'; +import RunningStatus from '../../utils/running-status'; +import { useIntlResultStatusMessages } from '../../utils/aggrid-rows-handler'; +import { useIntl } from 'react-intl'; +import Overlay from '../common/Overlay'; + +const DynamicMarginCalculationResultLogs = memo(() => { + const dynamicMarginCalculationStatus = useSelector( + (state: AppState) => state.computingStatus[ComputingType.DYNAMIC_MARGIN_CALCULATION] + ); + + const intl = useIntl(); + const messages = useIntlResultStatusMessages(intl); + + const overlayMessage = useMemo(() => { + switch (dynamicMarginCalculationStatus) { + case RunningStatus.IDLE: + return messages.noCalculation; + case RunningStatus.RUNNING: + return messages.running; + case RunningStatus.FAILED: + case RunningStatus.SUCCEED: + return undefined; + default: + return messages.noCalculation; + } + }, [dynamicMarginCalculationStatus, messages]); + return ( + + + + ); +}); + +export default DynamicMarginCalculationResultLogs; diff --git a/src/components/results/dynamic-margin-calculation/dynamic-margin-calculation-result-synthesis.tsx b/src/components/results/dynamic-margin-calculation/dynamic-margin-calculation-result-synthesis.tsx new file mode 100644 index 0000000000..11b8e534d5 --- /dev/null +++ b/src/components/results/dynamic-margin-calculation/dynamic-margin-calculation-result-synthesis.tsx @@ -0,0 +1,115 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { useIntl } from 'react-intl'; +import { Box, LinearProgress } from '@mui/material'; +import { memo, useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { getNoRowsMessage, useIntlResultStatusMessages } from '../../utils/aggrid-rows-handler'; +import { makeAgGridCustomHeaderColumn } from '../../custom-aggrid/utils/custom-aggrid-header-utils'; +import { ComputingType, CustomAGGrid, DefaultCellRenderer, type MuiStyles } from '@gridsuite/commons-ui'; +import { COL_STATUS, StatusCellRender } from '../common/result-cell-renderers'; +import type { UUID } from 'node:crypto'; +import { AppState } from '../../../redux/reducer'; +import { fetchDynamicMarginCalculationStatus } from '../../../services/study/dynamic-margin-calculation'; +import { MEDIUM_COLUMN_WIDTH } from '../dynamicsimulation/utils/dynamic-simulation-result-utils'; +import { dynamicMarginCalculationResultInvalidations } from '../../computing-status/use-all-computing-status'; +import { useNodeData } from 'components/use-node-data'; +import { AGGRID_LOCALES } from '../../../translations/not-intl/aggrid-locales'; + +const styles = { + loader: { + height: '4px', + }, +} as const satisfies MuiStyles; + +const defaultColDef = { + filter: true, + sortable: true, + resizable: true, + lockPinned: true, + suppressMovable: true, + wrapHeaderText: true, + autoHeaderHeight: true, + cellRenderer: DefaultCellRenderer, +}; + +type DynamicMarginCalculationResultSynthesisProps = { + studyUuid: UUID; + nodeUuid: UUID; + currentRootNetworkUuid: UUID; +}; + +const DynamicMarginCalculationResultSynthesis = memo( + ({ nodeUuid, studyUuid, currentRootNetworkUuid }: DynamicMarginCalculationResultSynthesisProps) => { + const intl = useIntl(); + + const { result, isLoading } = useNodeData({ + studyUuid, + nodeUuid, + rootNetworkUuid: currentRootNetworkUuid, + fetcher: fetchDynamicMarginCalculationStatus, + invalidations: dynamicMarginCalculationResultInvalidations, + resultConverter: (status: string | null) => { + return status === null + ? undefined + : [ + { + [COL_STATUS]: status, + }, + ]; + }, + }); + + const columnDefs = useMemo( + () => [ + makeAgGridCustomHeaderColumn({ + headerName: intl.formatMessage({ + id: COL_STATUS, + }), + colId: COL_STATUS, + field: COL_STATUS, + width: MEDIUM_COLUMN_WIDTH, + cellRenderer: StatusCellRender, + }), + ], + [intl] + ); + + // messages to show when no data + const dynamicMarginCalculationStatus = useSelector( + (state: AppState) => state.computingStatus[ComputingType.DYNAMIC_SECURITY_ANALYSIS] + ); + const messages = useIntlResultStatusMessages(intl, true); + const overlayMessage = useMemo( + () => getNoRowsMessage(messages, result, dynamicMarginCalculationStatus, !isLoading), + [messages, result, dynamicMarginCalculationStatus, isLoading] + ); + + const rowDataToShow = useMemo(() => (overlayMessage ? [] : result), [result, overlayMessage]); + + return ( + <> + {isLoading && ( + + + + )} + + + ); + } +); + +export default DynamicMarginCalculationResultSynthesis; diff --git a/src/components/results/dynamic-margin-calculation/dynamic-margin-calculation-result-tab.tsx b/src/components/results/dynamic-margin-calculation/dynamic-margin-calculation-result-tab.tsx new file mode 100644 index 0000000000..6f0f08fe9e --- /dev/null +++ b/src/components/results/dynamic-margin-calculation/dynamic-margin-calculation-result-tab.tsx @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { SyntheticEvent, useState } from 'react'; +import { Box, Tab, Tabs } from '@mui/material'; +import { useIntl } from 'react-intl'; +import TabPanelLazy from '../common/tab-panel-lazy'; +import type { UUID } from 'node:crypto'; +import { type MuiStyles } from '@gridsuite/commons-ui'; +import DynamicMarginCalculationResultSynthesis from './dynamic-margin-calculation-result-synthesis'; +import DynamicMarginCalculationResultLogs from './dynamic-margin-calculation-result-logs'; + +const styles = { + resultContainer: { + flexGrow: 1, + }, +} as const satisfies MuiStyles; + +const TAB_INDEX_STATUS = 'DynamicMarginCalculationTabStatus'; +const TAB_INDEX_LOGS = 'ComputationResultsLogs'; + +interface DynamicMarginCalculationResultTabProps { + studyUuid: UUID; + nodeUuid: UUID; + currentRootNetworkUuid: UUID; +} + +function DynamicMarginCalculationResultTab({ + studyUuid, + nodeUuid, + currentRootNetworkUuid, +}: Readonly) { + const intl = useIntl(); + + const [tabIndex, setTabIndex] = useState(TAB_INDEX_STATUS); + + const handleTabChange = (event: SyntheticEvent, newTabIndex: string) => { + setTabIndex(newTabIndex); + }; + + return ( + <> + + + + + + + + + + + + + + + + ); +} + +export default DynamicMarginCalculationResultTab; diff --git a/src/services/dynamic-margin-calculation.ts b/src/services/dynamic-margin-calculation.ts index 5307b37924..73137bb752 100644 --- a/src/services/dynamic-margin-calculation.ts +++ b/src/services/dynamic-margin-calculation.ts @@ -4,7 +4,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { backendFetch, backendFetchJson } from '@gridsuite/commons-ui'; +import { backendFetch } from '@gridsuite/commons-ui'; import type { UUID } from 'node:crypto'; const PREFIX_DYNAMIC_MARGIN_CALCULATION_SERVER_QUERIES = @@ -14,13 +14,6 @@ function getDynamicMarginCalculationUrl() { return `${PREFIX_DYNAMIC_MARGIN_CALCULATION_SERVER_QUERIES}/v1/`; } -export function fetchDynamicMarginCalculationProviders() { - console.info('fetch dynamic margin calculation providers'); - const url = getDynamicMarginCalculationUrl() + 'providers'; - console.debug(url); - return backendFetchJson(url); -} - export function downloadDebugFileDynamicMarginCalculation(resultUuid: UUID): Promise { console.info(`Download dynamic margin calculation debug file of '${resultUuid}' ...`); diff --git a/src/services/study/dynamic-margin-calculation.ts b/src/services/study/dynamic-margin-calculation.ts index a39a9a318c..4bd32ec86c 100644 --- a/src/services/study/dynamic-margin-calculation.ts +++ b/src/services/study/dynamic-margin-calculation.ts @@ -4,14 +4,17 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { getStudyUrl, getStudyUrlWithNodeUuidAndRootNetworkUuid, PREFIX_STUDY_QUERIES } from './index'; -import { backendFetch, backendFetchJson, backendFetchText } from '@gridsuite/commons-ui'; import type { UUID } from 'node:crypto'; +import { getStudyUrl, getStudyUrlWithNodeUuidAndRootNetworkUuid } from './index'; import { + backendFetch, + backendFetchJson, + backendFetchText, + cleanLoadFilterNames, DynamicMarginCalculationParametersFetchReturn, DynamicMarginCalculationParametersInfos, -} from './dynamic-margin-calculation.type'; -import { fetchContingencyAndFiltersLists } from '../directory'; + enrichLoadFilterNames, +} from '@gridsuite/commons-ui'; export function startDynamicMarginCalculation( studyUuid: UUID, @@ -72,34 +75,6 @@ export function fetchDynamicMarginCalculationStatus( return backendFetchText(url); } -export function fetchDynamicMarginCalculationProvider(studyUuid: UUID) { - console.info(`Fetching dynamic margin calculation provider on study '${studyUuid}' ...`); - const url = getStudyUrl(studyUuid) + '/dynamic-margin-calculation/provider'; - console.debug(url); - return backendFetchText(url); -} - -export function fetchDefaultDynamicMarginCalculationProvider() { - console.info('Fetching default dynamic margin calculation provider'); - const url = PREFIX_STUDY_QUERIES + '/v1/dynamic-margin-calculation-default-provider'; - console.debug(url); - return backendFetchText(url); -} - -export function updateDynamicMarginCalculationProvider(studyUuid: UUID, newProvider: string) { - console.info(`Updating dynamic margin calculation provider on study '${studyUuid}' ...`); - const url = getStudyUrl(studyUuid) + '/dynamic-margin-calculation/provider'; - console.debug(url); - return backendFetch(url, { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - body: newProvider, - }); -} - export function fetchDynamicMarginCalculationParameters( studyUuid: UUID ): Promise { @@ -107,39 +82,14 @@ export function fetchDynamicMarginCalculationParameters( const url = getStudyUrl(studyUuid) + '/dynamic-margin-calculation/parameters'; console.debug(url); const parametersPromise: Promise = backendFetchJson(url); + return parametersPromise.then(enrichLoadFilterNames); +} - // enrich LoadsVariationInfos by LoadsVariationFetchReturn with id and name infos - return parametersPromise.then((parameters) => { - if (parameters?.loadsVariations) { - const loadsVariations = parameters?.loadsVariations; - const allLoadFilterUuids = loadsVariations.flatMap((loadVariation) => loadVariation.loadFilterUuids ?? []); - return fetchContingencyAndFiltersLists(allLoadFilterUuids).then((loadFilterInfos) => { - delete parameters.loadsVariations; - const loadFilterInfosMap = Object.fromEntries( - loadFilterInfos.map((info) => [info.elementUuid, info.elementName]) - ); - return { - ...parameters, - loadsVariationsInfos: loadsVariations?.map((infos) => { - const newLoadVariationInfos = { - ...infos, - loadFiltersInfos: infos.loadFilterUuids?.map((loadFilterUuid) => ({ - id: loadFilterUuid, - name: loadFilterInfosMap[loadFilterUuid], - })), - }; - delete newLoadVariationInfos.loadFilterUuids; - return newLoadVariationInfos; - }), - }; - }); - } - delete parameters.loadsVariations; - return { - ...parameters, - loadsVariationsInfos: [], - }; - }); +export function fetchDynamicMarginCalculationProvider(studyUuid: UUID) { + console.info(`Fetching dynamic margin calculation provider on study '${studyUuid}' ...`); + const url = getStudyUrl(studyUuid) + '/dynamic-margin-calculation/provider'; + console.debug(url); + return backendFetchText(url); } export function updateDynamicMarginCalculationParameters( @@ -149,23 +99,7 @@ export function updateDynamicMarginCalculationParameters( console.info(`Setting dynamic margin calculation parameters on study '${studyUuid}' ...`); const url = getStudyUrl(studyUuid) + '/dynamic-margin-calculation/parameters'; console.debug(url); - - // send to back raw LoadsVariations instead of LoadsVariationsInfos - const newParameters = - newParams != null - ? { - ...newParams, - loadsVariations: newParams?.loadsVariationsInfos?.map((infos) => { - const newLoadsVariationInfos = { - ...infos, - loadFilterUuids: infos.loadFiltersInfos?.map((loadFilterInfos) => loadFilterInfos.id), - }; - delete newLoadsVariationInfos.loadFiltersInfos; - return newLoadsVariationInfos; - }), - } - : newParams; - delete newParameters?.loadsVariationsInfos; + const newParameters = newParams !== null ? cleanLoadFilterNames(newParams) : newParams; return backendFetch(url, { method: 'POST', headers: { diff --git a/src/services/study/dynamic-margin-calculation.type.ts b/src/services/study/dynamic-margin-calculation.type.ts deleted file mode 100644 index f538da20dc..0000000000 --- a/src/services/study/dynamic-margin-calculation.type.ts +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Copyright (c) 2026, RTE (http://www.rte-france.com) - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ -import type { UUID } from 'node:crypto'; - -export enum CalculationType { - GLOBAL_MARGIN = 'GLOBAL_MARGIN', - LOCAL_MARGIN = 'LOCAL_MARGIN', -} - -export enum LoadModelsRule { - ALL_LOADS = 'ALL_LOADS', - TARGETED_LOADS = 'TARGETED_LOADS', -} - -export type LoadsVariationInfos = { - id?: UUID; // persisted id of the info to be modified - loadFilterUuids?: UUID[]; - variation: number; -}; - -export type LoadsVariationFetchReturn = Exclude & { - loadFiltersInfos?: { id: string; name: string }[]; -}; - -export type DynamicMarginCalculationParametersInfos = { - provider?: string; - startTime?: number; - stopTime?: number; - marginCalculationStartTime?: number; - loadIncreaseStartTime?: number; - loadIncreaseStopTime?: number; - calculationType?: CalculationType; - accuracy?: number; // integer - loadModelsRule?: LoadModelsRule; - loadsVariations?: LoadsVariationInfos[]; -}; - -export type DynamicMarginCalculationParametersFetchReturn = Exclude< - DynamicMarginCalculationParametersInfos, - 'loadsVariations' -> & { - loadsVariationsInfos?: LoadsVariationFetchReturn[]; -}; From 8ea5b5abb8bea6e2862d66ec51d758d73ea66c84 Mon Sep 17 00:00:00 2001 From: Thang PHAM Date: Wed, 28 Jan 2026 14:46:19 +0100 Subject: [PATCH 4/6] Remove isDeveloperMode Signed-off-by: Thang PHAM --- src/components/parameters-tabs.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/parameters-tabs.tsx b/src/components/parameters-tabs.tsx index c7bbd02109..105d3da77b 100644 --- a/src/components/parameters-tabs.tsx +++ b/src/components/parameters-tabs.tsx @@ -367,7 +367,6 @@ const ParametersTabs: FunctionComponent = () => { studyUuid={studyUuid} setHaveDirtyFields={setDirtyFields} parametersBackend={dynamicMarginCalculationParametersBackend} - isDeveloperMode={isDeveloperMode} /> ); From e0d00b2a260ffd8110cd1dfce37bcfedcae66cc3 Mon Sep 17 00:00:00 2001 From: Thang PHAM Date: Sun, 1 Feb 2026 23:00:03 +0100 Subject: [PATCH 5/6] do not fetch filter names from front-end Signed-off-by: Thang PHAM --- src/services/study/dynamic-margin-calculation.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/services/study/dynamic-margin-calculation.ts b/src/services/study/dynamic-margin-calculation.ts index 4bd32ec86c..f456372d83 100644 --- a/src/services/study/dynamic-margin-calculation.ts +++ b/src/services/study/dynamic-margin-calculation.ts @@ -10,10 +10,7 @@ import { backendFetch, backendFetchJson, backendFetchText, - cleanLoadFilterNames, - DynamicMarginCalculationParametersFetchReturn, DynamicMarginCalculationParametersInfos, - enrichLoadFilterNames, } from '@gridsuite/commons-ui'; export function startDynamicMarginCalculation( @@ -77,12 +74,11 @@ export function fetchDynamicMarginCalculationStatus( export function fetchDynamicMarginCalculationParameters( studyUuid: UUID -): Promise { +): Promise { console.info(`Fetching dynamic margin calculation parameters on study '${studyUuid}' ...`); const url = getStudyUrl(studyUuid) + '/dynamic-margin-calculation/parameters'; console.debug(url); - const parametersPromise: Promise = backendFetchJson(url); - return parametersPromise.then(enrichLoadFilterNames); + return backendFetchJson(url); } export function fetchDynamicMarginCalculationProvider(studyUuid: UUID) { @@ -94,18 +90,17 @@ export function fetchDynamicMarginCalculationProvider(studyUuid: UUID) { export function updateDynamicMarginCalculationParameters( studyUuid: UUID, - newParams: DynamicMarginCalculationParametersFetchReturn | null + newParams: DynamicMarginCalculationParametersInfos | null ): Promise { console.info(`Setting dynamic margin calculation parameters on study '${studyUuid}' ...`); const url = getStudyUrl(studyUuid) + '/dynamic-margin-calculation/parameters'; console.debug(url); - const newParameters = newParams !== null ? cleanLoadFilterNames(newParams) : newParams; return backendFetch(url, { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json', }, - body: JSON.stringify(newParameters), + body: JSON.stringify(newParams), }); } From 0fd5576f3acd6d829bddbf036d4f95f749d3d933 Mon Sep 17 00:00:00 2001 From: Thang PHAM Date: Mon, 2 Feb 2026 23:38:21 +0100 Subject: [PATCH 6/6] translation Signed-off-by: Thang PHAM --- src/translations/fr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/translations/fr.json b/src/translations/fr.json index f8c053dec6..f97aed1b29 100644 --- a/src/translations/fr.json +++ b/src/translations/fr.json @@ -366,7 +366,7 @@ "DynamicSecurityAnalysisTabStatus": "Statut", "startDynamicMarginCalculationError": "Une erreur est survenue lors du lancement du calcul de marge dynamique", - "DynamicMarginCalculation": "Calculation de marge dynamique", + "DynamicMarginCalculation": "Calcul de marge dynamique", "DynamicMarginCalculationRunError": "Une erreur est survenue lors de l'exécution du calcul de marge dynamique", "DynamicMarginCalculationTabStatus": "Statut",