PowerGradientConstraintFillerTest.java

/*
 * 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/.
 */

package com.powsybl.openrao.searchtreerao.linearoptimisation.algorithms.fillers;

import com.powsybl.iidm.network.Network;
import com.powsybl.openrao.commons.OpenRaoException;
import com.powsybl.openrao.commons.TemporalData;
import com.powsybl.openrao.commons.TemporalDataImpl;
import com.powsybl.openrao.commons.Unit;
import com.powsybl.openrao.data.crac.api.Crac;
import com.powsybl.openrao.data.crac.api.State;
import com.powsybl.openrao.data.crac.api.rangeaction.InjectionRangeAction;
import com.powsybl.openrao.data.crac.api.rangeaction.RangeAction;
import com.powsybl.openrao.data.crac.api.usagerule.UsageMethod;
import com.powsybl.openrao.data.generatorconstraints.GeneratorConstraints;
import com.powsybl.openrao.data.raoresult.api.ComputationStatus;
import com.powsybl.openrao.raoapi.InterTemporalRaoInput;
import com.powsybl.openrao.raoapi.RaoInput;
import com.powsybl.openrao.raoapi.parameters.RangeActionsOptimizationParameters;
import com.powsybl.openrao.raoapi.parameters.RaoParameters;
import com.powsybl.openrao.raoapi.parameters.extensions.SearchTreeRaoRangeActionsOptimizationParameters;
import com.powsybl.openrao.searchtreerao.commons.optimizationperimeters.OptimizationPerimeter;
import com.powsybl.openrao.searchtreerao.commons.optimizationperimeters.PreventiveOptimizationPerimeter;
import com.powsybl.openrao.searchtreerao.linearoptimisation.algorithms.linearproblem.LinearProblem;
import com.powsybl.openrao.searchtreerao.linearoptimisation.algorithms.linearproblem.LinearProblemBuilder;
import com.powsybl.openrao.searchtreerao.linearoptimisation.algorithms.linearproblem.OpenRaoMPConstraint;
import com.powsybl.openrao.searchtreerao.result.api.FlowResult;
import com.powsybl.openrao.searchtreerao.result.api.RangeActionSetpointResult;
import com.powsybl.openrao.searchtreerao.result.api.SensitivityResult;
import com.powsybl.openrao.searchtreerao.result.impl.RangeActionSetpointResultImpl;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import java.io.IOException;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

/**
 * @author Roxane Chen {@literal <roxane.chen at rte-france.com}
 */
class PowerGradientConstraintFillerTest {
    private LinearProblemBuilder linearProblemBuilder = new LinearProblemBuilder().withSolver(SearchTreeRaoRangeActionsOptimizationParameters.Solver.SCIP);
    private LinearProblem linearProblem;
    private final OffsetDateTime timestamp1 = OffsetDateTime.of(2025, 1, 9, 16, 21, 0, 0, ZoneOffset.UTC);
    private final OffsetDateTime timestamp2 = OffsetDateTime.of(2025, 1, 9, 17, 21, 0, 0, ZoneOffset.UTC);
    private final OffsetDateTime timestamp3 = OffsetDateTime.of(2025, 1, 9, 19, 21, 0, 0, ZoneOffset.UTC);
    InterTemporalRaoInput input;
    RaoParameters parameters;

