CastorSecondPreventive.java

/*
 * Copyright (c) 2024, 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.castor.algorithm;

import com.powsybl.iidm.network.Network;
import com.powsybl.openrao.commons.OpenRaoException;
import com.powsybl.openrao.commons.RandomizedString;
import com.powsybl.openrao.data.crac.api.Crac;
import com.powsybl.openrao.data.crac.api.Instant;
import com.powsybl.openrao.data.crac.api.InstantKind;
import com.powsybl.openrao.data.crac.api.NetworkElement;
import com.powsybl.openrao.data.crac.api.RaUsageLimits;
import com.powsybl.openrao.data.crac.api.RemedialAction;
import com.powsybl.openrao.data.crac.api.State;
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.api.rangeaction.StandardRangeAction;
import com.powsybl.openrao.data.raoresult.api.ComputationStatus;
import com.powsybl.openrao.data.raoresult.api.RaoResult;
import com.powsybl.openrao.raoapi.parameters.ObjectiveFunctionParameters;
import com.powsybl.openrao.raoapi.parameters.RaoParameters;
import com.powsybl.openrao.raoapi.parameters.extensions.SecondPreventiveRaoParameters;
import com.powsybl.openrao.searchtreerao.commons.NetworkActionCombination;
import com.powsybl.openrao.searchtreerao.commons.RaoLogger;
import com.powsybl.openrao.searchtreerao.commons.ToolProvider;
import com.powsybl.openrao.searchtreerao.commons.objectivefunction.ObjectiveFunction;
import com.powsybl.openrao.searchtreerao.commons.optimizationperimeters.*;
import com.powsybl.openrao.searchtreerao.commons.parameters.TreeParameters;
import com.powsybl.openrao.searchtreerao.commons.parameters.UnoptimizedCnecParameters;
import com.powsybl.openrao.searchtreerao.result.api.*;
import com.powsybl.openrao.searchtreerao.result.impl.*;
import com.powsybl.openrao.searchtreerao.searchtree.algorithms.SearchTree;
import com.powsybl.openrao.searchtreerao.searchtree.inputs.SearchTreeInput;
import com.powsybl.openrao.searchtreerao.searchtree.parameters.SearchTreeParameters;
import com.powsybl.openrao.sensitivityanalysis.AppliedRemedialActions;

import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;

import static com.powsybl.openrao.commons.logs.OpenRaoLoggerProvider.*;
import static com.powsybl.openrao.data.crac.api.range.RangeType.RELATIVE_TO_PREVIOUS_INSTANT;
import static com.powsybl.openrao.data.raoresult.api.ComputationStatus.FAILURE;
import static com.powsybl.openrao.raoapi.parameters.extensions.LoadFlowAndSensitivityParameters.getSensitivityFailureOvercost;
import static com.powsybl.openrao.raoapi.parameters.extensions.SearchTreeRaoObjectiveFunctionParameters.getCurativeMinObjImprovement;
import static com.powsybl.openrao.raoapi.parameters.extensions.SecondPreventiveRaoParameters.*;

/**
 * @author Joris Mancini {@literal <joris.mancini at rte-france.com>}
 * @author Philippe Edwards {@literal <philippe.edwards at rte-france.com>}
 * @author Peter Mitri {@literal <peter.mitri at rte-france.com>}
 * @author Godelaine de Montmorillon {@literal <godelaine.demontmorillon at rte-france.com>}
 * @author Baptiste Seguinot {@literal <baptiste.seguinot at rte-france.com>}
 */
public class CastorSecondPreventive {
    private final Crac crac;
    private final RaoParameters raoParameters;
    private final Network network;
    private final StateTree stateTree;
    private final ToolProvider toolProvider;
    private final java.time.Instant targetEndInstant;

    private static final String SECOND_PREVENTIVE_SCENARIO_BEFORE_OPT = "SecondPreventiveScenario";
    private static final int NUMBER_LOGGED_ELEMENTS_DURING_RAO = 2;
    private static final int NUMBER_LOGGED_ELEMENTS_END_RAO = 10;

    public CastorSecondPreventive(Crac crac,
                                  RaoParameters raoParameters,
                                  Network network,
                                  StateTree stateTree,
                                  ToolProvider toolProvider,
                                  java.time.Instant targetEndInstant) {
        this.crac = crac;
        this.raoParameters = raoParameters;
        this.network = network;
        this.stateTree = stateTree;
        this.toolProvider = toolProvider;
        this.targetEndInstant = targetEndInstant;
    }

