PreventiveAndCurativesRaoResultImplTest.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.result.impl;

import com.powsybl.contingency.ContingencyElementType;
import com.powsybl.openrao.commons.OpenRaoException;
import com.powsybl.openrao.data.crac.api.*;
import com.powsybl.openrao.data.crac.api.cnec.FlowCnec;
import com.powsybl.openrao.data.crac.api.range.RangeType;
import com.powsybl.openrao.data.crac.api.usagerule.UsageMethod;
import com.powsybl.openrao.data.crac.impl.CracImpl;
import com.powsybl.openrao.data.raoresult.api.ComputationStatus;
import com.powsybl.openrao.raoapi.parameters.RaoParameters;
import com.powsybl.openrao.searchtreerao.castor.algorithm.ContingencyScenario;
import com.powsybl.openrao.searchtreerao.castor.algorithm.Perimeter;
import com.powsybl.openrao.searchtreerao.result.api.FlowResult;
import com.powsybl.openrao.searchtreerao.result.api.OptimizationResult;
import com.powsybl.openrao.searchtreerao.result.api.PrePerimeterResult;
import com.powsybl.openrao.searchtreerao.castor.algorithm.StateTree;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.opentest4j.AssertionFailedError;

import java.util.*;
import java.util.concurrent.atomic.AtomicReference;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.Mockito.when;
import static com.powsybl.iidm.network.TwoSides.ONE;
import static com.powsybl.openrao.commons.Unit.*;

/**
 * @author Peter Mitri {@literal <peter.mitri at rte-france.com>}
 */
class PreventiveAndCurativesRaoResultImplTest {
    private static final Map<InstantKind, Double> FLOW_PER_INSTANT = Map.of(InstantKind.OUTAGE, 10d, InstantKind.AUTO, 20d, InstantKind.CURATIVE, 30d);
    private static final Map<InstantKind, Double> FLOW_PER_OPTIMIZED_INSTANT = Map.of(InstantKind.PREVENTIVE, 100d, InstantKind.AUTO, 200d, InstantKind.CURATIVE, 300d);

    private static final double DOUBLE_TOLERANCE = 1e-3;
    private Crac crac;
    private Instant preventiveInstant;
    private Instant autoInstant;
    private Instant curativeInstant;
    private List<Set<Instant>> optimizationInstantsPerContingency;

    private PrePerimeterResult initialResult;
    private OptimizationResult prevResult;
    private PrePerimeterResult postPrevPrePerimResult;
    private PostPerimeterResult postPrevResult;
    private OptimizationResult autoResult3;
    private PrePerimeterResult postAutoPrePerimResult3;
    private PostPerimeterResult postAutoResult3;
    private OptimizationResult autoResult4;
    private PrePerimeterResult postAutoPrePerimResult4;
    private PostPerimeterResult postAutoResult4;
    private OptimizationResult curativeResult2;
    private PrePerimeterResult postCurativePrePerimResult2;
    private PostPerimeterResult postCurativeResult2;
    private OptimizationResult curativeResult3;
    private PrePerimeterResult postCurativePrePerimResult3;
    private PostPerimeterResult postCurativeResult3;
    private Map<State, PostPerimeterResult> postContingencyResults = new HashMap<>();

    private PreventiveAndCurativesRaoResultImpl output;

    private void initCrac() {
        crac = new CracImpl("crac");
        // Instants
        crac.newInstant("preventive", InstantKind.PREVENTIVE)
            .newInstant("outage", InstantKind.OUTAGE)
            .newInstant("auto", InstantKind.AUTO)
            .newInstant("curative", InstantKind.CURATIVE);
        for (int i = 1; i <= 4; i++) {
            crac.newContingency()
                .withId("contingency-" + i)
                .withName("CO" + i)
                .withContingencyElement("element-" + i, ContingencyElementType.LINE)
                .add();
            crac.newFlowCnec()
                .withId("cnec-" + i + "out")
                .withInstant("outage")
                .withContingency("contingency-" + i)
                .withNetworkElement("line-" + i)
                .withOptimized(true)
                .withMonitored(false)
                .newThreshold()
                .withSide(ONE)
                .withUnit(MEGAWATT)
                .withMin(0d) // so margin = flow for simplicity
                .add()
                .add();
            crac.newFlowCnec()
                .withId("cnec-" + i + "auto")
                .withInstant("auto")
                .withContingency("contingency-" + i)
                .withNetworkElement("line-" + i)
                .withOptimized(true)
                .withMonitored(false)
                .newThreshold()
                .withSide(ONE)
                .withUnit(MEGAWATT)
                .withMin(0d) // so margin = flow for simplicity
                .add()
                .add();
            crac.newFlowCnec()
                .withId("cnec-" + i + "cur")
                .withInstant("curative")
                .withContingency("contingency-" + i)
                .withNetworkElement("line-" + i)
                .withOptimized(true)
                .withMonitored(false)
                .newThreshold()
                .withSide(ONE)
                .withUnit(MEGAWATT)
                .withMin(0d) // so margin = flow for simplicity
                .add()
                .add();
        }
        crac.newPstRangeAction()
            .withId("pst")
            .withNetworkElement("pst-elt")
            .withInitialTap(0)
            .withTapToAngleConversionMap(Map.of(-1, -1., 0, 0., 1, 1.))
            .newTapRange()
            .withMinTap(-1)
            .withMaxTap(1)
            .withRangeType(RangeType.ABSOLUTE)
            .add()
            .newOnInstantUsageRule()
            .withInstant("preventive")
            .withUsageMethod(UsageMethod.AVAILABLE)
            .add()
            .add();
    }

