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',