    /**
     * This function decides if a 2nd preventive RAO should be run. It checks the user parameter first, then takes the
     * decision depending on the curative RAO results and the curative RAO stop criterion.
     */
    boolean shouldRunSecondPreventiveRao(OptimizationResult firstPreventiveResult, Collection<OptimizationResult> curativeRaoResults, RaoResult postFirstRaoResult, long estimatedPreventiveRaoTimeInSeconds) {
        Instant lastCurativeInstant = crac.getLastInstant();
        if (getSecondPreventiveExecutionCondition(raoParameters).equals(SecondPreventiveRaoParameters.ExecutionCondition.DISABLED)) {
            return false;
        }
        if (!Objects.isNull(targetEndInstant) && ChronoUnit.SECONDS.between(java.time.Instant.now(), targetEndInstant) < estimatedPreventiveRaoTimeInSeconds) {
            BUSINESS_LOGS.info("There is not enough time to run a 2nd preventive RAO (target end time: {}, estimated time needed based on first preventive RAO: {} seconds)", targetEndInstant, estimatedPreventiveRaoTimeInSeconds);
            return false;
        }
        if (getSecondPreventiveExecutionCondition(raoParameters).equals(SecondPreventiveRaoParameters.ExecutionCondition.COST_INCREASE)
            && postFirstRaoResult.getCost(lastCurativeInstant) <= postFirstRaoResult.getCost(null)) {
            BUSINESS_LOGS.info("Cost has not increased during RAO, there is no need to run a 2nd preventive RAO.");
            // it is not necessary to compare initial & post-preventive costs since the preventive RAO cannot increase its own cost
            // only compare initial cost with the curative costs
            return false;
        }
        ObjectiveFunctionParameters.ObjectiveFunctionType objectiveFunctionType = raoParameters.getObjectiveFunctionParameters().getType();
        if (objectiveFunctionType.equals(ObjectiveFunctionParameters.ObjectiveFunctionType.SECURE_FLOW)) {
            if (firstPreventiveResult.getCost() > 0) {
                // in case of curative optimization even if preventive unsecure (see parameter enforce-curative-security)
                // we do not want to run a second preventive that would not be able to fix the situation, to save time
                BUSINESS_LOGS.info("First preventive RAO was not able to fix all preventive constraints, second preventive RAO cancelled to save computation time.");
                return false;
            }
            // Run 2nd preventive RAO if one perimeter of the curative optimization is unsecure
            return isAnyResultUnsecure(curativeRaoResults);
        } else {  // MIN OBJECTIVE
            // Run 2nd preventive RAO if the final result has a worse cost than the preventive perimeter
            return isFinalCostWorseThanPreventive(getCurativeMinObjImprovement(raoParameters), firstPreventiveResult, postFirstRaoResult, lastCurativeInstant);
        }
    }

    /**
     * Returns true if any result has a positive functional cost
     */
    private static boolean isAnyResultUnsecure(Collection<OptimizationResult> results) {
        return results.stream().anyMatch(optimizationResult -> optimizationResult.getFunctionalCost() >= 0 || optimizationResult.getVirtualCost() > 1e-6);
    }

    /**
     * Returns true if final cost (after PRAO + ARAO + CRAO) is worse than the cost at the end of the preventive perimeter
     */
    private static boolean isFinalCostWorseThanPreventive(double curativeMinObjImprovement, OptimizationResult preventiveResult, RaoResult postFirstRaoResult, Instant curativeInstant) {
        return postFirstRaoResult.getCost(curativeInstant) > preventiveResult.getCost() - curativeMinObjImprovement;
    }

