DiscretePstTapFillerTest.java

/*
 * Copyright (c) 2021, 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.openrao.commons.Unit;
import com.powsybl.openrao.data.crac.api.State;
import com.powsybl.openrao.data.crac.api.range.RangeType;
import com.powsybl.openrao.data.crac.api.rangeaction.PstRangeAction;
import com.powsybl.openrao.data.crac.api.rangeaction.RangeAction;
import com.powsybl.openrao.raoapi.parameters.RangeActionsOptimizationParameters;
import com.powsybl.openrao.raoapi.parameters.extensions.SearchTreeRaoRangeActionsOptimizationParameters.PstModel;
import com.powsybl.openrao.raoapi.parameters.extensions.SearchTreeRaoRangeActionsOptimizationParameters.Solver;
import com.powsybl.openrao.raoapi.parameters.RaoParameters;
import com.powsybl.openrao.searchtreerao.commons.optimizationperimeters.OptimizationPerimeter;
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.linearoptimisation.algorithms.linearproblem.OpenRaoMPVariable;
import com.powsybl.openrao.searchtreerao.result.api.RangeActionSetpointResult;
import com.powsybl.openrao.searchtreerao.result.impl.RangeActionActivationResultImpl;
import com.powsybl.openrao.searchtreerao.result.impl.RangeActionSetpointResultImpl;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mockito;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import static com.powsybl.openrao.data.crac.api.usagerule.UsageMethod.AVAILABLE;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

/**
 * @author Baptiste Seguinot {@literal <baptiste.seguinot at rte-france.com>}
 */
class DiscretePstTapFillerTest extends AbstractFillerTest {

    private LinearProblem linearProblem;
    private State preventiveState;
    private State curativeState;
    private DiscretePstTapFiller discretePstTapFiller;
    private double initialAlpha;
    private Map<Integer, Double> tapToAngle;
    private PstRangeAction pra;
    private PstRangeAction cra;

    void setUpAndFill(boolean costOptimization) throws IOException {
        // prepare data
        init();
        preventiveState = crac.getPreventiveState();
        curativeState = crac.getCurativeStates().iterator().next();
        tapToAngle = pstRangeAction.getTapToAngleConversionMap();
        pra = crac.getPstRangeAction("PRA_PST_BE");
        cra = crac.newPstRangeAction()
            .withId("cra")
            .withNetworkElement("BBE2AA1  BBE3AA1  1")
            .newOnContingencyStateUsageRule().withUsageMethod(AVAILABLE).withContingency("N-1 NL1-NL3").withInstant("curative").add()
            .withInitialTap(0)
            .withActivationCost(10.0)
            .withTapToAngleConversionMap(tapToAngle)
            .newTapRange()
            .withMinTap(-10)
            .withMaxTap(7)
            .withRangeType(RangeType.RELATIVE_TO_PREVIOUS_INSTANT)
            .add()
            .add();
        PstRangeAction pstRangeAction = crac.getPstRangeAction(RANGE_ACTION_ID);
        initialAlpha = network.getTwoWindingsTransformer(RANGE_ACTION_ELEMENT_ID).getPhaseTapChanger().getCurrentStep().getAlpha();
        RangeActionSetpointResult initialRangeActionSetpointResult = new RangeActionSetpointResultImpl(Map.of(pstRangeAction, initialAlpha, cra, initialAlpha));
        OptimizationPerimeter optimizationPerimeter = Mockito.mock(OptimizationPerimeter.class);

        Map<State, Set<RangeAction<?>>> rangeActions = new HashMap<>();
        rangeActions.put(preventiveState, Set.of(pstRangeAction));
        rangeActions.put(curativeState, Set.of(cra));
        Mockito.when(optimizationPerimeter.getRangeActionsPerState()).thenReturn(rangeActions);
        Mockito.when(optimizationPerimeter.getMainOptimizationState()).thenReturn(preventiveState);

        RangeActionsOptimizationParameters rangeActionParameters = (new RaoParameters()).getRangeActionsOptimizationParameters();

        MarginCoreProblemFiller coreProblemFiller = new MarginCoreProblemFiller(
            optimizationPerimeter,
            initialRangeActionSetpointResult,
            rangeActionParameters,
            null,
            Unit.MEGAWATT,
            false,
            PstModel.APPROXIMATED_INTEGERS,
            null);

        Map<State, Set<PstRangeAction>> pstRangeActions = new HashMap<>();
        pstRangeActions.put(preventiveState, Set.of(pstRangeAction));
        pstRangeActions.put(curativeState, Set.of(cra));
        discretePstTapFiller = new DiscretePstTapFiller(
            optimizationPerimeter,
            pstRangeActions,
            initialRangeActionSetpointResult,
            rangeActionParameters,
            true, null);

        linearProblem = new LinearProblemBuilder()
            .withProblemFiller(coreProblemFiller)
            .withProblemFiller(discretePstTapFiller)
            .withSolver(Solver.SCIP)
            .withInitialRangeActionActivationResult(getInitialRangeActionActivationResult())
            .build();

        // fill linear problem
        linearProblem.fill(flowResult, sensitivityResult);
    }