    @BeforeEach
    public void setUp() {
        initCrac();
        preventiveInstant = crac.getInstant("preventive");
        autoInstant = crac.getInstant("auto");
        curativeInstant = crac.getInstant("curative");

        /**
         * Optimized instants:
         * -----------------------------------------------------
         * |                   * PREVENTIVE *                  |
         * -     --     -     --     ---------------------------
         * |   AUTO 1   |   AUTO 2   | * AUTO 3 * | * AUTO 4 * |
         * -     --     ---------------------------     --     -
         * |   CURA 1   | * CURA 2 * | * CURA 3 * |   CURA 4   |
         * -----------------------------------------------------
         */

        optimizationInstantsPerContingency = List.of(
            Set.of(preventiveInstant),
            Set.of(preventiveInstant, curativeInstant),
            Set.of(preventiveInstant, autoInstant, curativeInstant),
            Set.of(preventiveInstant, autoInstant));

        // only prepare results for perimeters that were optimized (like it would be in a normal run)
        // The result class will fill in the "holes".
        prepareInitialResult();
        preparePreventiveResult();
        prepareAutoResult3();
        prepareAutoResult4();
        prepareCurativeResult2();
        prepareCurativeResult3();

        StateTree stateTree = generateStateTree();

        output = new PreventiveAndCurativesRaoResultImpl(stateTree, initialResult, postPrevResult, postContingencyResults, crac, new RaoParameters());
    }

    private void prepareInitialResult() {
        initialResult = Mockito.mock(PrePerimeterResult.class);
        crac.getFlowCnecs().forEach(cnec -> {
            double flow = -1 * (FLOW_PER_INSTANT.get(cnec.getState().getInstant().getKind()) + Double.parseDouble(cnec.getId().charAt(5) + ""));
            when(initialResult.getFlow(cnec, ONE, MEGAWATT)).thenReturn(flow);
            when(initialResult.getMargin(cnec, ONE, MEGAWATT)).thenReturn(flow);
            when(initialResult.getLoopFlow(cnec, ONE, MEGAWATT)).thenThrow(new OpenRaoException("No commercial flow"));
        });
        when(initialResult.getFunctionalCost()).thenReturn(34.); //cnec4 at curative
        when(initialResult.getVirtualCost("sensitivity-failure-cost")).thenReturn(34.1);
    }

    private void preparePreventiveResult() {
        prevResult = Mockito.mock(OptimizationResult.class);
        postPrevPrePerimResult = Mockito.mock(PrePerimeterResult.class);
        postPrevResult = new PostPerimeterResult(prevResult, postPrevPrePerimResult);
        prepareResultsForState(prevResult, postPrevPrePerimResult, crac.getPreventiveState());
    }

    private void prepareAutoResult3() {
        autoResult3 = Mockito.mock(OptimizationResult.class);
        postAutoPrePerimResult3 = Mockito.mock(PrePerimeterResult.class);
        postAutoResult3 = new PostPerimeterResult(autoResult3, postAutoPrePerimResult3);
        prepareResultsForState(autoResult3, postAutoPrePerimResult3, crac.getState("contingency-3", autoInstant));
        postContingencyResults.put(crac.getState("contingency-3", autoInstant), postAutoResult3);
    }

    private void prepareAutoResult4() {
        autoResult4 = Mockito.mock(OptimizationResult.class);
        postAutoPrePerimResult4 = Mockito.mock(PrePerimeterResult.class);
        postAutoResult4 = new PostPerimeterResult(autoResult4, postAutoPrePerimResult4);
        prepareResultsForState(autoResult4, postAutoPrePerimResult4, crac.getState("contingency-4", autoInstant));
        postContingencyResults.put(crac.getState("contingency-4", autoInstant), postAutoResult4);
    }