    RaoResult runSecondPreventiveAndAutoRao(CastorContingencyScenarios castorContingencyScenarios,
                                            PrePerimeterSensitivityAnalysis prePerimeterSensitivityAnalysis,
                                            PrePerimeterResult initialOutput,
                                            OptimizationResult firstPreventiveResult,
                                            Map<State, OptimizationResult> postContingencyResults) {
        // Run 2nd preventive RAO
        SecondPreventiveRaoResult secondPreventiveRaoResult;
        try {
            secondPreventiveRaoResult = runSecondPreventiveRao(prePerimeterSensitivityAnalysis, initialOutput, firstPreventiveResult, postContingencyResults);
            if (secondPreventiveRaoResult.postPraSensitivityAnalysisOutput.getSensitivityStatus() == ComputationStatus.FAILURE) {
                return new FailedRaoResultImpl("Post-PRA sensitivity analysis failed during 2nd preventive RAO");
            }
        } catch (OpenRaoException e) {
            BUSINESS_LOGS.error(e.getMessage());
            return new FailedRaoResultImpl(String.format("RAO failed during second preventive : %s", e.getMessage()));
        }

        // Run 2nd automaton simulation and update results
        BUSINESS_LOGS.info("----- Second automaton simulation [start]");
        Map<State, OptimizationResult> newPostContingencyResults = castorContingencyScenarios.optimizeContingencyScenarios(network, secondPreventiveRaoResult.postPraSensitivityAnalysisOutput, true);
        BUSINESS_LOGS.info("----- Second automaton simulation [end]");

        BUSINESS_LOGS.info("Merging first, second preventive and post-contingency RAO results:");
        // Always re-run curative sensitivity analysis (re-run is necessary in several specific cases)
        // -- Gather all post contingency remedial actions
        // ---- Curative remedial actions :
        // ------ appliedCras from secondPreventiveRaoResult
        AppliedRemedialActions appliedArasAndCras = secondPreventiveRaoResult.appliedArasAndCras().copyCurative();
        // ------ + curative range actions optimized during second preventive with global optimization
        if (getSecondPreventiveReOptimizeCurativeRangeActions(raoParameters)) {
            for (Map.Entry<State, OptimizationResult> entry : postContingencyResults.entrySet()) {
                State state = entry.getKey();
                if (!state.getInstant().isCurative()) {
                    continue;
                }
                secondPreventiveRaoResult.perimeterResult().getActivatedRangeActions(state)
                    .forEach(rangeAction -> appliedArasAndCras.addAppliedRangeAction(state, rangeAction, secondPreventiveRaoResult.perimeterResult.getOptimizedSetpoint(rangeAction, state)));
            }
        }
        // ---- Auto remedial actions : computed during second auto, saved in newPostContingencyResults
        // ---- only RAs from perimeters that haven't failed are included in appliedArasAndCras
        // ---- this check is only performed here because SkippedOptimizationResultImpl with appliedRas can only be generated for AUTO instant
        newPostContingencyResults.entrySet().stream().filter(entry ->
                !(entry.getValue() instanceof SkippedOptimizationResultImpl) && entry.getKey().getInstant().isAuto())
            .forEach(entry -> {
                appliedArasAndCras.addAppliedNetworkActions(entry.getKey(), entry.getValue().getActivatedNetworkActions());
                entry.getValue().getActivatedRangeActions(entry.getKey()).forEach(rangeAction -> appliedArasAndCras.addAppliedRangeAction(entry.getKey(), rangeAction, entry.getValue().getOptimizedSetpoint(rangeAction, entry.getKey())));
            });
        // Run curative sensitivity analysis with appliedArasAndCras
        // TODO: this is too slow, we can replace it with load-flow computations or security analysis since we don't need sensitivity values
        PrePerimeterResult postCraSensitivityAnalysisOutput = prePerimeterSensitivityAnalysis.runBasedOnInitialResults(network, crac, initialOutput, Collections.emptySet(), appliedArasAndCras);
        if (postCraSensitivityAnalysisOutput.getSensitivityStatus() == ComputationStatus.FAILURE) {
            BUSINESS_LOGS.error("Systematic sensitivity analysis after curative remedial actions after second preventive optimization failed");
            return new FailedRaoResultImpl("Systematic sensitivity analysis after curative remedial actions after second preventive optimization failed");
        }
        for (Map.Entry<State, OptimizationResult> entry : postContingencyResults.entrySet()) {
            State state = entry.getKey();
            if (!state.getInstant().isCurative()) {
                continue;
            }
            // Specific case : curative state was previously skipped because it led to a sensitivity analysis failure.
            // Curative state is still a SkippedOptimizationResultImpl, but its computation status must be updated
            if (entry.getValue() instanceof SkippedOptimizationResultImpl) {
                newPostContingencyResults.put(state, new SkippedOptimizationResultImpl(state, new HashSet<>(), new HashSet<>(), postCraSensitivityAnalysisOutput.getSensitivityStatus(entry.getKey()), getSensitivityFailureOvercost(raoParameters)));
            } else {
                newPostContingencyResults.put(state, new CurativeWithSecondPraoResult(state, entry.getValue(), secondPreventiveRaoResult.perimeterResult(), secondPreventiveRaoResult.remedialActionsExcluded(), postCraSensitivityAnalysisOutput, raoParameters.getObjectiveFunctionParameters().getType().costOptimization()));
            }
        }
        RaoLogger.logMostLimitingElementsResults(BUSINESS_LOGS, postCraSensitivityAnalysisOutput, raoParameters.getObjectiveFunctionParameters().getType(), raoParameters.getObjectiveFunctionParameters().getUnit(), NUMBER_LOGGED_ELEMENTS_END_RAO);
        RaoLogger.checkIfMostLimitingElementIsFictional(BUSINESS_LOGS, postCraSensitivityAnalysisOutput);

        return new PreventiveAndCurativesRaoResultImpl(stateTree,
            initialOutput,
            firstPreventiveResult,
            secondPreventiveRaoResult.perimeterResult(),
            secondPreventiveRaoResult.remedialActionsExcluded(),
            secondPreventiveRaoResult.postPraSensitivityAnalysisOutput(),
            newPostContingencyResults,
            postCraSensitivityAnalysisOutput,
            crac,
            raoParameters.getObjectiveFunctionParameters());
    }

