PreventiveAndCurativesRaoResultImpl.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.Contingency;
import com.powsybl.openrao.commons.OpenRaoException;
import com.powsybl.openrao.commons.PhysicalParameter;
import com.powsybl.openrao.commons.Unit;
import com.powsybl.openrao.data.crac.api.*;
import com.powsybl.openrao.data.crac.api.cnec.FlowCnec;
import com.powsybl.iidm.network.TwoSides;
import com.powsybl.openrao.data.crac.api.networkaction.NetworkAction;
import com.powsybl.openrao.data.crac.api.rangeaction.PstRangeAction;
import com.powsybl.openrao.data.crac.api.rangeaction.RangeAction;
import com.powsybl.openrao.data.crac.impl.PostContingencyState;
import com.powsybl.openrao.data.raoresult.api.ComputationStatus;
import com.powsybl.openrao.data.raoresult.api.OptimizationStepsExecuted;
import com.powsybl.openrao.raoapi.parameters.RaoParameters;
import com.powsybl.openrao.searchtreerao.castor.algorithm.Perimeter;
import com.powsybl.openrao.searchtreerao.commons.objectivefunction.ObjectiveFunction;
import com.powsybl.openrao.searchtreerao.result.api.*;
import com.powsybl.openrao.searchtreerao.castor.algorithm.StateTree;

import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BinaryOperator;
import java.util.stream.Collectors;

import static com.powsybl.openrao.data.raoresult.api.ComputationStatus.*;
import static com.powsybl.openrao.searchtreerao.commons.RaoUtil.getDuplicateCnecs;

/**
 * @author Joris Mancini {@literal <joris.mancini at rte-france.com>}
 */
public class PreventiveAndCurativesRaoResultImpl extends AbstractFlowRaoResult {
    private final State preventiveState;
    // contains the result before any optimization (PrePerimeterResult because no ActionResult)
    private final PrePerimeterResult initialResult;
    // contains the result for only the preventive and outage elements
    private final OptimizationResult preventiveAndOutageOnlyResult;
    // contains the result after first preventive, used to get results for actions excluded from second preventive
    private final PostPerimeterResult firstPreventivePerimeterResult;
    // can contain either result of second preventive or first preventive if no second was run
    private final PostPerimeterResult finalPreventivePerimeterResult;
    private final Set<RemedialAction<?>> remedialActionsExcludedFromSecondPreventive;
    private final Map<State, PostPerimeterResult> postContingencyResults;
    private final Crac crac;
    private String executionDetails = OptimizationStepsExecuted.FIRST_PREVENTIVE_ONLY;
    private final RaoParameters raoParameters;

    private final Map<Instant, Map<State, State>> optimizedStateForInstantAndState = new HashMap<>();

    /**
     * Constructor used when no post-contingency RAO has been run. Then the post-contingency results will be the
     * same as the post-preventive RAO results.
     */
    public PreventiveAndCurativesRaoResultImpl(StateTree stateTree,
                                               PrePerimeterResult initialResult,
                                               PostPerimeterResult preventivePerimeterResult,
                                               Crac crac,
                                               RaoParameters raoParameters) {
        this(stateTree, initialResult, preventivePerimeterResult, preventivePerimeterResult, new HashSet<>(), new HashMap<>(), crac, raoParameters);
    }

    /**
     * Constructor used when preventive and post-contingency RAOs have been run
     */
    public PreventiveAndCurativesRaoResultImpl(StateTree stateTree,
                                               PrePerimeterResult initialResult,
                                               PostPerimeterResult preventiveResult,
                                               Map<State, PostPerimeterResult> postContingencyResults,
                                               Crac crac,
                                               RaoParameters raoParameters) {
        this(stateTree, initialResult, preventiveResult, preventiveResult, new HashSet<>(), postContingencyResults, crac, raoParameters);
    }

