IteratingLinearOptimizerTest.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;

import com.powsybl.openrao.commons.Unit;
import com.powsybl.openrao.data.crac.api.Crac;
import com.powsybl.openrao.data.crac.api.CracFactory;
import com.powsybl.openrao.data.crac.api.Instant;
import com.powsybl.openrao.data.crac.api.NetworkElement;
import com.powsybl.openrao.data.crac.api.State;
import com.powsybl.openrao.data.crac.api.rangeaction.RangeAction;
import com.powsybl.openrao.data.crac.impl.utils.NetworkImportsUtil;
import com.powsybl.openrao.data.raoresult.api.ComputationStatus;
import com.powsybl.openrao.raoapi.parameters.ObjectiveFunctionParameters;
import com.powsybl.openrao.raoapi.parameters.extensions.SearchTreeRaoRangeActionsOptimizationParameters;
import com.powsybl.openrao.searchtreerao.commons.SensitivityComputer;
import com.powsybl.openrao.searchtreerao.commons.adapter.BranchResultAdapter;
import com.powsybl.openrao.searchtreerao.commons.adapter.SensitivityResultAdapter;
import com.powsybl.openrao.searchtreerao.commons.objectivefunction.ObjectiveFunction;
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.OpenRaoMPVariable;
import com.powsybl.openrao.searchtreerao.linearoptimisation.inputs.IteratingLinearOptimizerInput;
import com.powsybl.openrao.searchtreerao.linearoptimisation.parameters.IteratingLinearOptimizerParameters;
import com.powsybl.openrao.searchtreerao.result.api.*;
import com.powsybl.openrao.searchtreerao.result.impl.IteratingLinearOptimizationResultImpl;
import com.powsybl.openrao.searchtreerao.result.impl.RangeActionActivationResultImpl;
import com.powsybl.openrao.searchtreerao.result.impl.RangeActionSetpointResultImpl;
import com.powsybl.openrao.sensitivityanalysis.SystematicSensitivityInterface;
import com.powsybl.openrao.sensitivityanalysis.SystematicSensitivityResult;
import com.powsybl.iidm.network.Network;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;

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

/**
 * @author Joris Mancini {@literal <joris.mancini at rte-france.com>}
 */
class IteratingLinearOptimizerTest {
    private static final double DOUBLE_TOLERANCE = 0.1;

    private RangeAction<?> rangeAction;

    private ObjectiveFunction objectiveFunction;

    private LinearProblem linearProblem;
    private Network network;
    private RangeActionSetpointResult rangeActionSetpointResult;
    private RangeActionActivationResult rangeActionActivationResult;
    private SensitivityComputer sensitivityComputer;
    private State optimizedState;
    private IteratingLinearOptimizerInput input;
    private IteratingLinearOptimizerParameters parameters;
    private OptimizationPerimeter optimizationPerimeter;

    private MockedStatic<LinearProblem> linearProblemMockedStatic;
    private MockedStatic<SensitivityComputer> sensitivityComputerMockedStatic;