    public void createThreeTSInput() throws IOException {
        Network network1 = Network.read("4Nodes.uct", PowerGradientConstraintFillerTest.class.getResourceAsStream("/network/4Nodes.uct"));
        Network network2 = Network.read("4Nodes.uct", PowerGradientConstraintFillerTest.class.getResourceAsStream("/network/4Nodes.uct"));
        Network network3 = Network.read("4Nodes.uct", PowerGradientConstraintFillerTest.class.getResourceAsStream("/network/4Nodes.uct"));

        Crac crac1 = Crac.read("crac-1600.json", PowerGradientConstraintFillerTest.class.getResourceAsStream("/crac/crac-1600.json"), network1);
        Crac crac2 = Crac.read("crac-1700.json", PowerGradientConstraintFillerTest.class.getResourceAsStream("/crac/crac-1700.json"), network2);
        Crac crac3 = Crac.read("crac-1900.json", PowerGradientConstraintFillerTest.class.getResourceAsStream("/crac/crac-1900.json"), network3);

        RaoInput raoInput1 = RaoInput.build(network1, crac1).build();
        RaoInput raoInput2 = RaoInput.build(network2, crac2).build();
        RaoInput raoInput3 = RaoInput.build(network3, crac3).build();

        GeneratorConstraints generatorConstraintsFr1 = GeneratorConstraints.create().withGeneratorId("FFR1AA1 _load").withLeadTime(0.0).withLagTime(0.0).withPMin(0.0).withPMax(1000.0).withUpwardPowerGradient(500.0).withDownwardPowerGradient(-300.0).build();
        GeneratorConstraints generatorConstraintsFr2 = GeneratorConstraints.create().withGeneratorId("FFR2AA1 _generator").withLeadTime(0.0).withLagTime(0.0).withPMin(0.0).withPMax(1000.0).withUpwardPowerGradient(200.0).withDownwardPowerGradient(-100.0).build();
        GeneratorConstraints generatorConstraintsFr3 = GeneratorConstraints.create().withGeneratorId("FFR3AA1 _load").withLeadTime(0.0).withLagTime(0.0).withPMin(0.0).withPMax(1000.0).withUpwardPowerGradient(40.0).withDownwardPowerGradient(-150.0).build();

        input = new InterTemporalRaoInput(new TemporalDataImpl<>(Map.of(timestamp1, raoInput1, timestamp2, raoInput2, timestamp3, raoInput3)), Set.of(generatorConstraintsFr1, generatorConstraintsFr2, generatorConstraintsFr3));

        parameters = new RaoParameters();
    }

    private void createOneTSInput() throws IOException {
        Network network1 = Network.read("4Nodes.uct", PowerGradientConstraintFillerTest.class.getResourceAsStream("/network/4Nodes.uct"));
        Crac crac1 = Crac.read("crac-1600.json", PowerGradientConstraintFillerTest.class.getResourceAsStream("/crac/crac-1600.json"), network1);
        RaoInput raoInput1 = RaoInput.build(network1, crac1).build();

        GeneratorConstraints generatorConstraintsFr1 = GeneratorConstraints.create().withGeneratorId("FFR1AA1 _load").withLeadTime(0.0).withLagTime(0.0).withPMin(0.0).withPMax(1000.0).withUpwardPowerGradient(500.0).withDownwardPowerGradient(-300.0).build();
        GeneratorConstraints generatorConstraintsFr2 = GeneratorConstraints.create().withGeneratorId("FFR2AA1 _generator").withLeadTime(0.0).withLagTime(0.0).withPMin(0.0).withPMax(1000.0).withUpwardPowerGradient(200.0).withDownwardPowerGradient(-100.0).build();
        GeneratorConstraints generatorConstraintsFr3 = GeneratorConstraints.create().withGeneratorId("FFR3AA1 _load").withLeadTime(0.0).withLagTime(0.0).withPMin(0.0).withPMax(1000.0).withUpwardPowerGradient(40.0).withDownwardPowerGradient(-150.0).build();

        input = new InterTemporalRaoInput(new TemporalDataImpl<>(Map.of(timestamp1, raoInput1)), Set.of(generatorConstraintsFr1, generatorConstraintsFr2, generatorConstraintsFr3));
        parameters = new RaoParameters();
    }