    /**
     * Constructor used when preventive and post-contingency RAOs have been run, if 2 preventive RAOs were run, and 2 AUTO RAOs were run
     */
    public PreventiveAndCurativesRaoResultImpl(StateTree stateTree,
                                               PrePerimeterResult initialResult,
                                               PostPerimeterResult firstPreventivePerimeterResult,
                                               PostPerimeterResult secondPreventivePerimeterResult,
                                               Set<RemedialAction<?>> remedialActionsExcludedFromSecondPreventive,
                                               Map<State, PostPerimeterResult> postContingencyPerimeterResults,
                                               Crac crac,
                                               RaoParameters raoParameters) {
        this.preventiveState = crac.getPreventiveState();
        this.initialResult = initialResult;
        this.firstPreventivePerimeterResult = firstPreventivePerimeterResult;
        this.finalPreventivePerimeterResult = secondPreventivePerimeterResult;
        this.remedialActionsExcludedFromSecondPreventive = remedialActionsExcludedFromSecondPreventive;
        this.postContingencyResults = postContingencyPerimeterResults;
        this.crac = crac;
        this.raoParameters = raoParameters;
        this.preventiveAndOutageOnlyResult = generatePreventiveAndOutageOnlyResult();
        completePostContingencyResultsMap(stateTree);
        excludeContingencies(getContingenciesToExclude(stateTree));
        excludeDuplicateCnecs();
    }

    private OptimizationResult generatePreventiveAndOutageOnlyResult() {
        Set<FlowCnec> flowCnecs = crac.getFlowCnecs().stream()
            .filter(flowCnec -> flowCnec.getState().isPreventive() || flowCnec.getState().getInstant().getKind().equals(InstantKind.OUTAGE))
            .collect(Collectors.toSet());
        //For non loopflow cnecs, the result returns NaN or is missing commercial flows
        Set<FlowCnec> loopFlowCnecs = flowCnecs.stream()
            .filter(this::initialResultContainsLoopFlowResult)
            .collect(Collectors.toSet());
        ObjectiveFunction objectiveFunction = ObjectiveFunction.build(flowCnecs, loopFlowCnecs, initialResult, initialResult, Collections.emptySet(), raoParameters, Set.of(crac.getPreventiveState()));
        RemedialActionActivationResult remedialActionActivationResult = new RemedialActionActivationResultImpl(finalPreventivePerimeterResult.getOptimizationResult(), finalPreventivePerimeterResult.getOptimizationResult());
        ObjectiveFunctionResult objectiveFunctionResult = objectiveFunction.evaluate(finalPreventivePerimeterResult.getOptimizationResult(), remedialActionActivationResult);
        return new OptimizationResultImpl(objectiveFunctionResult, finalPreventivePerimeterResult.getOptimizationResult(), finalPreventivePerimeterResult.getOptimizationResult(), finalPreventivePerimeterResult.getOptimizationResult(), finalPreventivePerimeterResult.getOptimizationResult());
    }

    private boolean initialResultContainsLoopFlowResult(FlowCnec flowCnec) {
        boolean loopflowPresent;
        try {
            loopflowPresent = !Double.isNaN(initialResult.getLoopFlow(flowCnec, flowCnec.getMonitoredSides().iterator().next(), Unit.MEGAWATT));
        } catch (OpenRaoException e) {
            if (e.getMessage().contains("No commercial flow")) {
                loopflowPresent = false;
            } else {
                throw e;
            }
        }
        return loopflowPresent;
    }

    /**
     * Fill in results for states which were not optimized separately (either in preventive, or for states with no elements at all)
     * We go through only 2nd if statement for cases with CNECs without actions : state is defined, but no optimization was performed
     */
    private void completePostContingencyResultsMap(StateTree stateTree) {
        crac.getContingencies().forEach(contingency -> {
            crac.getSortedInstants().stream().filter(instant -> !instant.isPreventive() && !instant.isOutage()).forEach(instant -> {
                State state = crac.getState(contingency, instant);
                // States are defined in crac when there are associated cnecs or actions.
                // When no state is defined, we still want to evaluate objective functions at given contingency/instant
                if (Objects.isNull(state)) {
                    state = new PostContingencyState(contingency, instant, crac.getTimestamp().orElse(null));
                }
                if (!postContingencyResults.containsKey(state)) {
                    postContingencyResults.put(state, generateResultForUnoptimizedState(state, stateTree));
                }
            });
        });
    }

