diff --git a/distribution/pom.xml b/distribution/pom.xml index e15e978e2f..9738559d3a 100755 --- a/distribution/pom.xml +++ b/distribution/pom.xml @@ -112,6 +112,11 @@ open-rao-search-tree-rao ${project.version} + + ${project.groupId} + open-rao-roda + ${project.version} + ${project.groupId} open-rao-sensitivity-analysis diff --git a/docs/parameters/implementation-specific-parameters.md b/docs/parameters/implementation-specific-parameters.md index c3875f8110..41135b602c 100644 --- a/docs/parameters/implementation-specific-parameters.md +++ b/docs/parameters/implementation-specific-parameters.md @@ -580,3 +580,51 @@ fast-rao-parameters: ~~~ ::: :::: + +### RODA parameters + +RODA implementation specific parameters ("roda-parameters"). Optional. + +#### forced-preventive-actions-list + +You can create an optional forced-preventive-actions-list to force some preventive actions before running the RAO. +While this is equivalent to pre-processing the network file before running the RAO, it can be useful if +you want to test different preventive actions in an outside loop, without having to pre-process (and +eventually serialize) the network. +Note that this is not currently supported in the yaml format. +Also note that when using lazy networks, this will make all the networks load at the beginning of the RAO. So you may encounter memory issues. + +- **Expected value**: a list of PowSyBl Actions. In JSON, this should be represented by a serialized ActionList. +- **Default value**: empty array +- **Usage**: these actions will be applied on the network before running the RAO. In the case of time-coupled RAO, + the actions will be applied for all timestamps. + Actions that cannot be applied (for example if the ID of the element is wrong) will be ignored (the issue will be logged in a warning). + +#### Example +::::{tabs} +:::{group-tab} JSON +~~~json +"roda-parameters": { + "forced-preventive-actions-list": { + "version": "1.3", + "actions": [ + { + "type": "PHASE_TAP_CHANGER_TAP_POSITION", + "id": "PRA_PST_BE", + "transformerId": "BBE2AA1 BBE3AA1 1", + "tapPosition": -16, + "relativeValue": false, + "side": "TWO" + }, + { + "type": "TERMINALS_CONNECTION", + "id": "Open FR1 FR2", + "elementId": "FFR1AA1 FFR2AA1 1", + "open": true + } + ] + } +} +~~~ +::: +:::: \ No newline at end of file diff --git a/ra-optimisation/roda/pom.xml b/ra-optimisation/roda/pom.xml index ec520aaeaa..65eff22c81 100644 --- a/ra-optimisation/roda/pom.xml +++ b/ra-optimisation/roda/pom.xml @@ -21,6 +21,7 @@ + ${project.groupId} open-rao-rao-api @@ -29,6 +30,66 @@ ${project.groupId} open-rao-search-tree-rao + + com.powsybl + powsybl-action-api + + + + + ch.qos.logback + logback-classic + test + + + ${project.groupId} + open-rao-commons + ${project.version} + test + test-jar + + + ${project.groupId} + open-rao-crac-impl + ${project.version} + test-jar + test + + + ${project.groupId} + open-rao-rao-result-impl + ${project.version} + + + ch.qos.logback + logback-classic + test + + + com.google.jimfs + jimfs + test + + + com.powsybl + powsybl-commons-test + test + + + com.powsybl + powsybl-config-test + test + + + org.junit.jupiter + junit-jupiter + test + + + org.mockito + mockito-core + test + \ No newline at end of file diff --git a/ra-optimisation/roda/src/main/java/com/powsybl/openrao/roda/Roda.java b/ra-optimisation/roda/src/main/java/com/powsybl/openrao/roda/Roda.java index 521a992da6..ee17bfdf6b 100644 --- a/ra-optimisation/roda/src/main/java/com/powsybl/openrao/roda/Roda.java +++ b/ra-optimisation/roda/src/main/java/com/powsybl/openrao/roda/Roda.java @@ -11,6 +11,7 @@ import com.powsybl.openrao.commons.TemporalData; import com.powsybl.openrao.commons.TemporalDataImpl; import com.powsybl.openrao.commons.Unit; +import com.powsybl.openrao.commons.logs.OpenRaoLoggerProvider; import com.powsybl.openrao.data.crac.api.Crac; import com.powsybl.openrao.data.crac.api.State; import com.powsybl.openrao.data.crac.api.cnec.FlowCnec; @@ -23,6 +24,7 @@ import com.powsybl.openrao.raoapi.parameters.extensions.OpenRaoSearchTreeParameters; import com.powsybl.openrao.raoapi.parameters.extensions.SearchTreeRaoCostlyMinMarginParameters; import com.powsybl.openrao.raoapi.parameters.extensions.SearchTreeRaoRelativeMarginsParameters; +import com.powsybl.openrao.roda.parameters.RodaParameters; import com.powsybl.openrao.searchtreerao.commons.ToolProvider; import com.powsybl.openrao.searchtreerao.commons.objectivefunction.ObjectiveFunction; import com.powsybl.openrao.searchtreerao.commons.optimizationperimeters.OptimizationPerimeter; @@ -71,6 +73,8 @@ public class Roda implements TimeCoupledRaoProvider { @Override public CompletableFuture run(TimeCoupledRaoInput timeCoupledRaoInput, RaoParameters raoParameters) { + applyForcedActions(timeCoupledRaoInput.getRaoInputs(), raoParameters.getExtension(RodaParameters.class)); + if (timeCoupledRaoInput.getRaoInputs().getTimestamps().size() == 1) { TECHNICAL_LOGS.info("[RODA] Only one time-step in inputs. Calling single time-step RAO directly: {}", DEFAULT_SINGLE_TS_RAO); return runSingleTsRao(timeCoupledRaoInput, raoParameters); @@ -212,6 +216,18 @@ public CompletableFuture run(TimeCoupledRaoInput timeCoupl return CompletableFuture.completedFuture(timeCoupledRaoResult); } + static void applyForcedActions(TemporalData raoInputs, RodaParameters rodaParameters) { + if (rodaParameters == null || rodaParameters.getForcedPreventiveActions().isEmpty()) { + return; + } + OpenRaoLoggerProvider.BUSINESS_LOGS.info(String.format("Applying %d forced preventive actions before running RAO.", rodaParameters.getForcedPreventiveActions().size())); + raoInputs.getDataPerTimestamp().values().stream().map(RaoInput::getNetwork).forEach(network -> { + rodaParameters.getForcedPreventiveActions().stream().filter(action -> !action.toModification().apply(network, true)) + .forEach(action -> OpenRaoLoggerProvider.BUSINESS_WARNS.warn(String.format("Action '%s' could not be applied.", action.getId()))); + rodaParameters.getForcedPreventiveActions().forEach(action -> action.toModification().apply(network, false)); + }); + } + private CompletableFuture runSingleTsRao(TimeCoupledRaoInput raoInputs, RaoParameters raoParameters) { OffsetDateTime ts = raoInputs.getRaoInputs().getTimestamps().getFirst(); RaoInput raoInput = raoInputs.getRaoInputs().getData(ts).orElseThrow(); diff --git a/ra-optimisation/roda/src/main/java/com/powsybl/openrao/roda/parameters/JsonRodaParameters.java b/ra-optimisation/roda/src/main/java/com/powsybl/openrao/roda/parameters/JsonRodaParameters.java new file mode 100644 index 0000000000..e44a3c7f76 --- /dev/null +++ b/ra-optimisation/roda/src/main/java/com/powsybl/openrao/roda/parameters/JsonRodaParameters.java @@ -0,0 +1,80 @@ +/* + * 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/. + */ + +package com.powsybl.openrao.roda.parameters; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.google.auto.service.AutoService; +import com.powsybl.action.ActionList; +import com.powsybl.action.json.ActionJsonModule; +import com.powsybl.commons.json.JsonUtil; +import com.powsybl.openrao.commons.OpenRaoException; +import com.powsybl.openrao.raoapi.json.JsonRaoParameters; + +import java.io.IOException; +import java.util.List; + +/** + * RODA parameters extension json serializer & deserializer. + * Depends on PowSyBl's ActionList serializer & deserializer. + * + * @author Peter Mitri {@literal } + */ +@AutoService(JsonRaoParameters.ExtensionSerializer.class) +public class JsonRodaParameters implements JsonRaoParameters.ExtensionSerializer { + + private static final String PREVENTIVE_ACTION_LIST = "forced-preventive-actions-list"; + + @Override + public void serialize(RodaParameters rodaParameters, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { + jsonGenerator.writeStartObject(); + jsonGenerator.writeFieldName(PREVENTIVE_ACTION_LIST); + createObjectMapper().writeValue(jsonGenerator, new ActionList(rodaParameters.getForcedPreventiveActions())); + jsonGenerator.writeEndObject(); + } + + private static ObjectMapper createObjectMapper() { + return JsonUtil.createObjectMapper() + .registerModule(new ActionJsonModule()); + } + + @Override + public RodaParameters deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { + ActionList actionList = null; + while (!jsonParser.nextToken().isStructEnd()) { + if (jsonParser.currentName().equals(PREVENTIVE_ACTION_LIST)) { + jsonParser.nextToken(); + actionList = createObjectMapper().readValue(jsonParser, ActionList.class); + } else { + throw new OpenRaoException("Unexpected token: " + jsonParser.currentName()); + } + } + if (actionList == null) { + return new RodaParameters(List.of()); + } + return new RodaParameters(actionList.getActions()); + } + + @Override + public String getExtensionName() { + return RodaParameters.EXTENSION_NAME; + } + + @Override + public String getCategoryName() { + return "rao-parameters"; + } + + @Override + public Class getExtensionClass() { + return RodaParameters.class; + } +} diff --git a/ra-optimisation/roda/src/main/java/com/powsybl/openrao/roda/parameters/RodaParameters.java b/ra-optimisation/roda/src/main/java/com/powsybl/openrao/roda/parameters/RodaParameters.java new file mode 100644 index 0000000000..716b5b5fb3 --- /dev/null +++ b/ra-optimisation/roda/src/main/java/com/powsybl/openrao/roda/parameters/RodaParameters.java @@ -0,0 +1,41 @@ +/* + * 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/. + */ + +package com.powsybl.openrao.roda.parameters; + +import com.powsybl.action.Action; +import com.powsybl.commons.extensions.AbstractExtension; +import com.powsybl.openrao.raoapi.parameters.RaoParameters; + +import java.util.Collections; +import java.util.List; + +/** + * RODA specific RAO parameters. + * For now, only allows forcing actions on the network before running RAO. + * Could be useful to avoid pre-processing network multiple times (for example when testing topological changes + * in an outside loop). + * + * @author Peter Mitri {@literal } + */ +public class RodaParameters extends AbstractExtension { + public static final String EXTENSION_NAME = "roda-parameters"; + private final List forcedPreventiveActions; + + public RodaParameters(List forcedPreventiveActions) { + this.forcedPreventiveActions = Collections.unmodifiableList(forcedPreventiveActions); + } + + @Override + public String getName() { + return EXTENSION_NAME; + } + + public List getForcedPreventiveActions() { + return forcedPreventiveActions; + } +} diff --git a/ra-optimisation/roda/src/test/java/com/powsybl/openrao/roda/RodaTest.java b/ra-optimisation/roda/src/test/java/com/powsybl/openrao/roda/RodaTest.java new file mode 100644 index 0000000000..26cbebad31 --- /dev/null +++ b/ra-optimisation/roda/src/test/java/com/powsybl/openrao/roda/RodaTest.java @@ -0,0 +1,76 @@ +/* + * 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/. + */ + +package com.powsybl.openrao.roda; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; +import com.powsybl.action.Action; +import com.powsybl.action.PhaseTapChangerTapPositionAction; +import com.powsybl.action.TerminalsConnectionAction; +import com.powsybl.iidm.network.Network; +import com.powsybl.openrao.commons.TemporalData; +import com.powsybl.openrao.commons.TemporalDataImpl; +import com.powsybl.openrao.commons.logs.RaoBusinessWarns; +import com.powsybl.openrao.data.crac.api.Crac; +import com.powsybl.openrao.data.crac.impl.utils.CommonCracCreation; +import com.powsybl.openrao.data.crac.impl.utils.NetworkImportsUtil; +import com.powsybl.openrao.raoapi.RaoInput; +import com.powsybl.openrao.roda.parameters.RodaParameters; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; + +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * @author Peter Mitri {@literal } + */ +class RodaTest { + Network network; + TemporalData raoInput; + + @BeforeEach + void setUp() { + network = NetworkImportsUtil.import12NodesNetwork(); + Crac crac = CommonCracCreation.createWithPreventivePstRange(); + String variantId = network.getVariantManager().getWorkingVariantId(); + raoInput = new TemporalDataImpl<>( + Map.of(OffsetDateTime.now(), RaoInput.buildWithPreventiveState(network, crac) + .withNetworkVariantId(variantId) + .build())); + } + + @Test + void testApplyForcedActions() { + ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(RaoBusinessWarns.class); + ListAppender listAppender = new ListAppender<>(); + listAppender.start(); + logger.addAppender(listAppender); + List logsList = listAppender.list; + + Action action1 = new PhaseTapChangerTapPositionAction("action1", "BBE2AA1 BBE3AA1 1", false, -8); + Action action2 = new TerminalsConnectionAction("action2", "FFR1AA1 FFR2AA1 1", true); + Action action3 = new TerminalsConnectionAction("wrong_action", "wrong_id", true); + RodaParameters rodaParameters = new RodaParameters(List.of(action1, action2, action3)); + Roda.applyForcedActions(raoInput, rodaParameters); + assertEquals(-8, network.getTwoWindingsTransformer("BBE2AA1 BBE3AA1 1").getPhaseTapChanger().getTapPosition()); + assertFalse(network.getLine("FFR1AA1 FFR2AA1 1").getTerminal1().isConnected()); + assertFalse(network.getLine("FFR1AA1 FFR2AA1 1").getTerminal2().isConnected()); + assertTrue(logsList.stream().anyMatch(e -> e.getMessage().contains("Action 'wrong_action' could not be applied."))); + } + + @Test + void testApplyForcedActionsNullOrEmpty() { + assertDoesNotThrow(() -> Roda.applyForcedActions(raoInput, null)); + assertDoesNotThrow(() -> Roda.applyForcedActions(raoInput, new RodaParameters(List.of()))); + } +} diff --git a/ra-optimisation/roda/src/test/java/com/powsybl/openrao/roda/parameters/JsonRodaParametersTest.java b/ra-optimisation/roda/src/test/java/com/powsybl/openrao/roda/parameters/JsonRodaParametersTest.java new file mode 100644 index 0000000000..bc8b856c68 --- /dev/null +++ b/ra-optimisation/roda/src/test/java/com/powsybl/openrao/roda/parameters/JsonRodaParametersTest.java @@ -0,0 +1,121 @@ +/* + * 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/. + */ +package com.powsybl.openrao.roda.parameters; + +import com.powsybl.action.*; +import com.powsybl.commons.test.AbstractSerDeTest; +import com.powsybl.iidm.network.PhaseTapChanger; +import com.powsybl.iidm.network.StaticVarCompensator; +import com.powsybl.iidm.network.ThreeSides; +import com.powsybl.openrao.commons.OpenRaoException; +import com.powsybl.openrao.raoapi.json.JsonRaoParameters; +import com.powsybl.openrao.raoapi.parameters.RaoParameters; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.*; + +import static com.powsybl.action.PercentChangeLoadAction.QModificationStrategy.CONSTANT_Q; +import static com.powsybl.iidm.network.HvdcLine.ConvertersMode.SIDE_1_RECTIFIER_SIDE_2_INVERTER; +import static org.junit.jupiter.api.Assertions.*; + +/** + * @author Peter Mitri {@literal } + */ +class JsonRodaParametersTest extends AbstractSerDeTest { + @Test + void testForcedActionsRoundTrip() throws IOException { + List actions = new ArrayList<>(); + actions.add(new SwitchAction("id1", "switchId1", true)); + actions.add(new MultipleActionsAction("id2", Collections.singletonList(new SwitchAction("id3", "switchId2", true)))); + actions.add(new TerminalsConnectionAction("id3", "lineId3", true)); // both sides. + actions.add(new TerminalsConnectionAction("id4", "lineId4", false)); // both sides. + actions.add(new PhaseTapChangerTapPositionAction("id5", "transformerId1", true, 5, ThreeSides.TWO)); + actions.add(new PhaseTapChangerTapPositionAction("id6", "transformerId2", false, 12)); + actions.add(new PhaseTapChangerTapPositionAction("id7", "transformerId3", true, -5, ThreeSides.ONE)); + actions.add(new PhaseTapChangerTapPositionAction("id8", "transformerId3", false, 2, ThreeSides.THREE)); + actions.add(new GeneratorActionBuilder().withId("id9").withGeneratorId("generatorId1").withActivePowerRelativeValue(true).withActivePowerValue(100.0).build()); + actions.add(new GeneratorActionBuilder().withId("id10").withGeneratorId("generatorId2").withVoltageRegulatorOn(true).withTargetV(225.0).build()); + actions.add(new GeneratorActionBuilder().withId("id11").withGeneratorId("generatorId2").withVoltageRegulatorOn(false).withTargetQ(400.0).build()); + actions.add(new LoadActionBuilder().withId("id12").withLoadId("loadId1").withRelativeValue(false).withActivePowerValue(50.0).build()); + actions.add(new LoadActionBuilder().withId("id13").withLoadId("loadId1").withRelativeValue(true).withReactivePowerValue(5.0).build()); + actions.add(new PercentChangeLoadActionBuilder().withId("id26").withLoadId("loadId1").withP0PercentChange(5.0).withQModificationStrategy(CONSTANT_Q).build()); + actions.add(new BoundaryLineActionBuilder().withId("id17").withBoundaryLineId("dlId1").withRelativeValue(true).withReactivePowerValue(5.0).build()); + actions.add(new RatioTapChangerTapPositionAction("id14", "transformerId4", false, 2, ThreeSides.THREE)); + actions.add(new RatioTapChangerTapPositionAction("id15", "transformerId5", true, 1)); + actions.add(RatioTapChangerRegulationAction.activateRegulation("id16", "transformerId5", ThreeSides.THREE)); + actions.add(PhaseTapChangerRegulationAction.activateAndChangeRegulationMode("id17", "transformerId5", ThreeSides.ONE, + PhaseTapChanger.RegulationMode.ACTIVE_POWER_CONTROL, 10.0)); + actions.add(PhaseTapChangerRegulationAction.deactivateRegulation("id18", + "transformerId6", ThreeSides.ONE)); + actions.add(PhaseTapChangerRegulationAction.activateAndChangeRegulationMode("id19", + "transformerId6", ThreeSides.ONE, + PhaseTapChanger.RegulationMode.ACTIVE_POWER_CONTROL, 15.0)); + actions.add(RatioTapChangerRegulationAction.activateRegulationAndChangeTargetV("id20", "transformerId5", 90.0)); + actions.add(RatioTapChangerRegulationAction.deactivateRegulation("id21", "transformerId5", ThreeSides.THREE)); + actions.add(new HvdcActionBuilder() + .withId("id22") + .withHvdcId("hvdc1") + .withAcEmulationEnabled(false) + .build()); + actions.add(new HvdcActionBuilder() + .withId("id23") + .withHvdcId("hvdc2") + .withAcEmulationEnabled(true) + .build()); + actions.add(new HvdcActionBuilder() + .withId("id24") + .withHvdcId("hvdc2") + .withAcEmulationEnabled(true) + .withDroop(121.0) + .withP0(42.0) + .withConverterMode(SIDE_1_RECTIFIER_SIDE_2_INVERTER) + .withRelativeValue(false) + .build()); + actions.add(new HvdcActionBuilder() + .withId("id25") + .withHvdcId("hvdc1") + .withAcEmulationEnabled(false) + .withActivePowerSetpoint(12.0) + .withRelativeValue(true) + .build()); + actions.add(new ShuntCompensatorPositionActionBuilder().withId("id22").withShuntCompensatorId("shuntId1").withSectionCount(5).build()); + actions.add(new StaticVarCompensatorActionBuilder().withId("id23") + .withStaticVarCompensatorId("svc").withRegulationMode(StaticVarCompensator.RegulationMode.VOLTAGE) + .withVoltageSetpoint(56.0).build()); + actions.add(new StaticVarCompensatorActionBuilder().withId("id24") + .withStaticVarCompensatorId("svc").withRegulationMode(StaticVarCompensator.RegulationMode.REACTIVE_POWER) + .withReactivePowerSetpoint(120.0).build()); + actions.add(new TerminalsConnectionAction("id4", "transformerId25", ThreeSides.THREE, true)); // only one side. + actions.add(new AreaInterchangeTargetAction("id99", "AreaA", 101.0)); + actions.add(new AreaInterchangeTargetAction("idDisabledTarget", "AreaA", Double.NaN)); + + RaoParameters raoParameters = new RaoParameters(); + RodaParameters forcedActions = new RodaParameters(actions); + raoParameters.addExtension(RodaParameters.class, forcedActions); + + roundTripTest(raoParameters, JsonRaoParameters::write, JsonRaoParameters::read, "/RaoParameters_with_ForcedActions.json"); + } + + @Test + void testWrongForcedActions() { + try (var stream = getClass().getResourceAsStream("/RaoParameters_with_wrong_ForcedActions.json")) { + OpenRaoException exception = assertThrows(OpenRaoException.class, () -> JsonRaoParameters.read(stream)); + assertEquals("Unexpected token: wrong-key", exception.getMessage()); + } catch (IOException e) { + fail(e); + } + } + + @Test + void testEmptyForcedActions() { + RaoParameters raoParameters = JsonRaoParameters.read(getClass().getResourceAsStream("/RaoParameters_with_empty_ForcedActions.json")); + RodaParameters forcedActions = raoParameters.getExtension(RodaParameters.class); + assertNotNull(forcedActions); + assertTrue(forcedActions.getForcedPreventiveActions().isEmpty()); + } +} diff --git a/ra-optimisation/roda/src/test/resources/RaoParameters_with_ForcedActions.json b/ra-optimisation/roda/src/test/resources/RaoParameters_with_ForcedActions.json new file mode 100644 index 0000000000..66e945deac --- /dev/null +++ b/ra-optimisation/roda/src/test/resources/RaoParameters_with_ForcedActions.json @@ -0,0 +1,231 @@ +{ + "version" : "3.3", + "objective-function" : { + "type" : "SECURE_FLOW", + "enforce-curative-security" : false + }, + "range-actions-optimization" : { + "pst-ra-min-impact-threshold" : 0.01, + "hvdc-ra-min-impact-threshold" : 0.001, + "injection-ra-min-impact-threshold" : 0.001 + }, + "topological-actions-optimization" : { + "relative-minimum-impact-threshold" : 0.0, + "absolute-minimum-impact-threshold" : 0.0 + }, + "not-optimized-cnecs" : { + "do-not-optimize-curative-cnecs-for-tsos-without-cras" : false + }, + "extensions" : { + "roda-parameters" : { + "forced-preventive-actions-list" : { + "version" : "1.3", + "actions" : [ { + "type" : "SWITCH", + "id" : "id1", + "switchId" : "switchId1", + "open" : true + }, { + "type" : "MULTIPLE_ACTIONS", + "id" : "id2", + "actions" : [ { + "type" : "SWITCH", + "id" : "id3", + "switchId" : "switchId2", + "open" : true + } ] + }, { + "type" : "TERMINALS_CONNECTION", + "id" : "id3", + "elementId" : "lineId3", + "open" : true + }, { + "type" : "TERMINALS_CONNECTION", + "id" : "id4", + "elementId" : "lineId4", + "open" : false + }, { + "type" : "PHASE_TAP_CHANGER_TAP_POSITION", + "id" : "id5", + "transformerId" : "transformerId1", + "tapPosition" : 5, + "relativeValue" : true, + "side" : "TWO" + }, { + "type" : "PHASE_TAP_CHANGER_TAP_POSITION", + "id" : "id6", + "transformerId" : "transformerId2", + "tapPosition" : 12, + "relativeValue" : false + }, { + "type" : "PHASE_TAP_CHANGER_TAP_POSITION", + "id" : "id7", + "transformerId" : "transformerId3", + "tapPosition" : -5, + "relativeValue" : true, + "side" : "ONE" + }, { + "type" : "PHASE_TAP_CHANGER_TAP_POSITION", + "id" : "id8", + "transformerId" : "transformerId3", + "tapPosition" : 2, + "relativeValue" : false, + "side" : "THREE" + }, { + "type" : "GENERATOR", + "id" : "id9", + "generatorId" : "generatorId1", + "activePowerRelativeValue" : true, + "activePowerValue" : 100.0 + }, { + "type" : "GENERATOR", + "id" : "id10", + "generatorId" : "generatorId2", + "voltageRegulatorOn" : true, + "targetV" : 225.0 + }, { + "type" : "GENERATOR", + "id" : "id11", + "generatorId" : "generatorId2", + "voltageRegulatorOn" : false, + "targetQ" : 400.0 + }, { + "type" : "LOAD", + "id" : "id12", + "loadId" : "loadId1", + "relativeValue" : false, + "activePowerValue" : 50.0 + }, { + "type" : "LOAD", + "id" : "id13", + "loadId" : "loadId1", + "relativeValue" : true, + "reactivePowerValue" : 5.0 + }, { + "type" : "PCT_LOAD_CHANGE", + "id" : "id26", + "loadId" : "loadId1", + "p0PercentChange" : 5.0, + "qModificationStrategy" : "CONSTANT_Q" + }, { + "type" : "BOUNDARY_LINE", + "id" : "id17", + "boundaryLineId" : "dlId1", + "relativeValue" : true, + "reactivePowerValue" : 5.0 + }, { + "type" : "RATIO_TAP_CHANGER_TAP_POSITION", + "id" : "id14", + "transformerId" : "transformerId4", + "tapPosition" : 2, + "relativeValue" : false, + "side" : "THREE" + }, { + "type" : "RATIO_TAP_CHANGER_TAP_POSITION", + "id" : "id15", + "transformerId" : "transformerId5", + "tapPosition" : 1, + "relativeValue" : true + }, { + "type" : "RATIO_TAP_CHANGER_REGULATION", + "id" : "id16", + "transformerId" : "transformerId5", + "regulating" : true, + "side" : "THREE" + }, { + "type" : "PHASE_TAP_CHANGER_REGULATION", + "id" : "id17", + "transformerId" : "transformerId5", + "regulating" : true, + "side" : "ONE", + "regulationMode" : "ACTIVE_POWER_CONTROL", + "regulationValue" : 10.0 + }, { + "type" : "PHASE_TAP_CHANGER_REGULATION", + "id" : "id18", + "transformerId" : "transformerId6", + "regulating" : false, + "side" : "ONE" + }, { + "type" : "PHASE_TAP_CHANGER_REGULATION", + "id" : "id19", + "transformerId" : "transformerId6", + "regulating" : true, + "side" : "ONE", + "regulationMode" : "ACTIVE_POWER_CONTROL", + "regulationValue" : 15.0 + }, { + "type" : "RATIO_TAP_CHANGER_REGULATION", + "id" : "id20", + "transformerId" : "transformerId5", + "regulating" : true, + "targetV" : 90.0 + }, { + "type" : "RATIO_TAP_CHANGER_REGULATION", + "id" : "id21", + "transformerId" : "transformerId5", + "regulating" : false, + "side" : "THREE" + }, { + "type" : "HVDC", + "id" : "id22", + "hvdcId" : "hvdc1", + "acEmulationEnabled" : false + }, { + "type" : "HVDC", + "id" : "id23", + "hvdcId" : "hvdc2", + "acEmulationEnabled" : true + }, { + "type" : "HVDC", + "id" : "id24", + "hvdcId" : "hvdc2", + "acEmulationEnabled" : true, + "converterMode" : "SIDE_1_RECTIFIER_SIDE_2_INVERTER", + "droop" : 121.0, + "p0" : 42.0, + "relativeValue" : false + }, { + "type" : "HVDC", + "id" : "id25", + "hvdcId" : "hvdc1", + "acEmulationEnabled" : false, + "activePowerSetpoint" : 12.0, + "relativeValue" : true + }, { + "type" : "SHUNT_COMPENSATOR_POSITION", + "id" : "id22", + "shuntCompensatorId" : "shuntId1", + "sectionCount" : 5 + }, { + "type" : "STATIC_VAR_COMPENSATOR", + "id" : "id23", + "staticVarCompensatorId" : "svc", + "regulationMode" : "VOLTAGE", + "voltageSetpoint" : 56.0 + }, { + "type" : "STATIC_VAR_COMPENSATOR", + "id" : "id24", + "staticVarCompensatorId" : "svc", + "regulationMode" : "REACTIVE_POWER", + "reactivePowerSetpoint" : 120.0 + }, { + "type" : "TERMINALS_CONNECTION", + "id" : "id4", + "elementId" : "transformerId25", + "side" : "THREE", + "open" : true + }, { + "type" : "AREA_INTERCHANGE_TARGET_ACTION", + "id" : "id99", + "areaId" : "AreaA", + "interchangeTarget" : 101.0 + }, { + "type" : "AREA_INTERCHANGE_TARGET_ACTION", + "id" : "idDisabledTarget", + "areaId" : "AreaA" + } ] + } + } + } +} \ No newline at end of file diff --git a/ra-optimisation/roda/src/test/resources/RaoParameters_with_empty_ForcedActions.json b/ra-optimisation/roda/src/test/resources/RaoParameters_with_empty_ForcedActions.json new file mode 100644 index 0000000000..57d1807b1c --- /dev/null +++ b/ra-optimisation/roda/src/test/resources/RaoParameters_with_empty_ForcedActions.json @@ -0,0 +1,23 @@ +{ + "version" : "3.3", + "objective-function" : { + "type" : "SECURE_FLOW", + "enforce-curative-security" : false + }, + "range-actions-optimization" : { + "pst-ra-min-impact-threshold" : 0.01, + "hvdc-ra-min-impact-threshold" : 0.001, + "injection-ra-min-impact-threshold" : 0.001 + }, + "topological-actions-optimization" : { + "relative-minimum-impact-threshold" : 0.0, + "absolute-minimum-impact-threshold" : 0.0 + }, + "not-optimized-cnecs" : { + "do-not-optimize-curative-cnecs-for-tsos-without-cras" : false + }, + "extensions" : { + "roda-parameters" : { + } + } +} \ No newline at end of file diff --git a/ra-optimisation/roda/src/test/resources/RaoParameters_with_wrong_ForcedActions.json b/ra-optimisation/roda/src/test/resources/RaoParameters_with_wrong_ForcedActions.json new file mode 100644 index 0000000000..ad481a2228 --- /dev/null +++ b/ra-optimisation/roda/src/test/resources/RaoParameters_with_wrong_ForcedActions.json @@ -0,0 +1,24 @@ +{ + "version" : "3.3", + "objective-function" : { + "type" : "SECURE_FLOW", + "enforce-curative-security" : false + }, + "range-actions-optimization" : { + "pst-ra-min-impact-threshold" : 0.01, + "hvdc-ra-min-impact-threshold" : 0.001, + "injection-ra-min-impact-threshold" : 0.001 + }, + "topological-actions-optimization" : { + "relative-minimum-impact-threshold" : 0.0, + "absolute-minimum-impact-threshold" : 0.0 + }, + "not-optimized-cnecs" : { + "do-not-optimize-curative-cnecs-for-tsos-without-cras" : false + }, + "extensions" : { + "roda-parameters" : { + "wrong-key" : "something" + } + } +} \ No newline at end of file diff --git a/tests/pom.xml b/tests/pom.xml index a3946eb8bb..12323c4f25 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -209,6 +209,12 @@ ${project.version} test + + ${project.groupId} + open-rao-roda + ${project.version} + test + ${project.groupId} open-rao-swe-cne-exporter diff --git a/tests/src/test/java/com/powsybl/openrao/tests/steps/TimeCoupledRaoSteps.java b/tests/src/test/java/com/powsybl/openrao/tests/steps/TimeCoupledRaoSteps.java index 635954d176..61bc9faa34 100644 --- a/tests/src/test/java/com/powsybl/openrao/tests/steps/TimeCoupledRaoSteps.java +++ b/tests/src/test/java/com/powsybl/openrao/tests/steps/TimeCoupledRaoSteps.java @@ -35,10 +35,7 @@ import com.powsybl.openrao.data.refprog.refprogxmlimporter.TimeCoupledRefProg; import com.powsybl.openrao.data.timecoupledconstraints.TimeCoupledConstraints; import com.powsybl.openrao.data.timecoupledconstraints.io.JsonTimeCoupledConstraints; -import com.powsybl.openrao.raoapi.LazyNetwork; -import com.powsybl.openrao.raoapi.RaoInput; -import com.powsybl.openrao.raoapi.TimeCoupledRao; -import com.powsybl.openrao.raoapi.TimeCoupledRaoInput; +import com.powsybl.openrao.raoapi.*; import com.powsybl.openrao.tests.utils.CoreCcPreprocessor; import io.cucumber.datatable.DataTable; import io.cucumber.java.After; @@ -270,7 +267,12 @@ public static void loadDataForCoreTimeCoupledRao(DataTable arg1) throws IOExcept @When("I launch marmot") public static void iLaunchMarmot() { - timeCoupledRaoResult = TimeCoupledRao.run(timeCoupledRaoInput, getRaoParameters()); + timeCoupledRaoResult = TimeCoupledRao.find("TimeCoupledRao").run(timeCoupledRaoInput, getRaoParameters()); + } + + @When("I launch roda") + public static void iLaunchRoda() { + timeCoupledRaoResult = TimeCoupledRao.find("Roda").run(timeCoupledRaoInput, getRaoParameters()); } @When("I export marmot results to {string}") @@ -526,13 +528,22 @@ private static void applyRedispatchingAction(InjectionRangeAction injectionRange } } + @Then("the initial margin on {string} for timestamp {string} is {double} MW") + public static void theInitialMarginOnCnecForTimestampIsMW(String cnecId, String timestamp, double margin) { + checkMarginInMw(cnecId, timestamp, margin, null); + } + @Then("the optimized margin on {string} for timestamp {string} is {double} MW") public static void theOptimizedMarginOnCnecForTimestampIsMW(String cnecId, String timestamp, double margin) { + Instant afterCra = timeCoupledRaoInput.getRaoInputs().getData(getOffsetDateTimeFromBrusselsTimestamp(timestamp)).orElseThrow().getCrac().getLastInstant(); + checkMarginInMw(cnecId, timestamp, margin, afterCra); + } + + private static void checkMarginInMw(String cnecId, String timestamp, double margin, Instant optimizedInstant) { OffsetDateTime offsetDateTime = getOffsetDateTimeFromBrusselsTimestamp(timestamp); FlowCnec flowCnec = timeCoupledRaoInput.getRaoInputs().getData(offsetDateTime).orElseThrow().getCrac().getFlowCnec(cnecId); - Instant afterCra = timeCoupledRaoInput.getRaoInputs().getData(offsetDateTime).orElseThrow().getCrac().getLastInstant(); assertEquals(margin, - timeCoupledRaoResult.getIndividualRaoResult(offsetDateTime).getMargin(afterCra, flowCnec, Unit.MEGAWATT), + timeCoupledRaoResult.getIndividualRaoResult(offsetDateTime).getMargin(optimizedInstant, flowCnec, Unit.MEGAWATT), RaoSteps.TOLERANCE_FLOW_IN_MEGAWATT); } diff --git a/tests/src/test/resources/com/powsybl/openrao/tests/features/7_roda/forced_actions.feature b/tests/src/test/resources/com/powsybl/openrao/tests/features/7_roda/forced_actions.feature new file mode 100644 index 0000000000..f05d63a72b --- /dev/null +++ b/tests/src/test/resources/com/powsybl/openrao/tests/features/7_roda/forced_actions.feature @@ -0,0 +1,29 @@ +# 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/. + +Feature: Forced actions in RODA + @fast @rao @dc @redispatching @forced-actions + Scenario: ForcedActions.1: A clone of test 4.1.2, with optimal topological PRA (Close Line NL2 BE3 2) of second TS forced in parameters + Given network files are in folder "epic93/TestCases_93_2_2" + Given crac file is "epic93/cbcora_93_2_2.xml" + Given ics static file is "epic93/static_93_2_1.csv" + Given ics series file is "epic93/series_93_2_1.csv" + Given configuration file is "roda/conf_force_actions.json" + Given time-coupled rao inputs for CORE are: + | Timestamp | Network | + | 2019-01-08 00:30 | 12Nodes_0030.uct | + | 2019-01-08 01:30 | 12Nodes_0130.uct | + When I launch roda + When I export marmot results to "raoresults/ForcedActions.zip" + Then the initial margin on "NNL2AA1 BBE3AA1 1 - preventive" for timestamp "2019-01-08 00:30" is 523.04 MW + And the initial margin on "NNL2AA1 BBE3AA1 1 - preventive" for timestamp "2019-01-08 01:30" is 252.8 MW + And the optimized margin on "NNL2AA1 BBE3AA1 1 - preventive" for timestamp "2019-01-08 00:30" is 523.04 MW + And the optimized margin on "NNL2AA1 BBE3AA1 1 - preventive" for timestamp "2019-01-08 01:30" is 252.8 MW + And the functional cost for timestamp "2019-01-08 00:30" is 0 + And the functional cost for timestamp "2019-01-08 01:30" is 0 + And the functional cost for all timestamps is 0 + And the total cost for timestamp "2019-01-08 00:30" is 0 + And the total cost for timestamp "2019-01-08 01:30" is 0 + And the total cost for all timestamps is 0 \ No newline at end of file diff --git a/tests/src/test/resources/files/configurations/roda/conf_force_actions.json b/tests/src/test/resources/files/configurations/roda/conf_force_actions.json new file mode 100644 index 0000000000..ea0867adf1 --- /dev/null +++ b/tests/src/test/resources/files/configurations/roda/conf_force_actions.json @@ -0,0 +1,158 @@ +{ + "version" : "3.3", + "objective-function" : { + "type" : "MIN_COST" + }, + "range-actions-optimization" : { + "pst-ra-min-impact-threshold" : 0.1, + "hvdc-ra-min-impact-threshold" : 0.1, + "injection-ra-min-impact-threshold" : 0.1 + }, + "topological-actions-optimization" : { + "relative-minimum-impact-threshold" : 0.01, + "absolute-minimum-impact-threshold" : 100 + }, + "not-optimized-cnecs" : { + "do-not-optimize-curative-cnecs-for-tsos-without-cras" : false + }, + "extensions" : { + "open-rao-search-tree-parameters" : { + "objective-function" : { + "curative-min-obj-improvement" : 0.0 + }, + "range-actions-optimization" : { + "max-mip-iterations" : 2, + "pst-sensitivity-threshold" : 0.001, + "pst-model" : "APPROXIMATED_INTEGERS", + "hvdc-sensitivity-threshold" : 0.001, + "injection-ra-sensitivity-threshold" : 0.001, + "ra-range-shrinking" : "DISABLED", + "linear-optimization-solver" : { + "solver" : "CBC", + "relative-mip-gap" : 0.001, + "solver-specific-parameters" : null + } + }, + "topological-actions-optimization" : { + "max-preventive-search-tree-depth" : 2, + "max-curative-search-tree-depth" : 2, + "predefined-combinations" : [ ], + "skip-actions-far-from-most-limiting-element" : false, + "max-number-of-boundaries-for-skipping-actions" : 0 + }, + "second-preventive-rao" : { + "execution-condition" : "POSSIBLE_CURATIVE_IMPROVEMENT", + "hint-from-first-preventive-rao" : false + }, + "load-flow-and-sensitivity-computation" : { + "load-flow-provider" : "OpenLoadFlow", + "sensitivity-provider" : "OpenLoadFlow", + "sensitivity-failure-overcost" : 10000.0, + "sensitivity-parameters" : { + "version" : "1.0", + "load-flow-parameters" : { + "version" : "1.10", + "voltageInitMode" : "UNIFORM_VALUES", + "transformerVoltageControlOn" : false, + "phaseShifterRegulationOn" : false, + "useReactiveLimits" : true, + "twtSplitShuntAdmittance" : true, + "shuntCompensatorVoltageControlOn" : false, + "readSlackBus" : false, + "writeSlackBus" : true, + "dc" : true, + "distributedSlack" : true, + "balanceType" : "PROPORTIONAL_TO_GENERATION_P", + "dcUseTransformerRatio" : false, + "countriesToBalance" : [ "GR", "BE", "SK", "TR", "CH", "RS", "PL", "UA", "BG", "ES", "ME", "CZ", "HR", "AL", "RO", "HU", "AT", "FR", "PT", "DE", "MK", "BA", "SI", "IT", "NL" ], + "componentMode" : "MAIN_CONNECTED", + "hvdcAcEmulation" : true, + "dcPowerFactor" : 1.0, + "extensions" : { + "open-load-flow-parameters" : { + "slackBusSelectionMode" : "MOST_MESHED", + "slackBusesIds" : [ ], + "slackDistributionFailureBehavior" : "LEAVE_ON_SLACK_BUS", + "lowImpedanceBranchMode" : "REPLACE_BY_ZERO_IMPEDANCE_LINE", + "loadPowerFactorConstant" : false, + "plausibleActivePowerLimit" : 10000.0, + "newtonRaphsonStoppingCriteriaType" : "UNIFORM_CRITERIA", + "maxActivePowerMismatch" : 0.01, + "maxReactivePowerMismatch" : 0.01, + "maxVoltageMismatch" : 1.0E-4, + "maxAngleMismatch" : 1.0E-5, + "maxRatioMismatch" : 1.0E-5, + "maxSusceptanceMismatch" : 1.0E-4, + "slackBusPMaxMismatch" : 1.0, + "voltagePerReactivePowerControl" : false, + "maxNewtonRaphsonIterations" : 30, + "maxOuterLoopIterations" : 20, + "newtonRaphsonConvEpsPerEq" : 1.0E-4, + "voltageInitModeOverride" : "NONE", + "transformerVoltageControlMode" : "WITH_GENERATOR_VOLTAGE_CONTROL", + "shuntVoltageControlMode" : "WITH_GENERATOR_VOLTAGE_CONTROL", + "minPlausibleTargetVoltage" : 0.8, + "maxPlausibleTargetVoltage" : 1.2, + "minNominalVoltageTargetVoltageCheck" : 20.0, + "minRealisticVoltage" : 0.5, + "maxRealisticVoltage" : 1.5, + "lowImpedanceThreshold" : 1.0E-8, + "reactiveRangeCheckMode" : "MAX", + "networkCacheEnabled" : false, + "svcVoltageMonitoring" : true, + "stateVectorScalingMode" : "NONE", + "maxSlackBusCount" : 1, + "debugDir" : null, + "incrementalTransformerRatioTapControlOuterLoopMaxTapShift" : 3, + "secondaryVoltageControl" : false, + "reactiveLimitsMaxPqPvSwitch" : 3, + "phaseShifterControlMode" : "CONTINUOUS_WITH_DISCRETISATION", + "alwaysUpdateNetwork" : false, + "mostMeshedSlackBusSelectorMaxNominalVoltagePercentile" : 95.0, + "reportedFeatures" : [ ], + "slackBusCountryFilter" : [ ], + "actionableSwitchesIds" : [ ], + "asymmetrical" : false, + "reactivePowerDispatchMode" : "Q_EQUAL_PROPORTION", + "outerLoopNames" : null, + "useActiveLimits" : true, + "lineSearchStateVectorScalingMaxIteration" : 10, + "lineSearchStateVectorScalingStepFold" : 1.3333333333333333, + "maxVoltageChangeStateVectorScalingMaxDv" : 0.1, + "maxVoltageChangeStateVectorScalingMaxDphi" : 0.17453292519943295, + "linePerUnitMode" : "IMPEDANCE", + "useLoadModel" : false, + "dcApproximationType" : "IGNORE_R", + "simulateAutomationSystems" : false + } + } + } + } + }, + "multi-threading" : { + "available-cpus" : 14 + }, + "mnec-parameters" : { + "violation-cost" : 100000.0, + "constraint-adjustment-coefficient" : 3.0 + }, + "costly-min-margin-parameters" : { + "shifted-violation-threshold" : 10, + "shifted-violation-penalty" : 1000.0 + } + }, + "roda-parameters": { + "forced-preventive-actions-list": { + "version": "1.3", + "actions": [ + { + "type": "TERMINALS_CONNECTION", + "id": "Close Line NL2 BE3 2", + "elementId": "NNL2AA1 BBE3AA1 2", + "open": false + } + ] + } + } + } +} \ No newline at end of file