    private void createCoreProblemFillers() {
        input.getRaoInputs().getDataPerTimestamp().entrySet().forEach(entry -> {
            Crac crac = entry.getValue().getCrac();
            OptimizationPerimeter optimizationPerimeter = new PreventiveOptimizationPerimeter(
                crac.getPreventiveState(),
                crac.getFlowCnecs(),
                Set.of(),
                crac.getNetworkActions(crac.getPreventiveState()),
                crac.getRangeActions(crac.getPreventiveState(), UsageMethod.AVAILABLE)
            );

            RangeActionsOptimizationParameters rangeActionParameters = parameters.getRangeActionsOptimizationParameters();
            Map<RangeAction<?>, Double> map = new HashMap<>();
            crac.getRangeActions(crac.getPreventiveState(), UsageMethod.AVAILABLE).forEach(action -> map.put(action, 0.0));
            RangeActionSetpointResult rangeActionSetpointResult = new RangeActionSetpointResultImpl(map);
            MarginCoreProblemFiller coreProblemFiller = new MarginCoreProblemFiller(
                optimizationPerimeter,
                rangeActionSetpointResult,
                rangeActionParameters,
                null,
                Unit.MEGAWATT,
                false,
                SearchTreeRaoRangeActionsOptimizationParameters.PstModel.CONTINUOUS,
                entry.getKey()
            );
            linearProblemBuilder.withProblemFiller(coreProblemFiller);
        });
    }

    private void createPowerGradientConstraintFiller() {
        TemporalData<State> preventiveStates = input.getRaoInputs().map(RaoInput::getCrac).map(crac -> crac.getPreventiveState()).map(State.class::cast);
        TemporalData<Network> networks = input.getRaoInputs().map(RaoInput::getNetwork).map(Network.class::cast);
        TemporalData<Set<InjectionRangeAction>> injectionRangeActions = input.getRaoInputs().map(RaoInput::getCrac).map(crac -> crac.getRangeActions(crac.getPreventiveState(), UsageMethod.AVAILABLE).stream().filter(InjectionRangeAction.class::isInstance).map(InjectionRangeAction.class::cast).collect(Collectors.toSet()));
        Set<GeneratorConstraints> generatorConstraints = input.getGeneratorConstraints();
        PowerGradientConstraintFiller powerGradientConstraintFiller = new PowerGradientConstraintFiller(
            preventiveStates,
            networks,
            injectionRangeActions,
            generatorConstraints);
        linearProblemBuilder.withProblemFiller(powerGradientConstraintFiller);
    }

    private void buildAndFillLinearProblem() {
        FlowResult flowResult = Mockito.mock(FlowResult.class);
        when(flowResult.getFlow(any(), any(), any())).thenReturn(10.0);
        SensitivityResult sensitivityResult = Mockito.mock(SensitivityResult.class);
        when(sensitivityResult.getSensitivityStatus(any())).thenReturn(ComputationStatus.DEFAULT);
        linearProblem = linearProblemBuilder.build();
        linearProblem.fill(flowResult, sensitivityResult);
    }

    private void setUpLinearProblem() {
        createCoreProblemFillers();
        createPowerGradientConstraintFiller();
        buildAndFillLinearProblem();
    }

