Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/**
* 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 { FunctionComponent, useMemo } from 'react';
import { useFieldArray } from 'react-hook-form';
import { useIntl } from 'react-intl';
import { IconButton, Tooltip } from '@mui/material';
import InfoIcon from '@mui/icons-material/Info';
import { DndColumn, DndColumnType, DndTable, ElementType, EquipmentType } from '@gridsuite/commons-ui';
import { AREA_INVALIDATIONS, FILTER, INVALIDATE, INVALIDATION_TYPE } from '../../../utils/field-constants';
import { InvalidationType, TabValue } from './state-estimation-parameters-utils';

const invalidationTypeLabelIds: Record<InvalidationType, string> = {
[InvalidationType.ERRONEOUS_TOPOLOGY_PV]: 'StateEstimationParametersInvalidationTypeErroneousTopologyPv',
[InvalidationType.ERRONEOUS_TOPOLOGY_PQ]: 'StateEstimationParametersInvalidationTypeErroneousTopologyPq',
[InvalidationType.NOT_FULLY_DESCRIBED]: 'StateEstimationParametersInvalidationTypeNotFullyDescribed',
};

export const StateEstimationAreaInvalidationParameters: FunctionComponent = () => {
const intl = useIntl();
const formName = `${TabValue.AREA_INVALIDATION}.${AREA_INVALIDATIONS}`;

const columnsDefinition = useMemo<DndColumn[]>(() => {
const filtersHeaderTooltip = (
<Tooltip
title={intl.formatMessage({ id: 'StateEstimationParametersAreaInvalidationTooltip' })}
placement="right"
>
<span>
<IconButton disabled size="small">
<InfoIcon fontSize="small" />
</IconButton>
</span>
</Tooltip>
);

return [
{
label: intl.formatMessage({ id: 'StateEstimationParametersInvalidateLabel' }),
dataKey: INVALIDATE,
initialValue: false,
editable: true,
type: DndColumnType.SWITCH,
dataTestId: 'SubstationAreaInvalidationInvalidateInput',
},
{
label: intl.formatMessage({ id: 'StateEstimationParametersSubstationFilterLabel' }),
dataKey: FILTER,
initialValue: [],
editable: true,
type: DndColumnType.DIRECTORY_ITEMS,
equipmentTypes: [EquipmentType.SUBSTATION],
elementType: ElementType.FILTER,
titleId: 'StateEstimationParametersAreaInvalidationFilterSelection',
dataTestId: 'SubstationAreaInvalidationFilterInput',
extra: filtersHeaderTooltip,
},
{
label: intl.formatMessage({ id: 'StateEstimationParametersInvalidationTypeLabel' }),
dataKey: INVALIDATION_TYPE,
initialValue: InvalidationType.ERRONEOUS_TOPOLOGY_PV,
editable: true,
type: DndColumnType.SELECT,
options: Object.values(InvalidationType).map((invalidationType) => ({
id: invalidationType,
label: intl.formatMessage({ id: invalidationTypeLabelIds[invalidationType] }),
})),
dataTestId: 'SubstationAreaInvalidationInvalidationTypeInput',
},
] satisfies DndColumn[];
Comment thread
benrejebmoh marked this conversation as resolved.
}, [intl]);

const useFieldArrayOutput = useFieldArray({ name: formName });

const createRows = () => {
const newRowData: Record<string, unknown> = {};
columnsDefinition.forEach((column) => (newRowData[column.dataKey] = column.initialValue));
return [newRowData];
};

return (
<DndTable
name={formName}
useFieldArrayOutput={useFieldArrayOutput}
createRows={createRows}
columnsDefinition={columnsDefinition}
tableHeight={440}
withAddRowsDialog={false}
rowDataTestId="SubstationAreaInvalidationLine"
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import StateEstimationGeneralParameters from './state-estimation-general-paramet
import { StateEstimationWeightsParameters } from './state-estimation-weights-parameters';
import { StateEstimationQualityParameters } from './state-estimation-quality-parameters';
import { StateEstimationLoadboundsParameters } from './state-estimation-loadbounds-parameters';
import { StateEstimationAreaInvalidationParameters } from './state-estimation-area-invalidation-parameters';
import { getTabStyle } from '../../../utils/tab-utils';

interface StateEstimationParametersFormProps {
Expand Down Expand Up @@ -67,6 +68,11 @@ export const StateEstimationParametersForm = ({
value={TabValue.LOADBOUNDS}
sx={getTabStyle(tabIndexesWithError, TabValue.LOADBOUNDS)}
/>
<Tab
label={<FormattedMessage id="StateEstimationParametersAreaInvalidationTabLabel" />}
value={TabValue.AREA_INVALIDATION}
sx={getTabStyle(tabIndexesWithError, TabValue.AREA_INVALIDATION)}
/>
</Tabs>
<Grid container sx={styles.container}>
<Grid sx={styles.maxWidth}>
Expand All @@ -82,6 +88,9 @@ export const StateEstimationParametersForm = ({
<TabPanel value={tabValue} index={TabValue.LOADBOUNDS}>
<StateEstimationLoadboundsParameters />
</TabPanel>
<TabPanel value={tabValue} index={TabValue.AREA_INVALIDATION}>
<StateEstimationAreaInvalidationParameters />
</TabPanel>
</Grid>
</Grid>
</Stack>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import * as yup from 'yup';
import { YUP_REQUIRED } from '@gridsuite/commons-ui';
import {
DEFAULT_BOUNDS,
DEFAULT_FIXED_BOUNDS,
ESTIM_ALGO_TYPE,
ESTIM_LOG_LEVEL,
FILTER,
INVALIDATE,
INVALIDATION_TYPE,
P_MAX,
P_MIN,
AREA_INVALIDATIONS,
PRINCIPAL_OBSERVABLE_ZONE,
UNIQUE_PHASE,
Q_MAX,
Expand Down Expand Up @@ -63,6 +68,13 @@ export enum TabValue {
WEIGHTS = 'weights',
QUALITY = 'quality',
LOADBOUNDS = 'loadBounds',
AREA_INVALIDATION = 'areaInvalidation',
}

export enum InvalidationType {
ERRONEOUS_TOPOLOGY_PV = 'ERRONEOUS_TOPOLOGY_PV',
ERRONEOUS_TOPOLOGY_PQ = 'ERRONEOUS_TOPOLOGY_PQ',
NOT_FULLY_DESCRIBED = 'NOT_FULLY_DESCRIBED',
}

export const TAB_VALUES = Object.values(TabValue);
Expand Down Expand Up @@ -163,6 +175,15 @@ interface ThresholdsPerVoltageLevel extends ThresholdVoltageLevelCode {
interface ThresholdsPerVoltageLevelForm
extends VoltageLevelLabel, Omit<ThresholdsPerVoltageLevel, 'thresholdVoltageLevel'> {}

// one entry per substation filter; applied top to bottom, a later "invalidate" entry overrides an earlier
// one for the same substation (see the Area invalidation tab tooltip, StateEstimationParametersAreaInvalidationTooltip)
export interface AreaInvalidationInfos {
invalidate: boolean;
invalidationType: string;
filterUuid: string | null;
filterName: string | null;
}

export interface StateEstimationParameters {
estimParameters: {
principalObservableZone: boolean;
Expand Down Expand Up @@ -190,6 +211,7 @@ export interface StateEstimationParameters {
defaultFixedBounds: LoadBoundsDetailsParameters[];
};
};
areaInvalidations: AreaInvalidationInfos[];
}

const ESTIM_PARAMETERS = 'estimParameters';
Expand Down Expand Up @@ -265,6 +287,12 @@ export const fromStateEstimationParametersFormToParamValues = (
})),
},
},
[AREA_INVALIDATIONS]: params.areaInvalidation[AREA_INVALIDATIONS]?.map((areaInvalidation) => ({
[INVALIDATE]: areaInvalidation.invalidate,
[INVALIDATION_TYPE]: areaInvalidation.invalidationType,
filterUuid: areaInvalidation.filter?.[0]?.id ?? null,
filterName: areaInvalidation.filter?.[0]?.name ?? null,
})),
});

const mapVoltageLevelData = <T extends VoltageLevelCode | ThresholdVoltageLevelCode, U extends VoltageLevelLabel>(
Expand All @@ -279,34 +307,48 @@ const mapVoltageLevelData = <T extends VoltageLevelCode | ThresholdVoltageLevelC
});

export const fromStateEstimationParametersParamToFormValues = (
values: StateEstimationParameters['estimParameters']
): StateEstimationParametersForm => ({
[TabValue.GENERAL]: {
[PRINCIPAL_OBSERVABLE_ZONE]: values.principalObservableZone,
[UNIQUE_PHASE]: values.uniquePhase,
[ESTIM_LOG_LEVEL]: values.estimLogLevel,
[ESTIM_ALGO_TYPE]: values.estimAlgoType,
},
[TabValue.WEIGHTS]: {
[WEIGHTS_PARAMETERS]: mapVoltageLevelData<WeightsParameters, WeightParametersForm>(
values.weights.weightsParameters
),
},
[TabValue.QUALITY]: {
...values.quality,
[THRESHOLD_PER_VOLTAGE_LEVEL]: mapVoltageLevelData<ThresholdsPerVoltageLevel, ThresholdsPerVoltageLevelForm>(
values.quality.thresholdsPerVoltageLevel
),
},
[TabValue.LOADBOUNDS]: {
[DEFAULT_BOUNDS]: mapVoltageLevelData<LoadBoundsDetailsParameters, LoadBoundsDetailsParametersForm>(
values.loadBounds.defaultBounds
),
[DEFAULT_FIXED_BOUNDS]: mapVoltageLevelData<LoadBoundsDetailsParameters, LoadBoundsDetailsParametersForm>(
values.loadBounds.defaultFixedBounds
),
},
});
parameters: StateEstimationParameters
): StateEstimationParametersForm => {
const values = parameters.estimParameters;
return {
[TabValue.GENERAL]: {
[PRINCIPAL_OBSERVABLE_ZONE]: values.principalObservableZone,
[UNIQUE_PHASE]: values.uniquePhase,
[ESTIM_LOG_LEVEL]: values.estimLogLevel,
[ESTIM_ALGO_TYPE]: values.estimAlgoType,
},
[TabValue.WEIGHTS]: {
[WEIGHTS_PARAMETERS]: mapVoltageLevelData<WeightsParameters, WeightParametersForm>(
values.weights.weightsParameters
),
},
[TabValue.QUALITY]: {
...values.quality,
[THRESHOLD_PER_VOLTAGE_LEVEL]: mapVoltageLevelData<
ThresholdsPerVoltageLevel,
ThresholdsPerVoltageLevelForm
>(values.quality.thresholdsPerVoltageLevel),
},
[TabValue.LOADBOUNDS]: {
[DEFAULT_BOUNDS]: mapVoltageLevelData<LoadBoundsDetailsParameters, LoadBoundsDetailsParametersForm>(
values.loadBounds.defaultBounds
),
[DEFAULT_FIXED_BOUNDS]: mapVoltageLevelData<LoadBoundsDetailsParameters, LoadBoundsDetailsParametersForm>(
values.loadBounds.defaultFixedBounds
),
},
[TabValue.AREA_INVALIDATION]: {
[AREA_INVALIDATIONS]: (parameters.areaInvalidations ?? []).map((areaInvalidation) => ({
[INVALIDATE]: areaInvalidation.invalidate,
[INVALIDATION_TYPE]: areaInvalidation.invalidationType,
[FILTER]:
areaInvalidation.filterUuid != null
? [{ id: areaInvalidation.filterUuid, name: areaInvalidation.filterName ?? '' }]
: [],
})),
},
};
};

export const stateEstimationParametersFormSchema = yup.object().shape({
[TabValue.GENERAL]: yup.object().shape({
Expand Down Expand Up @@ -388,6 +430,28 @@ export const stateEstimationParametersFormSchema = yup.object().shape({
)
.required(),
}),
[TabValue.AREA_INVALIDATION]: yup.object().shape({
[AREA_INVALIDATIONS]: yup
.array()
.of(
yup.object().shape({
[INVALIDATE]: yup.boolean().required(),
[FILTER]: yup
.array()
.of(
yup.object().shape({
id: yup.string().required(),
name: yup.string(),
})
)
.min(1, YUP_REQUIRED)
.max(1)
.required(),
[INVALIDATION_TYPE]: yup.string().required(),
})
)
.required(),
}),
});

export type StateEstimationParametersForm = yup.InferType<typeof stateEstimationParametersFormSchema>;
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,7 @@ export const StateEstimationParameters = ({
const [stateEstimationParams, setStateEstimationParams] = useStateEstimationParameters;

const initialFormValues = useMemo(
() =>
stateEstimationParams
? fromStateEstimationParametersParamToFormValues(stateEstimationParams?.estimParameters)
: {},
() => (stateEstimationParams ? fromStateEstimationParametersParamToFormValues(stateEstimationParams) : {}),
[stateEstimationParams]
);

Expand Down Expand Up @@ -91,7 +88,7 @@ export const StateEstimationParameters = ({

useEffect(() => {
if (stateEstimationParams) {
reset(fromStateEstimationParametersParamToFormValues(stateEstimationParams.estimParameters));
reset(fromStateEstimationParametersParamToFormValues(stateEstimationParams));
}
}, [reset, stateEstimationParams]);

Expand Down
Loading
Loading