    @BeforeEach
    public void setUp() {
        rangeAction = Mockito.mock(RangeAction.class);
        when(rangeAction.getId()).thenReturn("ra");
        when(rangeAction.getNetworkElements()).thenReturn(Set.of(Mockito.mock(NetworkElement.class)));
        optimizedState = Mockito.mock(State.class);
        Instant preventiveInstant = Mockito.mock(Instant.class);
        when(optimizedState.getInstant()).thenReturn(preventiveInstant);

        objectiveFunction = Mockito.mock(ObjectiveFunction.class);
        SystematicSensitivityInterface systematicSensitivityInterface = Mockito.mock(SystematicSensitivityInterface.class);
        SensitivityResultAdapter sensitivityResultAdapter = Mockito.mock(SensitivityResultAdapter.class);

        input = Mockito.mock(IteratingLinearOptimizerInput.class);
        when(input.objectiveFunction()).thenReturn(objectiveFunction);
        SensitivityResult sensitivityResult1 = Mockito.mock(SensitivityResult.class);
        when(input.preOptimizationSensitivityResult()).thenReturn(sensitivityResult1);
        optimizationPerimeter = Mockito.mock(OptimizationPerimeter.class);
        when(optimizationPerimeter.getRangeActionsPerState()).thenReturn(Map.of(
            optimizedState, Set.of(rangeAction)
        ));
        when(optimizationPerimeter.getMainOptimizationState()).thenReturn(optimizedState);
        when(input.optimizationPerimeter()).thenReturn(optimizationPerimeter);

        parameters = Mockito.mock(IteratingLinearOptimizerParameters.class);
        SearchTreeRaoRangeActionsOptimizationParameters.LinearOptimizationSolver solverParameters = Mockito.mock(SearchTreeRaoRangeActionsOptimizationParameters.LinearOptimizationSolver.class);
        when(solverParameters.getSolver()).thenReturn(SearchTreeRaoRangeActionsOptimizationParameters.Solver.CBC);
        when(parameters.getSolverParameters()).thenReturn(solverParameters);
        when(parameters.getMaxNumberOfIterations()).thenReturn(5);
        SearchTreeRaoRangeActionsOptimizationParameters rangeActionParameters = Mockito.mock(SearchTreeRaoRangeActionsOptimizationParameters.class);
        when(rangeActionParameters.getPstModel()).thenReturn(SearchTreeRaoRangeActionsOptimizationParameters.PstModel.CONTINUOUS);
        when(parameters.getRangeActionParametersExtension()).thenReturn(rangeActionParameters);
        when(parameters.getObjectiveFunction()).thenReturn(ObjectiveFunctionParameters.ObjectiveFunctionType.MAX_MIN_MARGIN);
        when(parameters.getObjectiveFunctionUnit()).thenReturn(Unit.MEGAWATT);
        when(parameters.getRaRangeShrinking()).thenReturn(false);

        linearProblem = Mockito.mock(LinearProblem.class);
        network = Mockito.mock(Network.class);
        when(input.network()).thenReturn(network);
        rangeActionSetpointResult = new RangeActionSetpointResultImpl(Map.of(rangeAction, 0.));
        when(input.prePerimeterSetpoints()).thenReturn(rangeActionSetpointResult);
        rangeActionActivationResult = new RangeActionActivationResultImpl(rangeActionSetpointResult);
        when(input.raActivationFromParentLeaf()).thenReturn(rangeActionActivationResult);
        BranchResultAdapter branchResultAdapter = Mockito.mock(BranchResultAdapter.class);
        sensitivityComputer = Mockito.mock(SensitivityComputer.class);

        Instant outageInstant = Mockito.mock(Instant.class);
        when(outageInstant.isOutage()).thenReturn(true);
        SystematicSensitivityResult sensi = Mockito.mock(SystematicSensitivityResult.class, "only sensi computation");
        when(systematicSensitivityInterface.run(network)).thenReturn(sensi);
        FlowResult flowResult = Mockito.mock(FlowResult.class);
        when(branchResultAdapter.getResult(sensi, network)).thenReturn(flowResult);
        when(sensitivityComputer.getBranchResult(network)).thenReturn(flowResult);
        when(sensitivityComputer.getSensitivityResult()).thenReturn(sensitivityResult1);
        SensitivityResult sensitivityResult = Mockito.mock(SensitivityResult.class);
        when(sensitivityResult.getSensitivityStatus()).thenReturn(ComputationStatus.DEFAULT);
        when(sensitivityResultAdapter.getResult(sensi)).thenReturn(sensitivityResult);

        linearProblemMockedStatic = Mockito.mockStatic(LinearProblem.class);
        sensitivityComputerMockedStatic = Mockito.mockStatic(SensitivityComputer.class);
        SensitivityComputer.SensitivityComputerBuilder sensitivityComputerBuilder = Mockito.spy(SensitivityComputer.SensitivityComputerBuilder.class);
        doReturn(sensitivityComputer).when(sensitivityComputerBuilder).build();
        sensitivityComputerMockedStatic.when(SensitivityComputer::create).thenReturn(sensitivityComputerBuilder);

        when(input.outageInstant()).thenReturn(outageInstant);
    }