    private record SecondPreventiveRaoResult(OptimizationResult perimeterResult,
                                             PrePerimeterResult postPraSensitivityAnalysisOutput,
                                             Set<RemedialAction<?>> remedialActionsExcluded,
                                             AppliedRemedialActions appliedArasAndCras) {
    }

    /**
     * Main function to run 2nd preventive RAO
     * Using 1st preventive and curative results, it ets up network and range action contexts, then calls the optimizer
     * It finally merges the three results into one RaoResult object
     */
    private SecondPreventiveRaoResult runSecondPreventiveRao(PrePerimeterSensitivityAnalysis prePerimeterSensitivityAnalysis,
                                                             PrePerimeterResult initialOutput,
                                                             OptimizationResult firstPreventiveResult,
                                                             Map<State, OptimizationResult> postContingencyResults) {
        // Go back to the initial state of the network, saved in the SECOND_PREVENTIVE_STATE variant
        network.getVariantManager().setWorkingVariant(SECOND_PREVENTIVE_SCENARIO_BEFORE_OPT);

        // Get the applied network actions for every contingency perimeter
        AppliedRemedialActions appliedArasAndCras = new AppliedRemedialActions();
        if (crac.hasAutoInstant()) {
            addAppliedNetworkActionsPostContingency(crac.getInstants(InstantKind.AUTO), appliedArasAndCras, postContingencyResults);
        }
        addAppliedNetworkActionsPostContingency(crac.getInstants(InstantKind.CURATIVE), appliedArasAndCras, postContingencyResults);
        // Get the applied range actions for every auto contingency perimeter
        if (crac.hasAutoInstant()) {
            addAppliedRangeActionsPostContingency(crac.getInstants(InstantKind.AUTO), appliedArasAndCras, postContingencyResults);
        }

        // Apply 1st preventive results for range actions that are both preventive and auto or curative. This way we are sure
        // that the optimal setpoints of the curative results stay coherent with their allowed range and close to
        // optimality in their perimeters. These range actions will be excluded from 2nd preventive RAO.
        Set<RemedialAction<?>> remedialActionsExcluded = new HashSet<>();
        if (!getSecondPreventiveReOptimizeCurativeRangeActions(raoParameters)) { // keep old behaviour
            remedialActionsExcluded = new HashSet<>(getRangeActionsExcludedFromSecondPreventive(firstPreventiveResult, postContingencyResults));
            applyPreventiveResultsForAutoOrCurativeRangeActions(firstPreventiveResult);
            addAppliedRangeActionsPostContingency(crac.getInstants(InstantKind.CURATIVE), appliedArasAndCras, postContingencyResults);
        }

        // Run a first sensitivity computation using initial network and applied CRAs
        // If any sensitivity computation fails, fail and fall back to 1st preventive result
        // TODO: can we / do we want to improve this behavior by excluding the failed contingencies?
        PrePerimeterResult sensiWithPostContingencyRemedialActions = prePerimeterSensitivityAnalysis.runBasedOnInitialResults(network, crac, initialOutput, stateTree.getOperatorsNotSharingCras(), appliedArasAndCras);
        if (sensiWithPostContingencyRemedialActions.getSensitivityStatus() == FAILURE) {
            throw new OpenRaoException("Systematic sensitivity analysis after curative remedial actions before second preventive optimization failed");
        }
        RaoLogger.logSensitivityAnalysisResults("Systematic sensitivity analysis after curative remedial actions before second preventive optimization: ",
            prePerimeterSensitivityAnalysis.getObjectiveFunction(),
            new RemedialActionActivationResultImpl(new RangeActionActivationResultImpl(RangeActionSetpointResultImpl.buildWithSetpointsFromNetwork(network, crac.getRangeActions())), new NetworkActionsResultImpl(getAllAppliedNetworkAraAndCra(appliedArasAndCras))),
            sensiWithPostContingencyRemedialActions,
            raoParameters,
            NUMBER_LOGGED_ELEMENTS_DURING_RAO);

        // Run second preventive RAO
        BUSINESS_LOGS.info("----- Second preventive perimeter optimization [start]");
        String newVariant = RandomizedString.getRandomizedString("SecondPreventive", network.getVariantManager().getVariantIds(), 10);
        network.getVariantManager().cloneVariant(SECOND_PREVENTIVE_SCENARIO_BEFORE_OPT, newVariant, true);
        network.getVariantManager().setWorkingVariant(newVariant);
        OptimizationResult secondPreventiveResult = optimizeSecondPreventivePerimeter(initialOutput, sensiWithPostContingencyRemedialActions, firstPreventiveResult, postContingencyResults, appliedArasAndCras)
            .join().getOptimizationResult(crac.getPreventiveState());
        // Re-run sensitivity computation based on PRAs without CRAs, to access after PRA results
        PrePerimeterResult postPraSensitivityAnalysisOutput = prePerimeterSensitivityAnalysis.runBasedOnInitialResults(network, crac, initialOutput, stateTree.getOperatorsNotSharingCras(), null);
        if (postPraSensitivityAnalysisOutput.getSensitivityStatus() == ComputationStatus.FAILURE) {
            BUSINESS_LOGS.error("Systematic sensitivity analysis after preventive remedial actions after second preventive optimization failed");
        }
        BUSINESS_LOGS.info("----- Second preventive perimeter optimization [end]");
        return new SecondPreventiveRaoResult(secondPreventiveResult, postPraSensitivityAnalysisOutput, remedialActionsExcluded, appliedArasAndCras);
    }