    private PostPerimeterResult generateResultForUnoptimizedState(State state, StateTree stateTree) {
        //Get previous result (either preventive if no preceding state, an optimized contingency state result, or a newly generated state result)
        PrePerimeterResult previousResult = postContingencyResults.keySet().stream()
            .filter(s -> s.getInstant().comesBefore(state.getInstant()))
            .filter(s -> s.getContingency().equals(state.getContingency()))
            .sorted(Comparator.comparing(s -> -s.getInstant().getOrder()))
            .map(s -> postContingencyResults.get(s).getPrePerimeterResultForAllFollowingStates())
            .findFirst().orElse(finalPreventivePerimeterResult.getPrePerimeterResultForAllFollowingStates());

        //compute objective function only considering that state cnecs
        Set<FlowCnec> stateCnecs = crac.getFlowCnecs(state);
        Set<FlowCnec> loopFlowCnecs = stateCnecs.stream()
            .filter(this::initialResultContainsLoopFlowResult)
            .collect(Collectors.toSet());
        RemedialActionActivationResult raActivationResult = RemedialActionActivationResultImpl.empty(previousResult);
        ObjectiveFunctionResult stateOfResult = ObjectiveFunction.build(
            stateCnecs,
            loopFlowCnecs,
            initialResult,
            previousResult,
            stateTree.getOperatorsNotSharingCras(),
            raoParameters,
            Set.of(state)
        ).evaluate(previousResult, raActivationResult);
        OptimizationResult optimizationResult = new OptimizationResultImpl(stateOfResult, previousResult, previousResult, raActivationResult, raActivationResult);

        //compute objective function considering all the cnecs from the state and following states
        Set<FlowCnec> allFollowingStatesCnecs = crac.getStates(state.getContingency().orElseThrow(() -> new OpenRaoException("State should have a contingency."))).stream()
            .filter(s -> !s.getInstant().comesBefore(state.getInstant()))
            .map(crac::getFlowCnecs)
            .reduce(new HashSet<>(), (x, y) -> {
                x.addAll(y);
                return x;
            });
        Set<FlowCnec> allFollowingStatesLoopFlowCnecs = stateCnecs.stream()
            .filter(this::initialResultContainsLoopFlowResult)
            .collect(Collectors.toSet());
        ObjectiveFunctionResult followingStatesOfResult = ObjectiveFunction.build(
            allFollowingStatesCnecs,
            allFollowingStatesLoopFlowCnecs,
            initialResult,
            previousResult,
            stateTree.getOperatorsNotSharingCras(),
            raoParameters,
            Set.of(state)
        ).evaluate(previousResult, raActivationResult);
        PrePerimeterResult prePerimeterResult = new PrePerimeterSensitivityResultImpl(previousResult, previousResult, previousResult, followingStatesOfResult);

        return new PostPerimeterResult(optimizationResult, prePerimeterResult);
    }

    private Set<String> getContingenciesToExclude(StateTree stateTree) {
        Set<String> contingenciesToExclude = new HashSet<>();
        stateTree.getContingencyScenarios().forEach(contingencyScenario -> {
            Optional<State> automatonState = contingencyScenario.getAutomatonState();
            if (automatonState.isPresent()) {
                OptimizationResult automatonResult = postContingencyResults.get(automatonState.get()).getOptimizationResult();
                if (!automatonResult.getContingencies().contains(contingencyScenario.getContingency().getId())) {
                    contingenciesToExclude.add(contingencyScenario.getContingency().getId());
                    return;
                }
            }
            for (Perimeter curativePerimeter : contingencyScenario.getCurativePerimeters()) {
                OptimizationResult curativeResult = postContingencyResults.get(curativePerimeter.getRaOptimisationState()).getOptimizationResult();
                if (!curativeResult.getContingencies().contains(contingencyScenario.getContingency().getId())) {
                    contingenciesToExclude.add(contingencyScenario.getContingency().getId());
                }
            }
        });
        return contingenciesToExclude;
    }

    private void excludeDuplicateCnecs() {
        Set<FlowCnec> flowCnecs = crac.getFlowCnecs();
        Set<String> cnecsToExclude = getDuplicateCnecs(flowCnecs);
        // exclude fictional cnec from the results
        initialResult.excludeCnecs(cnecsToExclude);
        preventiveAndOutageOnlyResult.excludeCnecs(cnecsToExclude);
        firstPreventivePerimeterResult.getOptimizationResult().excludeCnecs(cnecsToExclude);
        finalPreventivePerimeterResult.getOptimizationResult().excludeCnecs(cnecsToExclude);
        firstPreventivePerimeterResult.getPrePerimeterResultForAllFollowingStates().excludeCnecs(cnecsToExclude);
        finalPreventivePerimeterResult.getPrePerimeterResultForAllFollowingStates().excludeCnecs(cnecsToExclude);
        postContingencyResults.values().forEach(result -> {
            result.getOptimizationResult().excludeCnecs(cnecsToExclude);
            result.getPrePerimeterResultForAllFollowingStates().excludeCnecs(cnecsToExclude);
        });
    }