    @AfterEach
    public void tearDown() {
        linearProblemMockedStatic.close();
        sensitivityComputerMockedStatic.close();
    }

    private void prepareLinearProblemBuilder() {
        LinearProblemBuilder linearProblemBuilder = Mockito.mock(LinearProblemBuilder.class);
        when(linearProblemBuilder.buildFromInputsAndParameters(Mockito.any(), Mockito.any())).thenReturn(linearProblem);
        linearProblemMockedStatic.when(LinearProblem::create).thenReturn(linearProblemBuilder);
    }

    private void mockFunctionalCost(Double initialFunctionalCost, Double... iterationFunctionalCosts) {
        ObjectiveFunctionResult initialObjectiveFunctionResult = Mockito.mock(ObjectiveFunctionResult.class);
        when(initialObjectiveFunctionResult.getFunctionalCost()).thenReturn(initialFunctionalCost);
        if (iterationFunctionalCosts.length == 0) {
            when(objectiveFunction.evaluate(any(), any())).thenReturn(initialObjectiveFunctionResult);
        } else {
            ObjectiveFunctionResult[] objectiveFunctionResults = new ObjectiveFunctionResult[iterationFunctionalCosts.length];
            for (int i = 0; i < iterationFunctionalCosts.length; i++) {
                ObjectiveFunctionResult objectiveFunctionResult = Mockito.mock(ObjectiveFunctionResult.class);
                when(objectiveFunctionResult.getFunctionalCost()).thenReturn(iterationFunctionalCosts[i]);
                objectiveFunctionResults[i] = objectiveFunctionResult;
            }
            when(objectiveFunction.evaluate(any(), any())).thenReturn(
                initialObjectiveFunctionResult,
                objectiveFunctionResults
            );
        }
    }

    private void mockLinearProblem(List<LinearProblemStatus> statuses, List<Double> setPoints) {
        doAnswer(new Answer() {
            private int count = 0;

            public Object answer(InvocationOnMock invocation) {
                count += 1;
                if (statuses.get(count - 1) == LinearProblemStatus.OPTIMAL) {
                    OpenRaoMPVariable absVariationMpVarMock = Mockito.mock(OpenRaoMPVariable.class);
                    when(absVariationMpVarMock.solutionValue()).thenReturn(Math.abs(setPoints.get(count - 1)));
                    when(linearProblem.getAbsoluteRangeActionVariationVariable(rangeAction, optimizedState)).thenReturn(absVariationMpVarMock);
                    OpenRaoMPVariable setpointMpVarMock = Mockito.mock(OpenRaoMPVariable.class);
                    when(setpointMpVarMock.solutionValue()).thenReturn(setPoints.get(count - 1));
                    when(linearProblem.getRangeActionSetpointVariable(rangeAction, optimizedState)).thenReturn(setpointMpVarMock);
                }
                return statuses.get(count - 1);
            }
        }).when(linearProblem).solve();
    }

    @Test
    void firstOptimizationFails() {
        mockLinearProblem(List.of(LinearProblemStatus.INFEASIBLE), Collections.emptyList());
        mockFunctionalCost(100.);
        prepareLinearProblemBuilder();
        LinearOptimizationResult result = IteratingLinearOptimizer.optimize(input, parameters);

        assertEquals(LinearProblemStatus.INFEASIBLE, result.getStatus());
    }

    @Test
    void firstLinearProblemDoesNotChangeSetPoint() {
        mockLinearProblem(List.of(LinearProblemStatus.OPTIMAL), List.of(0.));
        mockFunctionalCost(100.);
        prepareLinearProblemBuilder();

        LinearOptimizationResult result = IteratingLinearOptimizer.optimize(input, parameters);

        assertEquals(LinearProblemStatus.OPTIMAL, result.getStatus());
        assertEquals(1, ((IteratingLinearOptimizationResultImpl) result).getNbOfIteration());
        assertEquals(100, result.getFunctionalCost(), DOUBLE_TOLERANCE);
        assertEquals(0, result.getOptimizedSetpoint(rangeAction, optimizedState), DOUBLE_TOLERANCE);
    }