    void addAppliedNetworkActionsPostContingency(Set<Instant> instants, AppliedRemedialActions appliedRemedialActions, Map<State, OptimizationResult> postContingencyResults) {
        instants.forEach(instant ->
            postContingencyResults.forEach((state, optimizationResult) -> {
                if (state.getInstant().equals(instant)) {
                    appliedRemedialActions.addAppliedNetworkActions(state, optimizationResult.getActivatedNetworkActions());
                }
            })
        );
    }

    void addAppliedRangeActionsPostContingency(Set<Instant> instants, AppliedRemedialActions appliedRemedialActions, Map<State, OptimizationResult> postContingencyResults) {
        // Add all range actions that were activated.
        // Curative/ preventive duplicates are handled via exclusion from 2nd preventive
        instants.forEach(instant ->
            postContingencyResults.forEach((state, optimizationResult) -> {
                if (state.getInstant().equals(instant)) {
                    optimizationResult.getActivatedRangeActions(state).forEach(rangeAction -> appliedRemedialActions.addAppliedRangeAction(state, rangeAction, optimizationResult.getOptimizedSetpoint(rangeAction, state)));
                }
            })
        );
    }

    private Map<State, Set<NetworkAction>> getAllAppliedNetworkAraAndCra(AppliedRemedialActions appliedArasAndCras) {
        Map<State, Set<NetworkAction>> appliedNetworkActions = new HashMap<>();
        crac.getStates().stream().filter(state -> state.getInstant().isAuto() || state.getInstant().isCurative())
            .forEach(state -> appliedNetworkActions.put(state, appliedArasAndCras.getAppliedNetworkActions(state)));
        return appliedNetworkActions;
    }