    private void excludeContingencies(Set<String> contingenciesToExclude) {
        initialResult.excludeContingencies(contingenciesToExclude);
        preventiveAndOutageOnlyResult.excludeContingencies(contingenciesToExclude);
        firstPreventivePerimeterResult.getOptimizationResult().excludeContingencies(contingenciesToExclude);
        finalPreventivePerimeterResult.getOptimizationResult().excludeContingencies(contingenciesToExclude);
        firstPreventivePerimeterResult.getPrePerimeterResultForAllFollowingStates().excludeContingencies(contingenciesToExclude);
        finalPreventivePerimeterResult.getPrePerimeterResultForAllFollowingStates().excludeContingencies(contingenciesToExclude);
        postContingencyResults.values().forEach(result -> {
            result.getOptimizationResult().excludeContingencies(contingenciesToExclude);
            result.getPrePerimeterResultForAllFollowingStates().excludeContingencies(contingenciesToExclude);
        });
    }

    @Override
    public ComputationStatus getComputationStatus() {
        if (initialResult.getComputationStatus() == FAILURE
            || finalPreventivePerimeterResult.getOptimizationResult().getComputationStatus() == FAILURE) {
            return FAILURE;
        }
        if (initialResult.getComputationStatus() == PARTIAL_FAILURE ||
            finalPreventivePerimeterResult.getOptimizationResult().getComputationStatus() == PARTIAL_FAILURE ||
            postContingencyResults.entrySet().stream().anyMatch(entry ->
                entry.getValue() == null || entry.getValue().getOptimizationResult().getSensitivityStatus(entry.getKey()) != DEFAULT)) {
            return PARTIAL_FAILURE;
        }
        return DEFAULT;
    }

    @Override
    public ComputationStatus getComputationStatus(State state) {
        Instant instant = state.getInstant();
        while (instant != null) {
            OptimizationResult perimeterResult = getOptimizationResult(instant, state);
            if (Objects.nonNull(perimeterResult)) {
                return perimeterResult.getComputationStatus(state);
            }
            instant = crac.getInstantBefore(instant);
        }
        return FAILURE;
    }

    public OptimizationResult getOptimizationResult(Instant optimizedInstant, State state) {
        if (optimizedInstant == null) {
            throw new OpenRaoException("No OptimizationResult for INITIAL optimization state");
        }
        if (state.getInstant().comesBefore(optimizedInstant)) {
            throw new OpenRaoException(String.format("Trying to access results for instant %s at optimization state %s is not allowed", state.getInstant(), optimizedInstant));
        }
        if (optimizedInstant.isPreventive() || optimizedInstant.isOutage()) {
            return finalPreventivePerimeterResult.getOptimizationResult();
        }
        if (optimizedInstant.isAuto()) {
            return postContingencyResults.keySet().stream()
                .filter(optimizedState -> optimizedState.getInstant().isAuto() && optimizedState.getContingency().equals(state.getContingency()))
                .findAny().map(s -> postContingencyResults.get(s).getOptimizationResult()).orElse(null);
        }
        if (optimizedInstant.isCurative()) {
            return postContingencyResults.get(state).getOptimizationResult();
        }
        throw new OpenRaoException(String.format("Optimized instant %s was not recognized", optimizedInstant));
    }