    private void checkContent(PstRangeAction pst, State state, int initialTap, int minAbsoluteTap, int maxAbsoluteTap, boolean firstIteration) {
        double initialSetpoint = tapToAngle.get(initialTap);

        // check that all constraints and variables exists
        OpenRaoMPVariable setpointV = linearProblem.getRangeActionSetpointVariable(pst, state);
        OpenRaoMPVariable variationUpV = linearProblem.getPstTapVariationVariable(pst, state, LinearProblem.VariationDirectionExtension.UPWARD);
        OpenRaoMPVariable variationDownV = linearProblem.getPstTapVariationVariable(pst, state, LinearProblem.VariationDirectionExtension.DOWNWARD);
        OpenRaoMPVariable binaryUpV = linearProblem.getPstTapVariationBinary(pst, state, LinearProblem.VariationDirectionExtension.UPWARD);
        OpenRaoMPVariable binaryDownV = linearProblem.getPstTapVariationBinary(pst, state, LinearProblem.VariationDirectionExtension.DOWNWARD);
        OpenRaoMPConstraint tapToAngleConversionC = linearProblem.getTapToAngleConversionConstraint(pst, state);
        OpenRaoMPConstraint upOrDownC = linearProblem.getUpOrDownPstVariationConstraint(pst, state);
        OpenRaoMPConstraint upVariationC = linearProblem.getIsVariationInDirectionConstraint(pst, state, LinearProblem.VariationReferenceExtension.PREVIOUS_ITERATION, LinearProblem.VariationDirectionExtension.UPWARD);
        OpenRaoMPConstraint downVariationC = linearProblem.getIsVariationInDirectionConstraint(pst, state, LinearProblem.VariationReferenceExtension.PREVIOUS_ITERATION, LinearProblem.VariationDirectionExtension.DOWNWARD);

        assertNotNull(setpointV);
        assertNotNull(variationUpV);
        assertNotNull(variationDownV);
        assertNotNull(binaryUpV);
        assertNotNull(binaryDownV);
        assertNotNull(tapToAngleConversionC);
        assertNotNull(upOrDownC);
        assertNotNull(upVariationC);
        assertNotNull(downVariationC);

        // check variable bounds
        assertEquals(0, variationUpV.lb(), 1e-6);
        assertEquals(maxAbsoluteTap - minAbsoluteTap, variationUpV.ub(), 1e-6);
        assertEquals(0, variationDownV.lb(), 1e-6);
        assertEquals(maxAbsoluteTap - minAbsoluteTap, variationDownV.ub(), 1e-6);
        assertEquals(0, binaryUpV.lb(), 1e-6);
        assertEquals(1, binaryUpV.ub(), 1e-6);
        assertEquals(0, binaryDownV.lb(), 1e-6);
        assertEquals(1, binaryDownV.ub(), 1e-6);

        // check tap to angle conversion constraints
        assertEquals(initialSetpoint, tapToAngleConversionC.lb(), 1e-6);
        assertEquals(initialSetpoint, tapToAngleConversionC.ub(), 1e-6);
        assertEquals(1, tapToAngleConversionC.getCoefficient(setpointV), 1e-6);
        double coeffUp;
        double coeffDown;
        if (firstIteration) {
            coeffUp = -(tapToAngle.get(maxAbsoluteTap) - tapToAngle.get(initialTap)) / (maxAbsoluteTap - initialTap);
            coeffDown = -(tapToAngle.get(minAbsoluteTap) - tapToAngle.get(initialTap)) / (initialTap - minAbsoluteTap);
        } else {
            coeffUp = -(tapToAngle.get(initialTap + 1) - tapToAngle.get(initialTap));
            coeffDown = -(tapToAngle.get(initialTap - 1) - tapToAngle.get(initialTap));
        }
        assertEquals(coeffUp, tapToAngleConversionC.getCoefficient(variationUpV), 1e-6);
        assertEquals(coeffDown, tapToAngleConversionC.getCoefficient(variationDownV), 1e-6);

        // check other constraints
        assertEquals(1, upOrDownC.ub(), 1e-6);
        assertEquals(1, upOrDownC.getCoefficient(binaryUpV), 1e-6);
        assertEquals(1, upOrDownC.getCoefficient(binaryDownV), 1e-6);

        assertEquals(0, upVariationC.ub(), 1e-6);
        assertEquals(1, upVariationC.getCoefficient(variationUpV), 1e-6);
        assertEquals(-(maxAbsoluteTap - initialTap), upVariationC.getCoefficient(binaryUpV), 1e-6);

        assertEquals(0, downVariationC.ub(), 1e-6);
        assertEquals(1, downVariationC.getCoefficient(variationDownV), 1e-6);
        assertEquals(-(initialTap - minAbsoluteTap), downVariationC.getCoefficient(binaryDownV), 1e-6);
    }