    private CompletableFuture<OneStateOnlyRaoResultImpl> optimizeSecondPreventivePerimeter(PrePerimeterResult initialOutput,
                                                                                           PrePerimeterResult prePerimeterResult,
                                                                                           OptimizationResult firstPreventiveResult,
                                                                                           Map<State, OptimizationResult> postContingencyResults,
                                                                                           AppliedRemedialActions appliedCras) {

        OptimizationPerimeter optPerimeter;
        Instant preventiveInstant = crac.getPreventiveInstant();
        State preventiveState = crac.getPreventiveState();
        Set<RangeAction<?>> excludedRangeActions = getRangeActionsExcludedFromSecondPreventive(firstPreventiveResult, postContingencyResults);

        if (getSecondPreventiveReOptimizeCurativeRangeActions(raoParameters)) {
            optPerimeter = GlobalOptimizationPerimeter.build(crac, network, raoParameters, prePerimeterResult);
        } else {
            Set<RangeAction<?>> rangeActionsFor2p = new HashSet<>(crac.getRangeActions());
            excludedRangeActions.forEach(rangeAction -> {
                BUSINESS_WARNS.warn("Range action {} will not be considered in 2nd preventive RAO as it is also auto/curative (or its network element has an associated ARA/CRA)", rangeAction.getId());
                rangeActionsFor2p.remove(rangeAction);
            });
            optPerimeter = PreventiveOptimizationPerimeter.buildWithAllCnecs(crac, rangeActionsFor2p, network, raoParameters, prePerimeterResult);
        }

        SearchTreeParameters searchTreeParameters = SearchTreeParameters.create()
            .withConstantParametersOverAllRao(raoParameters, crac)
            .withTreeParameters(TreeParameters.buildForSecondPreventivePerimeter(raoParameters))
            .withUnoptimizedCnecParameters(UnoptimizedCnecParameters.build(raoParameters.getNotOptimizedCnecsParameters(), stateTree.getOperatorsNotSharingCras()))
            .build();

        // update RaUsageLimits with already applied RangeActions
        if (!getSecondPreventiveReOptimizeCurativeRangeActions(raoParameters) && searchTreeParameters.getRaLimitationParameters().containsKey(preventiveInstant)) {
            Set<RangeAction<?>> activatedPreventiveRangeActions = firstPreventiveResult.getActivatedRangeActions(preventiveState);
            Set<RangeAction<?>> excludedActivatedRangeActions = excludedRangeActions.stream().filter(activatedPreventiveRangeActions::contains).collect(Collectors.toSet());
            searchTreeParameters.setRaLimitationsForSecondPreventive(searchTreeParameters.getRaLimitationParameters().get(preventiveInstant), excludedActivatedRangeActions, preventiveInstant);
        }

        if (getSecondPreventiveHintFromFirstPreventiveRao(raoParameters)) {
            // Set the optimal set of network actions decided in 1st preventive RAO as a hint for 2nd preventive RAO
            searchTreeParameters.getNetworkActionParameters().addNetworkActionCombination(new NetworkActionCombination(firstPreventiveResult.getActivatedNetworkActions(), true));
        }

        Set<State> statesToOptimize = new HashSet<>(optPerimeter.getMonitoredStates());
        statesToOptimize.add(optPerimeter.getMainOptimizationState());

        SearchTreeInput searchTreeInput = SearchTreeInput.create()
            .withNetwork(network)
            .withOptimizationPerimeter(optPerimeter)
            .withInitialFlowResult(initialOutput)
            .withPrePerimeterResult(prePerimeterResult)
            .withPreOptimizationAppliedNetworkActions(appliedCras) //no remedial Action applied
            .withObjectiveFunction(ObjectiveFunction.build(optPerimeter.getFlowCnecs(), optPerimeter.getLoopFlowCnecs(), initialOutput, prePerimeterResult, new HashSet<>(), raoParameters, statesToOptimize))
            .withToolProvider(toolProvider)
            .withOutageInstant(crac.getOutageInstant())
            .build();

        OptimizationResult result = new SearchTree(searchTreeInput, searchTreeParameters, true).run().join();

        // apply PRAs
        network.getVariantManager().setWorkingVariant(SECOND_PREVENTIVE_SCENARIO_BEFORE_OPT);
        result.getActivatedRangeActions(preventiveState).forEach(rangeAction -> rangeAction.apply(network, result.getOptimizedSetpoint(rangeAction, preventiveState)));
        result.getActivatedNetworkActions().forEach(networkAction -> networkAction.apply(network));

        return CompletableFuture.completedFuture(new OneStateOnlyRaoResultImpl(preventiveState, prePerimeterResult, result, optPerimeter.getFlowCnecs()));
    }

    /**
     * This method applies range action results on the network, for range actions that are auto or curative
     * It is used for second preventive optimization along with 1st preventive results in order to keep the result
     * of 1st preventive for range actions that are both preventive and auto or curative
     */
    void applyPreventiveResultsForAutoOrCurativeRangeActions(OptimizationResult preventiveResult) {
        preventiveResult.getActivatedRangeActions(crac.getPreventiveState()).stream()
            .filter(crac::isRangeActionAutoOrCurative)
            .forEach(rangeAction -> rangeAction.apply(network, preventiveResult.getOptimizedSetpoint(rangeAction, crac.getPreventiveState())));
    }