    /**
     * For a costly optimization, we want to sum the costs of the actions on all the perimeters.
     * However, for other functional costs, we are only interested in the worst margin, so we need to max the costs on all the perimeters.
     */
    @Override
    public double getFunctionalCost(Instant optimizedInstant) {
        if (optimizedInstant == null) {
            return initialResult.getFunctionalCost();
        } else if (optimizedInstant.isPreventive() || optimizedInstant.isOutage()) {
            if (raoParameters.getObjectiveFunctionParameters().getType().costOptimization()) {
                //for costly we only care about the cost of preventive actions (for after PRA result)
                return preventiveAndOutageOnlyResult.getFunctionalCost();
            } else {
                //for min margin, we care about the cost of all cnecs
                return finalPreventivePerimeterResult.getPrePerimeterResultForAllFollowingStates().getFunctionalCost();
            }
        } else {
            BinaryOperator<Double> operator;
            if (raoParameters.getObjectiveFunctionParameters().getType().costOptimization()) {
                operator = Double::sum;
            } else {
                operator = Math::max;
            }
            //initialize cost to preventive optimization cost
            AtomicReference<Double> totalCost = new AtomicReference<>(preventiveAndOutageOnlyResult.getFunctionalCost());
            //for states which come strictly before optimizedInstant, consider optimizationResult
            postContingencyResults.entrySet().stream()
                .filter(stateAndResult -> stateAndResult.getKey().getInstant().comesBefore(optimizedInstant))
                .forEach(stateAndResult -> totalCost.set(operator.apply(totalCost.get(), stateAndResult.getValue().getOptimizationResult().getFunctionalCost())));
            //for states which have same instant as optimizedInstant, consider prePerimeterResultForAllFollowingStates
            postContingencyResults.entrySet().stream()
                .filter(stateAndResult -> stateAndResult.getKey().getInstant().equals(optimizedInstant))
                .forEach(stateAndResult -> totalCost.set(operator.apply(totalCost.get(),
                    //for costly use optim result; for max min margin usel prePerim result
                    raoParameters.getObjectiveFunctionParameters().getType().costOptimization() ?
                        stateAndResult.getValue().getOptimizationResult().getFunctionalCost() :
                        stateAndResult.getValue().getPrePerimeterResultForAllFollowingStates().getFunctionalCost()))
            );

            return totalCost.get();
        }
    }

    @Override
    public double getMargin(Instant optimizedInstant, FlowCnec flowCnec, Unit unit) {
        FlowResult flowResult = getFlowResult(optimizedInstant, flowCnec);
        if (Objects.nonNull(flowResult)) {
            return flowResult.getMargin(flowCnec, unit);
        } else {
            return Double.NaN;
        }
    }

    @Override
    public double getRelativeMargin(Instant optimizedInstant, FlowCnec flowCnec, Unit unit) {
        FlowResult flowResult = getFlowResult(optimizedInstant, flowCnec);
        if (Objects.nonNull(flowResult)) {
            return flowResult.getRelativeMargin(flowCnec, unit);
        } else {
            return Double.NaN;
        }
    }

    @Override
    public double getFlow(Instant optimizedInstant, FlowCnec flowCnec, TwoSides side, Unit unit) {
        FlowResult flowResult = getFlowResult(optimizedInstant, flowCnec);
        if (Objects.nonNull(flowResult)) {
            return flowResult.getFlow(flowCnec, side, unit, optimizedInstant);
        } else {
            return Double.NaN;
        }
    }

    @Override
    public double getCommercialFlow(Instant optimizedInstant, FlowCnec flowCnec, TwoSides side, Unit unit) {
        FlowResult flowResult = getFlowResult(optimizedInstant, flowCnec);
        if (Objects.nonNull(flowResult)) {
            return flowResult.getCommercialFlow(flowCnec, side, unit);
        } else {
            return Double.NaN;
        }
    }

    @Override
    public double getLoopFlow(Instant optimizedInstant, FlowCnec flowCnec, TwoSides side, Unit unit) {
        FlowResult flowResult = getFlowResult(optimizedInstant, flowCnec);
        if (Objects.nonNull(flowResult)) {
            return flowResult.getLoopFlow(flowCnec, side, unit);
        } else {
            return Double.NaN;
        }
    }

    @Override
    public double getPtdfZonalSum(Instant optimizedInstant, FlowCnec flowCnec, TwoSides side) {
        FlowResult flowResult = getFlowResult(optimizedInstant, flowCnec);
        if (Objects.nonNull(flowResult)) {
            return flowResult.getPtdfZonalSum(flowCnec, side);
        } else {
            return Double.NaN;
        }
    }

    private FlowResult getFlowResult(Instant optimizedInstant, FlowCnec flowCnec) {
        if (optimizedInstant == null) {
            return initialResult;
        } else if (flowCnec.getState().getInstant().comesBefore(optimizedInstant)) {
            throw new OpenRaoException(String.format("Trying to access results for instant %s at optimization state %s is not allowed", flowCnec.getState().getInstant(), optimizedInstant));
        } else if (optimizedInstant.isPreventive() || optimizedInstant.isOutage()) {
            return finalPreventivePerimeterResult.getPrePerimeterResultForAllFollowingStates();
        } else {
            return postContingencyResults.get(findStateOptimizedFor(optimizedInstant, flowCnec)).getPrePerimeterResultForAllFollowingStates();
        }
    }