    private void prepareCurativeResult2() {
        curativeResult2 = Mockito.mock(OptimizationResult.class);
        postCurativePrePerimResult2 = Mockito.mock(PrePerimeterResult.class);
        postCurativeResult2 = new PostPerimeterResult(curativeResult2, postCurativePrePerimResult2);
        prepareResultsForState(curativeResult2, postCurativePrePerimResult2, crac.getState("contingency-2", curativeInstant));
        postContingencyResults.put(crac.getState("contingency-2", curativeInstant), postCurativeResult2);
    }

    private void prepareCurativeResult3() {
        curativeResult3 = Mockito.mock(OptimizationResult.class);
        postCurativePrePerimResult3 = Mockito.mock(PrePerimeterResult.class);
        postCurativeResult3 = new PostPerimeterResult(curativeResult3, postCurativePrePerimResult3);
        prepareResultsForState(curativeResult3, postCurativePrePerimResult3, crac.getState("contingency-3", curativeInstant));
        postContingencyResults.put(crac.getState("contingency-3", curativeInstant), postCurativeResult3);
    }

    private void prepareResultsForState(OptimizationResult optimizationResult, PrePerimeterResult prePerimeterResult, State state) {
        AtomicReference<Double> lowestPerimeterFlow = new AtomicReference<>(Double.MAX_VALUE);
        AtomicReference<Double> lowestPostPerimeterFlow = new AtomicReference<>(Double.MAX_VALUE);
        Instant instant = state.getInstant();
        crac.getFlowCnecs().stream()
            .filter(cnec -> instant.isPreventive() || cnec.getState().getContingency().equals(state.getContingency()))
            .forEach(cnec -> {
                double signum = shouldBeSecured(cnec, instant) ? 1 : -1;
                // flow = +/- abc with
                // +/- depends of if cnec can be optimized later
                // a depends on most recent optimization (0 for init, 1 for prev, 2 for auto, 3 for cur)
                // b depends on instant of cnec
                // c depends on contingency
                double flow = signum * (
                    FLOW_PER_OPTIMIZED_INSTANT.get(getMostRecentOptimInstant(cnec, instant).getKind()) +
                    FLOW_PER_INSTANT.get(cnec.getState().getInstant().getKind()) +
                    Double.parseDouble(cnec.getId().charAt(5) + ""));
                if (isCnecOptimizedDuringInstant(cnec, instant)) {
                    addFlowAndMarginResults(optimizationResult, cnec, flow, instant);
                    lowestPerimeterFlow.set(Math.min(lowestPerimeterFlow.get(), flow));
                }
                if (!instant.comesAfter(cnec.getState().getInstant())) {
                    addFlowAndMarginResults(prePerimeterResult, cnec, flow, instant);
                    lowestPostPerimeterFlow.set(Math.min(lowestPostPerimeterFlow.get(), flow));
                }
            });
        when(optimizationResult.getFunctionalCost()).thenReturn(-lowestPerimeterFlow.get());
        when(prePerimeterResult.getFunctionalCost()).thenReturn(-lowestPostPerimeterFlow.get());
        when(optimizationResult.getVirtualCost("sensitivity-failure-cost")).thenReturn(-lowestPerimeterFlow.get() + 0.1);
        when(prePerimeterResult.getVirtualCost("sensitivity-failure-cost")).thenReturn(-lowestPostPerimeterFlow.get() + 0.1);
        if (!state.isPreventive()) {
            Set<String> contingencies = Set.of(state.getContingency().get().getId());
            when(optimizationResult.getContingencies()).thenReturn(contingencies);
        }
    }

    private void addFlowAndMarginResults(FlowResult flowResult, FlowCnec cnec, double flow, Instant instant) {
        when(flowResult.getFlow(cnec, ONE, MEGAWATT)).thenReturn(flow);
        for (Instant i : crac.getSortedInstants()) {
            if (!i.comesBefore(instant)) {
                when(flowResult.getFlow(cnec, ONE, MEGAWATT, i)).thenReturn(flow);
            }
        }
        when(flowResult.getMargin(cnec, ONE, MEGAWATT)).thenReturn(flow);
        when(flowResult.getMargin(cnec, MEGAWATT)).thenReturn(flow);
    }

    private boolean isCnecOptimizedDuringInstant(FlowCnec cnec, Instant instant) {
        return shouldBeSecured(cnec, instant) && getMostRecentOptimInstant(cnec, instant).equals(instant);
    }

    private Instant getMostRecentOptimInstant(FlowCnec cnec, Instant instant) {
        return optimizationInstantsPerContingency.get(Integer.parseInt(cnec.getId().charAt(5) + "") - 1).stream()
            .filter(i -> !i.comesAfter(instant))
            .max(Instant::compareTo)
            .orElse(null);
    }