    /**
     * Returns the set of range actions that are excluded from the 2nd preventive RAO.
     * The concerned range actions meet certain criterion.
     * 1- The RA has a range limit relative to the previous instant.
     * This way we avoid incoherence between preventive & curative tap positions.
     * 2- For the remaining RAs we are going to remove some for the reason explained below.
     * Let's consider a rangeAction that has the same tap in preventive and in another state.
     * If so, considering it in the second preventive optimization could change its tap for preventive only.
     * Therefore, the RA would no longer have the same taps in preventive and for the given contingency state: It's consider used for the given state.
     * That could lead the RAO to wrongly exceed the RaUsageLimits for the given state.
     * To avoid this, we don't want to optimize these RAs.
     * For the same reason, we are going to check preventive RAs that share the same network elements as auto or curative RAs.
     */
    Set<RangeAction<?>> getRangeActionsExcludedFromSecondPreventive(OptimizationResult firstPreventiveResult, Map<State, OptimizationResult> contingencyResults) {

        // Excludes every non-preventive RA.
        Set<RangeAction<?>> nonPreventiveRangeActions = crac.getRangeActions().stream().filter(ra -> !crac.isRangeActionPreventive(ra)).collect(Collectors.toSet());
        Set<RangeAction<?>> rangeActionsToExclude = new HashSet<>(nonPreventiveRangeActions);

        // Gathers PRAs that are also ARA/CRAs.
        Set<RangeAction<?>> multipleInstantRangeActions = crac.getRangeActions().stream()
            .filter(ra -> crac.isRangeActionPreventive(ra) && crac.isRangeActionAutoOrCurative(ra))
            .collect(Collectors.toSet());

        // Excludes the ones that have a range limit relative to the previous instant.
        multipleInstantRangeActions.stream().filter(CastorSecondPreventive::raHasRelativeToPreviousInstantRange).forEach(rangeActionsToExclude::add);
        rangeActionsToExclude.forEach(multipleInstantRangeActions::remove);

        // We look for PRAs that share the same network element as ARA/CRAs as the same rules apply to them.
        Map<RangeAction<?>, Set<RangeAction<?>>> correspondanceMap = new HashMap<>();
        crac.getRangeActions().stream().filter(ra -> crac.isRangeActionPreventive(ra) && !crac.isRangeActionAutoOrCurative(ra)).forEach(pra -> {
            Set<NetworkElement> praNetworkElements = pra.getNetworkElements();
            for (RangeAction<?> cra : nonPreventiveRangeActions) {
                if (cra.getNetworkElements().equals(praNetworkElements)) {
                    if (raHasRelativeToPreviousInstantRange(cra)) {
                        // Excludes PRAs which share the same network element as an ARA/CRA with a range limit relative to the previous instant.
                        rangeActionsToExclude.add(pra);
                        correspondanceMap.remove(pra);
                        break;
                    } else {
                        // Gathers PRAs with their associated ARA/CRAs inside a map.
                        correspondanceMap.putIfAbsent(pra, new HashSet<>());
                        correspondanceMap.get(pra).add(cra);
                    }
                }
            }
        });

        // If first preventive diverged, we want to remove every range action that is both preventive and auto or curative.
        if (firstPreventiveResult instanceof SkippedOptimizationResultImpl) {
            multipleInstantRangeActions.addAll(correspondanceMap.keySet());
            return multipleInstantRangeActions;
        }

        // Excludes RAs that put crac RaUsageLimits at risk.
        // First, we filter out state that diverged because we know no set-point was chosen for this state.
        Map<State, OptimizationResult> newContingencyResults = new HashMap<>(contingencyResults);
        newContingencyResults.entrySet().removeIf(entry -> entry.getValue() instanceof SkippedOptimizationResultImpl);

        // Then, we build a map that gives for each RA, its tap at each state it's available at.
        State preventiveState = crac.getPreventiveState();
        Map<State, Map<RangeAction<?>, Double>> setPointResults = buildSetPointResultsMap(crac, firstPreventiveResult, newContingencyResults, correspondanceMap, multipleInstantRangeActions, preventiveState);

        // Finally, we filter out RAs that put crac RaUsageLimits at risk.
        rangeActionsToExclude.addAll(getRangeActionsToRemove(crac, preventiveState, setPointResults, newContingencyResults));
        return rangeActionsToExclude;
    }

    /**
     * Creates a map that gives for a given state, each available RA with its tap.
     * The only subtlety being that RAs sharing exactly the same network elements are considered to be only one RA.
     */
    private static Map<State, Map<RangeAction<?>, Double>> buildSetPointResultsMap(Crac crac, OptimizationResult firstPreventiveResult, Map<State, OptimizationResult> contingencyResults, Map<RangeAction<?>, Set<RangeAction<?>>> correspondanceMap, Set<RangeAction<?>> multipleInstantRangeActions, State preventiveState) {
        Map<State, Map<RangeAction<?>, Double>> setPointResults = new HashMap<>(Map.of(preventiveState, new HashMap<>()));
        correspondanceMap.forEach((pra, associatedCras) -> {
            setPointResults.get(preventiveState).put(pra, firstPreventiveResult.getOptimizedSetpoint(pra, preventiveState));
            associatedCras.forEach(cra -> contingencyResults.forEach((state, result) -> {
                if (crac.isRangeActionAvailableInState(cra, state) && result.getComputationStatus() != FAILURE) {
                    setPointResults.putIfAbsent(state, new HashMap<>());
                    setPointResults.get(state).put(pra, result.getOptimizedSetpoint(cra, state));
                }
            }));
        });
        multipleInstantRangeActions.forEach(ra -> {
            setPointResults.get(preventiveState).put(ra, firstPreventiveResult.getOptimizedSetpoint(ra, preventiveState));
            contingencyResults.forEach((state, result) -> {
                if (crac.isRangeActionAvailableInState(ra, state) && result.getComputationStatus() != FAILURE) {
                    setPointResults.putIfAbsent(state, new HashMap<>());
                    setPointResults.get(state).put(ra, result.getOptimizedSetpoint(ra, state));
                }
            });
        });
        return setPointResults;
    }