    @Test
    void secondLinearProblemDoesNotChangeSetPoint() {
        mockLinearProblem(Collections.nCopies(2, LinearProblemStatus.OPTIMAL), List.of(1., 1.));
        mockFunctionalCost(100., 50.);
        prepareLinearProblemBuilder();

        LinearOptimizationResult result = IteratingLinearOptimizer.optimize(input, parameters);

        assertEquals(LinearProblemStatus.OPTIMAL, result.getStatus());
        assertEquals(2, ((IteratingLinearOptimizationResultImpl) result).getNbOfIteration());
        assertEquals(50, result.getFunctionalCost(), DOUBLE_TOLERANCE);
        assertEquals(1, result.getOptimizedSetpoint(rangeAction, optimizedState), DOUBLE_TOLERANCE);
    }

    @Test
    void linearProblemDegradesTheSolutionButKeepsBestIteration() {
        when(parameters.getRaRangeShrinking()).thenReturn(true);
        mockLinearProblem(Collections.nCopies(5, LinearProblemStatus.OPTIMAL), List.of(1., 2., 3., 4., 5.));
        mockFunctionalCost(100., 150., 140., 130., 120., 110.);
        prepareLinearProblemBuilder();

        LinearOptimizationResult result = IteratingLinearOptimizer.optimize(input, parameters);

        assertEquals(LinearProblemStatus.MAX_ITERATION_REACHED, result.getStatus());
        assertEquals(5, ((IteratingLinearOptimizationResultImpl) result).getNbOfIteration());
        assertEquals(100, result.getFunctionalCost(), DOUBLE_TOLERANCE);
        assertEquals(0, result.getOptimizedSetpoint(rangeAction, optimizedState), DOUBLE_TOLERANCE);
    }

    @Test
    void linearProblemDegradesTheSolution() {
        mockLinearProblem(Collections.nCopies(5, LinearProblemStatus.OPTIMAL), List.of(1., 2., 3., 4., 5.));
        mockFunctionalCost(100., 150., 140., 130., 120., 110.);
        prepareLinearProblemBuilder();

        LinearOptimizationResult result = IteratingLinearOptimizer.optimize(input, parameters);

        assertEquals(LinearProblemStatus.OPTIMAL, result.getStatus());
        assertEquals(1, ((IteratingLinearOptimizationResultImpl) result).getNbOfIteration());
        assertEquals(100, result.getFunctionalCost(), DOUBLE_TOLERANCE);
        assertEquals(0, result.getOptimizedSetpoint(rangeAction, optimizedState), DOUBLE_TOLERANCE);
    }

    @Test
    void linearProblemFluctuatesButKeepsBestIteration() {
        when(parameters.getRaRangeShrinking()).thenReturn(true);
        mockLinearProblem(Collections.nCopies(5, LinearProblemStatus.OPTIMAL), List.of(1., 2., 3., 4., 5.));
        mockFunctionalCost(100., 120., 105., 90., 100., 95.);
        prepareLinearProblemBuilder();

        LinearOptimizationResult result = IteratingLinearOptimizer.optimize(input, parameters);

        assertEquals(LinearProblemStatus.MAX_ITERATION_REACHED, result.getStatus());
        assertEquals(5, ((IteratingLinearOptimizationResultImpl) result).getNbOfIteration());
        assertEquals(90, result.getFunctionalCost(), DOUBLE_TOLERANCE);
        assertEquals(3, result.getOptimizedSetpoint(rangeAction, optimizedState), DOUBLE_TOLERANCE);
    }

    @Test
    void reachMaxIterations() {
        mockLinearProblem(Collections.nCopies(5, LinearProblemStatus.OPTIMAL), List.of(1., 2., 3., 4., 5.));
        mockFunctionalCost(100., 90., 80., 70., 60., 50.);
        prepareLinearProblemBuilder();

        LinearOptimizationResult result = IteratingLinearOptimizer.optimize(input, parameters);

        assertEquals(LinearProblemStatus.MAX_ITERATION_REACHED, result.getStatus());
        assertEquals(5, ((IteratingLinearOptimizationResultImpl) result).getNbOfIteration());
        assertEquals(50, result.getFunctionalCost(), DOUBLE_TOLERANCE);
        assertEquals(5, result.getOptimizedSetpoint(rangeAction, optimizedState), DOUBLE_TOLERANCE);
    }

