diff --git a/src/components/computing-status/use-all-computing-status.ts b/src/components/computing-status/use-all-computing-status.ts index 594e203977..008846af77 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 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]; @@ -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 082510b061..75b30c6765 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/parameters-tabs.tsx b/src/components/parameters-tabs.tsx index 10c400f305..105d3da77b 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,15 @@ 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 +475,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/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 79dd0723f6..2a091eecea 100644 --- a/src/components/study-container.jsx +++ b/src/components/study-container.jsx @@ -30,13 +30,13 @@ import { getWorkspacesMetadata, getWorkspace } from '../services/study/workspace import WaitingLoader from './utils/waiting-loader'; import { hasElementPermission, - PermissionType, NotificationsUrlKeys, + parseError, + PermissionType, snackWithFallback, useIntlRef, useNotificationsListener, useSnackMessage, - parseError, } from '@gridsuite/commons-ui'; import NetworkModificationTreeModel from './graph/network-modification-tree-model'; import { getFirstNodeOfType } from './graph/util/model-functions'; @@ -208,6 +208,11 @@ export function StudyContainer() { headerId: 'DynamicSecurityAnalysisRunError', }); } + if (updateTypeHeader === NotificationType.DYNAMIC_MARGIN_CALCULATION_FAILED) { + snackWithFallback(snackError, parseError(errorMessage), { + headerId: 'DynamicMarginCalculationRunError', + }); + } if (updateTypeHeader === NotificationType.VOLTAGE_INIT_FAILED) { snackWithFallback(snackError, parseError(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 919396bf83..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, diff --git a/src/services/dynamic-margin-calculation.ts b/src/services/dynamic-margin-calculation.ts new file mode 100644 index 0000000000..73137bb752 --- /dev/null +++ b/src/services/dynamic-margin-calculation.ts @@ -0,0 +1,27 @@ +/** + * 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 } 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 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..f456372d83 --- /dev/null +++ b/src/services/study/dynamic-margin-calculation.ts @@ -0,0 +1,106 @@ +/** + * 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 { getStudyUrl, getStudyUrlWithNodeUuidAndRootNetworkUuid } from './index'; +import { + backendFetch, + backendFetchJson, + backendFetchText, + DynamicMarginCalculationParametersInfos, +} from '@gridsuite/commons-ui'; + +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 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); + return backendFetchJson(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 updateDynamicMarginCalculationParameters( + studyUuid: UUID, + 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); + return backendFetch(url, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(newParams), + }); +} diff --git a/src/translations/en.json b/src/translations/en.json index 979bacb615..09bf02a17f 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -367,6 +367,11 @@ "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", + "DynamicMarginCalculationTabStatus": "Status", + "TwoSides.ONE": "Origin side", "TwoSides.TWO": "Extremity side", diff --git a/src/translations/fr.json b/src/translations/fr.json index 1b998ad5bc..f97aed1b29 100644 --- a/src/translations/fr.json +++ b/src/translations/fr.json @@ -365,6 +365,11 @@ "DynamicSecurityAnalysisContingenciesStartTime": "Temps de début des aléas", "DynamicSecurityAnalysisTabStatus": "Statut", + "startDynamicMarginCalculationError": "Une erreur est survenue lors du lancement du calcul 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", + "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 58824dd774..e2b78b9d1e 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',