    /**
     * Checks if raUsageLimits are at risk if we choose to re-optimize a range action.
     * Returns True if it's at risk, False otherwise.
     */
    private static boolean shouldRemoveRaDueToUsageLimits(String operator, RaUsageLimits raUsageLimits, Set<RangeAction<?>> activatableRangeActions, Set<NetworkAction> activatedNetworkActions) {
        if (operator == null) {
            return raUsageLimits.getMaxRa() < activatableRangeActions.size() + activatedNetworkActions.size();
        }

        Set<RemedialAction<?>> activatableRemedialActions = new HashSet<>(activatableRangeActions);
        activatableRemedialActions.addAll(activatedNetworkActions);

        long activatableRangeActionsForTheTso = activatableRangeActions.stream().filter(ra -> operator.equals(ra.getOperator())).count();
        long activatableRemedialActionsForTheTso = activatableRemedialActions.stream().filter(ra -> operator.equals(ra.getOperator())).count();
        long activatableTsos = activatableRemedialActions.stream().map(RemedialAction::getOperator).filter(Objects::nonNull).distinct().count();

        int limitingRangeActionValueForTheTso = raUsageLimits.getMaxPstPerTso().getOrDefault(operator, Integer.MAX_VALUE);
        int limitingRemedialActionValueForTheTso = raUsageLimits.getMaxRaPerTso().getOrDefault(operator, Integer.MAX_VALUE);

        return raUsageLimits.getMaxRa() < activatableRangeActions.size() + activatedNetworkActions.size()
            || limitingRangeActionValueForTheTso < activatableRangeActionsForTheTso
            || limitingRemedialActionValueForTheTso < activatableRemedialActionsForTheTso
            || raUsageLimits.getMaxTso() < activatableTsos;
    }

    /**
     * Gathers every range action that should not be considered in the second preventive if those 2 criterion are met :
     * 1- The range action has the same tap in preventive and in a contingency scenario.
     * 2- For the given state, the crac has limiting RaUsageLimits.
     */
    private static Set<RangeAction<?>> getRangeActionsToRemove(Crac crac, State preventiveState, Map<State, Map<RangeAction<?>, Double>> setPointResults, Map<State, OptimizationResult> contingencyResults) {
        Set<RangeAction<?>> rangeActionsToRemove = new HashSet<>();
        setPointResults.forEach((state, spMap) -> {
            if (!state.isPreventive()) {
                Set<RangeAction<?>> activatableRangeActions = crac.getPotentiallyAvailableRangeActions(state);
                Set<NetworkAction> activatedNetworkActions = contingencyResults.get(state).getActivatedNetworkActions();
                spMap.forEach((ra, setPoint) -> {
                    if (setPoint.equals(setPointResults.get(preventiveState).get(ra))
                        && crac.getRaUsageLimitsPerInstant().containsKey(state.getInstant())
                        && shouldRemoveRaDueToUsageLimits(ra.getOperator(), crac.getRaUsageLimits(state.getInstant()), activatableRangeActions, activatedNetworkActions)) {
                        rangeActionsToRemove.add(ra);
                    }
                });
            }
        });
        return rangeActionsToRemove;
    }

    /**
     * Returns True if the rangeAction has a RELATIVE_TO_PREVIOUS_INSTANT range. Else, returns False.
     */
    private static boolean raHasRelativeToPreviousInstantRange(RangeAction<?> rangeAction) {
        if (rangeAction instanceof PstRangeAction pstRangeAction) {
            return pstRangeAction.getRanges().stream().anyMatch(tapRange -> tapRange.getRangeType().equals(RELATIVE_TO_PREVIOUS_INSTANT));
        }
        return ((StandardRangeAction<?>) rangeAction).getRanges().stream().anyMatch(standardRange -> standardRange.getRangeType().equals(RELATIVE_TO_PREVIOUS_INSTANT));
    }
}