    private State findStateOptimizedFor(Instant optimizedInstant, FlowCnec flowCnec) {
        if (optimizedInstant.isPreventive()) {
            return null;
        }
        optimizedStateForInstantAndState.putIfAbsent(optimizedInstant, new HashMap<>());
        Map<State, State> optimizedStateForState = optimizedStateForInstantAndState.get(optimizedInstant);
        State cnecState = flowCnec.getState();
        if (optimizedStateForState.containsKey(cnecState)) {
            return optimizedStateForState.get(cnecState);
        } else {
            State optimizedState = postContingencyResults.keySet().stream().filter(state ->
                state.getInstant().equals(optimizedInstant) && state.getContingency().equals(cnecState.getContingency())
            ).findAny().orElseThrow(() -> new OpenRaoException("Contingency Results does not contain a result for every state"));
            optimizedStateForState.put(cnecState, optimizedState);
            return optimizedState;
        }
    }

    public List<FlowCnec> getMostLimitingElements() {
        //TODO : store values to be able to merge easily
        return null;
    }

    @Override
    public double getVirtualCost(Instant optimizedInstant) {
        AtomicReference<Double> s = new AtomicReference<>(0.);
        getVirtualCostNames().forEach(name -> {
            s.getAndUpdate(v -> v + this.getVirtualCost(optimizedInstant, name));
        });
        return s.get();
    }

    @Override
    public Set<String> getVirtualCostNames() {
        Set<String> virtualCostNames = new HashSet<>();
        virtualCostNames.addAll(initialResult.getVirtualCostNames());
        virtualCostNames.addAll(firstPreventivePerimeterResult.getOptimizationResult().getVirtualCostNames());
        virtualCostNames.addAll(finalPreventivePerimeterResult.getOptimizationResult().getVirtualCostNames());
        postContingencyResults.values()
            .forEach(optimizationResult -> virtualCostNames.addAll(optimizationResult.getOptimizationResult().getVirtualCostNames()));

        return virtualCostNames;
    }

    /**
     * For MNECs and Loopflows, we want to sum the costs incurred by each overload.
     * For min margin violation, we're only interested by the worst margin so we take the max of costs.
     * For sensitivity failure we just want the cost once so we also take the max.
     */
    @Override
    public double getVirtualCost(Instant optimizedInstant, String virtualCostName) {
        if (optimizedInstant == null) {
            double virtualCost = initialResult.getVirtualCost(virtualCostName);
            //The cost will be NaN for mnecs and loopflows for the initial result because we do not bother computing them because they are always 0 by definition.
            return Double.isNaN(virtualCost) ? 0 : virtualCost;
        } else if (optimizedInstant.isPreventive() || optimizedInstant.isOutage()) {
            return finalPreventivePerimeterResult.getPrePerimeterResultForAllFollowingStates().getVirtualCost(virtualCostName);
        } else {
            BinaryOperator<Double> operator;
            if (virtualCostName.equals("min-margin-violation-evaluator") || virtualCostName.equals("sensitivity-failure-cost")) {
                operator = Math::max;
            } else {
                operator = Double::sum;
            }
            //initialize cost to preventive optimization cost
            AtomicReference<Double> totalCost = new AtomicReference<>(preventiveAndOutageOnlyResult.getVirtualCost(virtualCostName));
            //for states which come strictly before optimizedInstant, consider optimizationResult
            postContingencyResults.entrySet().stream()
                .filter(stateAndResult -> stateAndResult.getKey().getInstant().comesBefore(optimizedInstant))
                .forEach(stateAndResult -> totalCost.set(operator.apply(totalCost.get(), stateAndResult.getValue().getOptimizationResult().getVirtualCost(virtualCostName))));
            //for states which have same instant as optimizedInstant, consider prePerimeterResultForAllFollowingStates
            postContingencyResults.entrySet().stream()
                .filter(stateAndResult -> stateAndResult.getKey().getInstant().equals(optimizedInstant))
                .forEach(stateAndResult -> totalCost.set(operator.apply(totalCost.get(), stateAndResult.getValue().getPrePerimeterResultForAllFollowingStates().getVirtualCost(virtualCostName))));

            return totalCost.get();
        }
    }