    @Test
    void testGeneratorPowerConstraintFiller() throws IOException {
        createOneTSInput();
        setUpLinearProblem();

        // check generator power variable
        assertNotNull(linearProblem.getGeneratorPowerVariable("FFR1AA1 _load", timestamp1));
        assertNotNull(linearProblem.getGeneratorPowerVariable("FFR2AA1 _generator", timestamp1));
        assertNotNull(linearProblem.getGeneratorPowerVariable("FFR3AA1 _load", timestamp1));

        OpenRaoMPConstraint fr1Timestamp1PowerConstraint = linearProblem.getGeneratorPowerConstraint("FFR1AA1 _load", timestamp1);
        OpenRaoMPConstraint fr2Timestamp1PowerConstraint = linearProblem.getGeneratorPowerConstraint("FFR2AA1 _generator", timestamp1);
        OpenRaoMPConstraint fr3Timestamp1PowerConstraint = linearProblem.getGeneratorPowerConstraint("FFR3AA1 _load", timestamp1);

        assertNotNull(fr1Timestamp1PowerConstraint); //constraint created even if no injection action defined on this element
        assertNotNull(fr2Timestamp1PowerConstraint);
        assertNotNull(fr3Timestamp1PowerConstraint);

        //No power gradient constraint but injection range action defined on it -> No variable created for this generator
        assertThrows(OpenRaoException.class, () -> linearProblem.getGeneratorPowerVariable("FFR4AA1 _load", timestamp1));
        assertThrows(OpenRaoException.class, () -> linearProblem.getGeneratorPowerConstraint("FFR4AA1 _load", timestamp1));

        // check bound
        assertEquals(123.0, fr1Timestamp1PowerConstraint.ub());
        assertEquals(123.0, fr1Timestamp1PowerConstraint.lb());
        assertEquals(2000.0, fr2Timestamp1PowerConstraint.ub());
        assertEquals(2000.0, fr2Timestamp1PowerConstraint.lb());
        assertEquals(600.0, fr3Timestamp1PowerConstraint.ub());
        assertEquals(600.0, fr3Timestamp1PowerConstraint.lb());

        Crac crac1 = input.getRaoInputs().getData(timestamp1).get().getCrac();
        // check coefficient for injection action variable
        assertEquals(0, fr1Timestamp1PowerConstraint.getCoefficient(linearProblem.getRangeActionVariationVariable(crac1.getInjectionRangeAction("redispatchingAction1600"), crac1.getPreventiveState(), LinearProblem.VariationDirectionExtension.UPWARD)), 1e-5);
        assertEquals(0, fr1Timestamp1PowerConstraint.getCoefficient(linearProblem.getRangeActionVariationVariable(crac1.getInjectionRangeAction("redispatchingAction1600"), crac1.getPreventiveState(), LinearProblem.VariationDirectionExtension.DOWNWARD)), 1e-5);
        assertEquals(1.0, fr2Timestamp1PowerConstraint.getCoefficient(linearProblem.getRangeActionVariationVariable(crac1.getInjectionRangeAction("redispatchingAction1600"), crac1.getPreventiveState(), LinearProblem.VariationDirectionExtension.UPWARD)), 1e-5);
        assertEquals(-1.0, fr2Timestamp1PowerConstraint.getCoefficient(linearProblem.getRangeActionVariationVariable(crac1.getInjectionRangeAction("redispatchingAction1600"), crac1.getPreventiveState(), LinearProblem.VariationDirectionExtension.DOWNWARD)), 1e-5);
        assertEquals(-0.4, fr3Timestamp1PowerConstraint.getCoefficient(linearProblem.getRangeActionVariationVariable(crac1.getInjectionRangeAction("redispatchingAction1600"), crac1.getPreventiveState(), LinearProblem.VariationDirectionExtension.UPWARD)), 1e-5);
        assertEquals(0.4, fr3Timestamp1PowerConstraint.getCoefficient(linearProblem.getRangeActionVariationVariable(crac1.getInjectionRangeAction("redispatchingAction1600"), crac1.getPreventiveState(), LinearProblem.VariationDirectionExtension.DOWNWARD)), 1e-5);
    }