    private void checkPstRelativeTapConstraint(double expectedLb, double expectedUb) {
        OpenRaoMPVariable variationUpV = linearProblem.getPstTapVariationVariable(pstRangeAction, preventiveState, LinearProblem.VariationDirectionExtension.UPWARD);
        OpenRaoMPVariable variationDownV = linearProblem.getPstTapVariationVariable(pstRangeAction, preventiveState, LinearProblem.VariationDirectionExtension.DOWNWARD);
        OpenRaoMPVariable craVariationUpV = linearProblem.getPstTapVariationVariable(cra, curativeState, LinearProblem.VariationDirectionExtension.UPWARD);
        OpenRaoMPVariable craVariationDownV = linearProblem.getPstTapVariationVariable(cra, curativeState, LinearProblem.VariationDirectionExtension.DOWNWARD);
        OpenRaoMPConstraint craRelativeTapC = linearProblem.getPstRelativeTapConstraint(cra, curativeState);

        assertNotNull(craRelativeTapC);

        assertEquals(expectedLb, craRelativeTapC.lb(), linearProblem.infinity() * 1e-3);
        assertEquals(expectedUb, craRelativeTapC.ub(), 1e-6);
        assertEquals(1, craRelativeTapC.getCoefficient(craVariationUpV));
        assertEquals(-1, craRelativeTapC.getCoefficient(craVariationDownV));
        assertEquals(-1, craRelativeTapC.getCoefficient(variationUpV));
        assertEquals(1, craRelativeTapC.getCoefficient(variationDownV));
    }

    @ParameterizedTest
    @ValueSource(booleans = {true, false})
    void testFillAndUpdateMethods(boolean costOptimization) throws IOException {
        setUpAndFill(costOptimization);
        checkContent(pstRangeAction, preventiveState, 0, -15, 15, true);
        checkContent(cra, curativeState, 0, -16, 16, true);
        checkPstRelativeTapConstraint(-10, 7);

        // update linear problem, with a new PST tap equal to -4
        double alphaBeforeUpdate = tapToAngle.get(-4);
        RangeActionActivationResultImpl rangeActionActivationResultBeforeUpdate = new RangeActionActivationResultImpl(new RangeActionSetpointResultImpl(Map.of(this.pstRangeAction, alphaBeforeUpdate, cra, alphaBeforeUpdate)));
        // Update between sensi iterations considering the following optimal result of the 1st iteration:
        // pra optimal tap = -4, cra optimal tap = -6
        rangeActionActivationResultBeforeUpdate.putResult(pra, preventiveState, tapToAngle.get(-4));
        rangeActionActivationResultBeforeUpdate.putResult(cra, curativeState, tapToAngle.get(-6));
        linearProblem.updateBetweenSensiIteration(flowResult, sensitivityResult, rangeActionActivationResultBeforeUpdate);

        checkContent(pra, preventiveState, -4, -15, 15, false);
        checkContent(cra, curativeState, -6, -16, 16, false);
        checkPstRelativeTapConstraint(-8, 9);

        if (costOptimization) {
            assertEquals(10.0, linearProblem.getObjective().getCoefficient(linearProblem.getTotalPstRangeActionTapVariationVariable(pra, preventiveState, LinearProblem.VariationDirectionExtension.UPWARD)));
            assertEquals(10.0, linearProblem.getObjective().getCoefficient(linearProblem.getTotalPstRangeActionTapVariationVariable(pra, preventiveState, LinearProblem.VariationDirectionExtension.UPWARD)));
            assertEquals(0.01, linearProblem.getObjective().getCoefficient(linearProblem.getTotalPstRangeActionTapVariationVariable(cra, curativeState, LinearProblem.VariationDirectionExtension.UPWARD)), 1e-2);
            assertEquals(0.01, linearProblem.getObjective().getCoefficient(linearProblem.getTotalPstRangeActionTapVariationVariable(cra, curativeState, LinearProblem.VariationDirectionExtension.DOWNWARD)), 1e-2);
        }
    }

    @Test
    void testUpdateBetweenMipIteration() throws IOException {
        setUpAndFill(false);
        RangeActionActivationResultImpl rangeActionActivationResult =
            new RangeActionActivationResultImpl(new RangeActionSetpointResultImpl(Map.of(pra, 0., cra, 0.)));

        // Update between MIP iterations considering the following optimal result of the 1st iteration:
        // pra optimal tap = 10, cra optimal tap = 15
        rangeActionActivationResult.putResult(pra, preventiveState, tapToAngle.get(10));
        rangeActionActivationResult.putResult(cra, curativeState, tapToAngle.get(15));
        linearProblem.updateBetweenMipIteration(rangeActionActivationResult);

        checkContent(pstRangeAction, preventiveState, 10, -15, 15, false);
        checkContent(cra, curativeState, 15, -16, 16, false);
        checkPstRelativeTapConstraint(-15, 2);
    }
}