    @Override
    public boolean wasActivatedBeforeState(State state, NetworkAction networkAction) {
        if (state.getInstant().isPreventive()) {
            return false;
        }
        State previousState = getStateOptimizedBefore(state);
        return isActivatedDuringState(previousState, networkAction) || wasActivatedBeforeState(previousState, networkAction);
    }

    @Override
    public boolean isActivatedDuringState(State state, NetworkAction networkAction) {
        if (state.getInstant().isPreventive()) {
            return (remedialActionsExcludedFromSecondPreventive.contains(networkAction) ? firstPreventivePerimeterResult : finalPreventivePerimeterResult)
                .getOptimizationResult().getActivatedNetworkActions().contains(networkAction);
        } else if (postContingencyResults.containsKey(state)) {
            return postContingencyResults.get(state).getOptimizationResult().getActivatedNetworkActions().contains(networkAction);
        } else {
            return false;
        }
    }

    @Override
    public Set<NetworkAction> getActivatedNetworkActionsDuringState(State state) {
        if (state.getInstant().isPreventive()) {
            Set<NetworkAction> set = finalPreventivePerimeterResult.getOptimizationResult().getActivatedNetworkActions();
            firstPreventivePerimeterResult.getOptimizationResult().getActivatedNetworkActions().stream()
                .filter(remedialActionsExcludedFromSecondPreventive::contains)
                .forEach(set::add);
            return set;
        } else if (postContingencyResults.containsKey(state)) {
            return postContingencyResults.get(state).getOptimizationResult().getActivatedNetworkActions();
        } else {
            return new HashSet<>();
        }
    }

    @Override
    public boolean isActivatedDuringState(State state, RangeAction<?> rangeAction) {
        if (state.getInstant().isPreventive()) {
            return (remedialActionsExcludedFromSecondPreventive.contains(rangeAction) ? firstPreventivePerimeterResult : finalPreventivePerimeterResult)
                .getOptimizationResult().getActivatedRangeActions(state).contains(rangeAction);
        } else if (postContingencyResults.containsKey(state)) {
            return postContingencyResults.get(state).getOptimizationResult().getActivatedRangeActions(state).contains(rangeAction);
        } else {
            return false;
        }
    }

    private void throwIfNotOptimized(State state) {
        if (!postContingencyResults.containsKey(state)) {
            throw new OpenRaoException(String.format("State %s was not optimized and does not have pre-optim values", state.getId()));
        }
    }

    @Override
    public int getPreOptimizationTapOnState(State state, PstRangeAction pstRangeAction) {
        if (state.getInstant().isPreventive()) {
            return initialResult.getTap(pstRangeAction);
        }
        throwIfNotOptimized(state);
        State previousState = getStateOptimizedBefore(state);
        if (preventiveState.equals(previousState)) {
            return (remedialActionsExcludedFromSecondPreventive.contains(pstRangeAction) ? firstPreventivePerimeterResult : finalPreventivePerimeterResult)
                .getOptimizationResult().getOptimizedTap(pstRangeAction, preventiveState);
        } else {
            return postContingencyResults.get(previousState).getOptimizationResult().getOptimizedTap(pstRangeAction, previousState);
        }
    }

    @Override
    public int getOptimizedTapOnState(State state, PstRangeAction pstRangeAction) {
        if (state.getInstant().isPreventive() || !postContingencyResults.containsKey(state)) {
            return (remedialActionsExcludedFromSecondPreventive.contains(pstRangeAction) ? firstPreventivePerimeterResult : finalPreventivePerimeterResult)
                .getOptimizationResult().getOptimizedTap(pstRangeAction, state);
        } else {
            return postContingencyResults.get(state).getOptimizationResult().getOptimizedTap(pstRangeAction, state);
        }
    }

    @Override
    public double getPreOptimizationSetPointOnState(State state, RangeAction<?> rangeAction) {
        if (state.getInstant().isPreventive()) {
            return initialResult.getSetpoint(rangeAction);
        }
        throwIfNotOptimized(state);
        State previousState = getStateOptimizedBefore(state);
        if (preventiveState.equals(previousState)) {
            return (remedialActionsExcludedFromSecondPreventive.contains(rangeAction) ? firstPreventivePerimeterResult : finalPreventivePerimeterResult)
                .getOptimizationResult().getOptimizedSetpoint(rangeAction, preventiveState);
        } else {
            return postContingencyResults.get(previousState).getOptimizationResult().getOptimizedSetpoint(rangeAction, previousState);
        }
    }