    @Test
    void optimizeWithInfeasibility() {
        mockLinearProblem(List.of(LinearProblemStatus.OPTIMAL, LinearProblemStatus.INFEASIBLE), List.of(1.));
        mockFunctionalCost(100., 50.);
        prepareLinearProblemBuilder();

        LinearOptimizationResult result = IteratingLinearOptimizer.optimize(input, parameters);

        assertEquals(LinearProblemStatus.FEASIBLE, result.getStatus());
        assertEquals(2, ((IteratingLinearOptimizationResultImpl) result).getNbOfIteration());
        assertEquals(50, result.getFunctionalCost(), DOUBLE_TOLERANCE);
        assertEquals(1, result.getOptimizedSetpoint(rangeAction, optimizedState), DOUBLE_TOLERANCE);
    }

    @Test
    void optimizeWithSensitivityComputationFailure() {
        SensitivityResult sensitivityResult = Mockito.mock(SensitivityResult.class);
        Mockito.when(sensitivityComputer.getSensitivityResult()).thenReturn(sensitivityResult);
        Mockito.when(sensitivityResult.getSensitivityStatus()).thenReturn(ComputationStatus.FAILURE);
        Mockito.doNothing().when(sensitivityComputer).compute(network);
        mockLinearProblem(List.of(LinearProblemStatus.OPTIMAL), List.of(1.));
        mockFunctionalCost(100.);
        prepareLinearProblemBuilder();

        LinearOptimizationResult result = IteratingLinearOptimizer.optimize(input, parameters);

        assertEquals(LinearProblemStatus.SENSITIVITY_COMPUTATION_FAILED, result.getStatus());
        assertEquals(1, ((IteratingLinearOptimizationResultImpl) result).getNbOfIteration());
        assertEquals(100, result.getFunctionalCost(), DOUBLE_TOLERANCE);
        assertEquals(0, result.getOptimizedSetpoint(rangeAction, optimizedState), DOUBLE_TOLERANCE);
    }

    @Test
    void testUnapplyRangeAction() {
        when(parameters.getRaRangeShrinking()).thenReturn(true);
        network = NetworkImportsUtil.import12NodesNetwork();
        when(input.network()).thenReturn(network);
        mockLinearProblem(Collections.nCopies(5, LinearProblemStatus.OPTIMAL), List.of(1., 2., 3., 4., 5.));
        mockFunctionalCost(100., 120., 105., 90., 100., 95.);
        Crac crac = CracFactory.findDefault().create("test-crac");
        rangeAction = crac.newPstRangeAction().withId("test-pst").withNetworkElement("BBE2AA1  BBE3AA1  1")
            .withInitialTap(0)
            .withTapToAngleConversionMap(Map.of(0, 0., 1, 1., 2, 2., 3, 3., 4, 4., 5, 5.)).add();
        when(optimizationPerimeter.getRangeActionsPerState()).thenReturn(Map.of(
            optimizedState, Set.of(rangeAction)
        ));
        when(optimizationPerimeter.getRangeActionOptimizationStates()).thenReturn(Set.of(optimizedState));
        rangeActionSetpointResult = new RangeActionSetpointResultImpl(Map.of(rangeAction, 5.));
        when(input.prePerimeterSetpoints()).thenReturn(rangeActionSetpointResult);
        rangeActionActivationResult = new RangeActionActivationResultImpl(rangeActionSetpointResult);
        when(input.raActivationFromParentLeaf()).thenReturn(rangeActionActivationResult);
        prepareLinearProblemBuilder();

        IteratingLinearOptimizer.optimize(input, parameters);
        assertEquals(3, network.getTwoWindingsTransformer("BBE2AA1  BBE3AA1  1").getPhaseTapChanger().getTapPosition());
    }
}