    @Test
    void testGeneratorPowerGradientConstraintFiller() throws IOException {
        createThreeTSInput();
        setUpLinearProblem();

        // check the power gradient constraint, expect two gradient constraints per generator
        OpenRaoMPConstraint powerGradientConstraintFR1TS12 = linearProblem.getGeneratorPowerGradientConstraint("FFR1AA1 _load", timestamp2, timestamp1);
        assertNotNull(powerGradientConstraintFR1TS12);
        assertEquals(-300.0, powerGradientConstraintFR1TS12.lb());
        assertEquals(500.0, powerGradientConstraintFR1TS12.ub());

        OpenRaoMPConstraint powerGradientConstraintFR1TS23 = linearProblem.getGeneratorPowerGradientConstraint("FFR1AA1 _load", timestamp3, timestamp2);
        assertNotNull(powerGradientConstraintFR1TS23);
        assertEquals(-600.0, powerGradientConstraintFR1TS23.lb());
        assertEquals(1000.0, powerGradientConstraintFR1TS23.ub());

        OpenRaoMPConstraint powerGradientConstraintFR2TS12 = linearProblem.getGeneratorPowerGradientConstraint("FFR2AA1 _generator", timestamp2, timestamp1);
        assertNotNull(powerGradientConstraintFR2TS12);
        assertEquals(-100.0, powerGradientConstraintFR2TS12.lb());
        assertEquals(200.0, powerGradientConstraintFR2TS12.ub());

        OpenRaoMPConstraint powerGradientConstraintFR2TS23 = linearProblem.getGeneratorPowerGradientConstraint("FFR2AA1 _generator", timestamp3, timestamp2);
        assertNotNull(powerGradientConstraintFR2TS23);
        assertEquals(-200.0, powerGradientConstraintFR2TS23.lb());
        assertEquals(400.0, powerGradientConstraintFR2TS23.ub());

        OpenRaoMPConstraint powerGradientConstraintFR3TS12 = linearProblem.getGeneratorPowerGradientConstraint("FFR3AA1 _load", timestamp2, timestamp1);
        assertNotNull(powerGradientConstraintFR3TS12);
        assertEquals(-150.0, powerGradientConstraintFR3TS12.lb());
        assertEquals(40.0, powerGradientConstraintFR3TS12.ub());

        OpenRaoMPConstraint powerGradientConstraintFR3TS23 = linearProblem.getGeneratorPowerGradientConstraint("FFR3AA1 _load", timestamp3, timestamp2);
        assertNotNull(powerGradientConstraintFR3TS23);
        assertEquals(-300.0, powerGradientConstraintFR3TS23.lb());
        assertEquals(80.0, powerGradientConstraintFR3TS23.ub());

        // wrong previous timestamp
        assertThrows(OpenRaoException.class, () -> linearProblem.getGeneratorPowerGradientConstraint("FFR1AA1 _load", timestamp3, timestamp1));
    }

    @Test
    void testMissingGradientBound() throws IOException {
        createThreeTSInput();

        GeneratorConstraints generatorConstraintsFr1 = GeneratorConstraints.create().withGeneratorId("FFR1AA1 _load").withLeadTime(0.0).withLagTime(0.0).withPMin(0.0).withPMax(1000.0).withUpwardPowerGradient(960.0).build();
        GeneratorConstraints generatorConstraintsFr2 = GeneratorConstraints.create().withGeneratorId("FFR2AA1 _generator").withLeadTime(0.0).withLagTime(0.0).withPMin(0.0).withPMax(1000.0).withDownwardPowerGradient(-100.0).build();
        GeneratorConstraints generatorConstraintsFr3 = GeneratorConstraints.create().withGeneratorId("FFR3AA1 _load").withLeadTime(0.0).withLagTime(0.0).withPMin(0.0).withPMax(1000.0).withUpwardPowerGradient(300.0).withDownwardPowerGradient(-200.0).build();

        input = new InterTemporalRaoInput(new TemporalDataImpl<>(input.getRaoInputs().getDataPerTimestamp()), Set.of(generatorConstraintsFr1, generatorConstraintsFr2, generatorConstraintsFr3));

        setUpLinearProblem();

        OpenRaoMPConstraint powerGradientConstraintFR1TS12 = linearProblem.getGeneratorPowerGradientConstraint("FFR1AA1 _load", timestamp2, timestamp1);
        assertNotNull(powerGradientConstraintFR1TS12);
        assertEquals(-linearProblem.infinity(), powerGradientConstraintFR1TS12.lb(), linearProblem.infinity() * 1e-3);
        assertEquals(960.0, powerGradientConstraintFR1TS12.ub());

        OpenRaoMPConstraint powerGradientConstraintFR2TS12 = linearProblem.getGeneratorPowerGradientConstraint("FFR2AA1 _generator", timestamp2, timestamp1);
        assertNotNull(powerGradientConstraintFR2TS12);
        assertEquals(-100.0, powerGradientConstraintFR2TS12.lb(), linearProblem.infinity() * 1e-3);
        assertEquals(linearProblem.infinity(), powerGradientConstraintFR2TS12.ub(), linearProblem.infinity() * 1e-3);
    }
}