    @Override
    public double getOptimizedSetPointOnState(State state, RangeAction<?> rangeAction) {
        if (state.getInstant().isPreventive() || !postContingencyResults.containsKey(state)) {
            return (remedialActionsExcludedFromSecondPreventive.contains(rangeAction) ? firstPreventivePerimeterResult : finalPreventivePerimeterResult)
                .getOptimizationResult().getOptimizedSetpoint(rangeAction, state);
        } else {
            return postContingencyResults.get(state).getOptimizationResult().getOptimizedSetpoint(rangeAction, state);
        }
    }

    @Override
    public Set<RangeAction<?>> getActivatedRangeActionsDuringState(State state) {
        if (state.getInstant().isPreventive()) {
            Set<RangeAction<?>> set = finalPreventivePerimeterResult.getOptimizationResult().getActivatedRangeActions(state);
            firstPreventivePerimeterResult.getOptimizationResult().getActivatedRangeActions(state).stream()
                .filter(remedialActionsExcludedFromSecondPreventive::contains)
                .forEach(set::add);
            return set;
        } else if (postContingencyResults.containsKey(state)) {
            return postContingencyResults.get(state).getOptimizationResult().getActivatedRangeActions(state);
        } else {
            return new HashSet<>();
        }
    }

    @Override
    public Map<PstRangeAction, Integer> getOptimizedTapsOnState(State state) {
        if (state.getInstant().isPreventive() || !postContingencyResults.containsKey(state)) {
            Map<PstRangeAction, Integer> map = new HashMap<>(finalPreventivePerimeterResult.getOptimizationResult().getOptimizedTapsOnState(state));
            firstPreventivePerimeterResult.getOptimizationResult().getOptimizedTapsOnState(state).entrySet().stream()
                .filter(entry -> remedialActionsExcludedFromSecondPreventive.contains(entry.getKey()))
                .forEach(entry -> map.put(entry.getKey(), entry.getValue()));
            return map;
        } else {
            return postContingencyResults.get(state).getOptimizationResult().getOptimizedTapsOnState(state);
        }
    }

    @Override
    public Map<RangeAction<?>, Double> getOptimizedSetPointsOnState(State state) {
        if (state.getInstant().isPreventive() || !postContingencyResults.containsKey(state)) {
            Map<RangeAction<?>, Double> map = new HashMap<>(finalPreventivePerimeterResult.getOptimizationResult().getOptimizedSetpointsOnState(state));
            firstPreventivePerimeterResult.getOptimizationResult().getOptimizedSetpointsOnState(state).entrySet().stream()
                .filter(entry -> remedialActionsExcludedFromSecondPreventive.contains(entry.getKey()))
                .forEach(entry -> map.put(entry.getKey(), entry.getValue()));
            return map;
        } else {
            return postContingencyResults.get(state).getOptimizationResult().getOptimizedSetpointsOnState(state);
        }
    }

    private State getStateOptimizedBefore(State state) {
        if (state.getInstant().isPreventive()) {
            throw new OpenRaoException("No state before preventive.");
        } else if (state.getInstant().isOutage() || state.getInstant().isAuto()) {
            return preventiveState;
        } else {
            // curative
            Contingency contingency = state.getContingency().orElseThrow();
            return postContingencyResults.keySet().stream()
                .filter(mapState -> mapState.getContingency().equals(Optional.of(contingency)))
                .filter(mapState -> mapState.getInstant().isAuto() || mapState.getInstant().isCurative())
                .filter(mapState -> mapState.getInstant().comesBefore(state.getInstant()))
                .max(Comparator.comparingInt(mapState -> mapState.getInstant().getOrder()))
                .orElse(preventiveState);
        }
    }

    @Override
    public void setExecutionDetails(String executionDetails) {
        this.executionDetails = executionDetails;
    }

    @Override
    public boolean isSecure(PhysicalParameter... u) {
        return isSecure(crac.getLastInstant(), u);
    }

    @Override
    public String getExecutionDetails() {
        return executionDetails;
    }
}