    private boolean shouldBeSecured(FlowCnec cnec, Instant instant) {
        return optimizationInstantsPerContingency.get(Integer.parseInt(cnec.getId().charAt(5) + "") - 1).stream()
            .noneMatch(i -> i.comesAfter(instant) && !i.comesAfter(cnec.getState().getInstant()));
    }

    private StateTree generateStateTree() {
        StateTree stateTree = Mockito.mock(StateTree.class);
        Set<ContingencyScenario> contingencyScenarios = new HashSet<>();
        for (int i = 1; i <= 4; i++) {
            Set<Instant> optimizationInstants = optimizationInstantsPerContingency.get(i - 1);
            ContingencyScenario contingencyScenario = Mockito.mock(ContingencyScenario.class);
            when(contingencyScenario.getContingency()).thenReturn(crac.getContingency("contingency-" + i));
            if (optimizationInstants.contains(autoInstant)) {
                when(contingencyScenario.getAutomatonState()).thenReturn(Optional.of(crac.getState("contingency-" + i, autoInstant)));
            } else {
                when(contingencyScenario.getAutomatonState()).thenReturn(Optional.empty());
            }
            if (optimizationInstants.contains(curativeInstant)) {
                Perimeter curativePerimeter = Mockito.mock(Perimeter.class);
                when(curativePerimeter.getRaOptimisationState()).thenReturn(crac.getState("contingency-" + i, curativeInstant));
            }
            contingencyScenarios.add(contingencyScenario);
        }
        when(stateTree.getContingencyScenarios()).thenReturn(contingencyScenarios);
        return stateTree;
    }

    @Test
    public void testResult() {
        checkFunctionalCosts();
        checkVirtualCosts();
        checkFlows();
        checkOptimizationResults();
    }

    private void checkFunctionalCosts() {
        assertEquals(34., output.getFunctionalCost(null), DOUBLE_TOLERANCE);
        assertEquals(134., output.getFunctionalCost(preventiveInstant), DOUBLE_TOLERANCE);
        assertEquals(233., output.getFunctionalCost(autoInstant), DOUBLE_TOLERANCE);
        assertEquals(-111., output.getFunctionalCost(curativeInstant), DOUBLE_TOLERANCE);
    }

    private void checkVirtualCosts() {
        assertEquals(34.1, output.getVirtualCost(null), DOUBLE_TOLERANCE);
        assertEquals(134.1, output.getVirtualCost(preventiveInstant), DOUBLE_TOLERANCE);
        assertEquals(233.1, output.getVirtualCost(autoInstant), DOUBLE_TOLERANCE);
        assertEquals(0., output.getVirtualCost(curativeInstant), DOUBLE_TOLERANCE);
    }

    private void checkFlows() {
        for (FlowCnec cnec : crac.getFlowCnecs().stream().sorted(Comparator.comparing(Identifiable::getId)).toList()) {
            for (Instant instant : List.of(preventiveInstant, autoInstant, curativeInstant)) {
                if (!instant.comesAfter(cnec.getState().getInstant())) {
                    double signum = shouldBeSecured(cnec, instant) ? 1 : -1;
                    double expectedFlow = signum * (
                        FLOW_PER_OPTIMIZED_INSTANT.get(getMostRecentOptimInstant(cnec, instant).getKind()) +
                            FLOW_PER_INSTANT.get(cnec.getState().getInstant().getKind()) +
                            Double.parseDouble(cnec.getId().charAt(5) + ""));
                    try {
                        assertEquals(expectedFlow, output.getFlow(instant, cnec, ONE, MEGAWATT), DOUBLE_TOLERANCE);
                    } catch (AssertionFailedError e) {
                        System.out.println("Error for flow on " + cnec.getId() + " at " + instant);
                        throw e;
                    }
                }
            }
        }
    }

    private void checkOptimizationResults() {
        crac.getStates().stream()
            .filter(state -> !state.getInstant().isOutage())
            .forEach(state -> assertNotNull(output.getOptimizationResult(state.getInstant(), state)));
    }

    @Test
    public void testGlobalComputationStatusWhenFinalPreventiveFails() {
        when(prevResult.getComputationStatus()).thenReturn(ComputationStatus.FAILURE);
        assertEquals(ComputationStatus.FAILURE, output.getComputationStatus());
    }

    @Test
    public void testGlobalComputationStatusWhenFinalPreventivePartiallyFails() {
        when(prevResult.getComputationStatus()).thenReturn(ComputationStatus.PARTIAL_FAILURE);
        assertEquals(ComputationStatus.PARTIAL_FAILURE, output.getComputationStatus());
    }

    @Test
    public void testGlobalComputationStatusWhenAContingencyFails() {
        when(autoResult4.getComputationStatus()).thenReturn(ComputationStatus.FAILURE);
        assertEquals(ComputationStatus.PARTIAL_FAILURE, output.getComputationStatus());
    }
}