AbstractSecurityAnalysis.java

/*
 * Copyright (c) 2020-2025, 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/.
 * SPDX-License-Identifier: MPL-2.0
 */
package com.powsybl.openloadflow.sa;

import com.google.common.base.Stopwatch;
import com.powsybl.action.*;
import com.powsybl.commons.PowsyblException;
import com.powsybl.commons.report.ReportNode;
import com.powsybl.computation.CompletableFutureTask;
import com.powsybl.computation.ComputationManager;
import com.powsybl.contingency.ContingenciesProvider;
import com.powsybl.contingency.Contingency;
import com.powsybl.iidm.network.*;
import com.powsybl.loadflow.LoadFlowParameters;
import com.powsybl.loadflow.LoadFlowResult;
import com.powsybl.math.matrix.MatrixFactory;
import com.powsybl.openloadflow.OpenLoadFlowParameters;
import com.powsybl.openloadflow.equations.Quantity;
import com.powsybl.openloadflow.graph.GraphConnectivityFactory;
import com.powsybl.openloadflow.lf.AbstractLoadFlowParameters;
import com.powsybl.openloadflow.lf.LoadFlowContext;
import com.powsybl.openloadflow.lf.LoadFlowEngine;
import com.powsybl.openloadflow.network.*;
import com.powsybl.openloadflow.network.action.LfAction;
import com.powsybl.openloadflow.network.action.LfActionUtils;
import com.powsybl.openloadflow.network.impl.*;
import com.powsybl.openloadflow.sa.extensions.ContingencyLoadFlowParameters;
import com.powsybl.openloadflow.util.Lists2;
import com.powsybl.openloadflow.util.PerUnit;
import com.powsybl.openloadflow.util.Reports;
import com.powsybl.security.*;
import com.powsybl.security.condition.AllViolationCondition;
import com.powsybl.security.condition.AnyViolationCondition;
import com.powsybl.security.condition.AtLeastOneViolationCondition;
import com.powsybl.security.condition.TrueCondition;
import com.powsybl.security.limitreduction.LimitReduction;
import com.powsybl.security.monitor.StateMonitor;
import com.powsybl.security.monitor.StateMonitorIndex;
import com.powsybl.security.results.*;
import com.powsybl.security.strategy.ConditionalActions;
import com.powsybl.security.strategy.OperatorStrategy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.event.Level;

import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * @author Geoffroy Jamgotchian {@literal <geoffroy.jamgotchian at rte-france.com>}
 */
public abstract class AbstractSecurityAnalysis<V extends Enum<V> & Quantity, E extends Enum<E> & Quantity,
                                               P extends AbstractLoadFlowParameters<P>,
                                               C extends LoadFlowContext<V, E, P>,
                                               R extends com.powsybl.openloadflow.lf.LoadFlowResult> {

    protected static final Logger LOGGER = LoggerFactory.getLogger(AbstractSecurityAnalysis.class);

    protected final Network network;

    protected final MatrixFactory matrixFactory;

    protected final GraphConnectivityFactory<LfBus, LfBranch> connectivityFactory;

    protected final StateMonitorIndex monitorIndex;

    protected final ReportNode reportNode;

    private static final String NOT_FOUND = "' not found in the network";

    protected Level logLevel = Level.INFO; // level of the post contingency and action logs

    protected AbstractSecurityAnalysis(Network network, MatrixFactory matrixFactory, GraphConnectivityFactory<LfBus, LfBranch> connectivityFactory,
                                       List<StateMonitor> stateMonitors, ReportNode reportNode) {
        this.network = Objects.requireNonNull(network);
        this.matrixFactory = Objects.requireNonNull(matrixFactory);
        this.connectivityFactory = Objects.requireNonNull(connectivityFactory);
        this.monitorIndex = new StateMonitorIndex(stateMonitors);
        this.reportNode = Objects.requireNonNull(reportNode);
    }

    protected abstract LoadFlowModel getLoadFlowModel();

    protected static SecurityAnalysisResult createNoResult() {
        return new SecurityAnalysisResult(new LimitViolationsResult(Collections.emptyList()), LoadFlowResult.ComponentResult.Status.FAILED, Collections.emptyList());
    }

    public CompletableFuture<SecurityAnalysisReport> run(String workingVariantId, SecurityAnalysisParameters securityAnalysisParameters,
                                                         ContingenciesProvider contingenciesProvider, ComputationManager computationManager,
                                                         List<OperatorStrategy> operatorStrategies, List<Action> actions, List<LimitReduction> limitReductions) {
        Objects.requireNonNull(workingVariantId);
        Objects.requireNonNull(securityAnalysisParameters);
        Objects.requireNonNull(contingenciesProvider);
        return CompletableFutureTask.runAsync(() -> runSync(securityAnalysisParameters, contingenciesProvider, operatorStrategies, actions, limitReductions, workingVariantId, computationManager.getExecutor()), computationManager.getExecutor());
    }

    protected abstract ReportNode createSaRootReportNode();

    protected abstract boolean isShuntCompensatorVoltageControlOn(LoadFlowParameters lfParameters);

    protected abstract P createParameters(LoadFlowParameters lfParameters, OpenLoadFlowParameters lfParametersExt, boolean breakers, boolean areas);

    SecurityAnalysisReport runSync(SecurityAnalysisParameters securityAnalysisParameters, ContingenciesProvider contingenciesProvider,
                                   List<OperatorStrategy> operatorStrategies, List<Action> actions, List<LimitReduction> limitReductions,
                                   String workingVariantId, Executor executor) throws ExecutionException {
        var saReportNode = createSaRootReportNode();

        Stopwatch stopwatch = Stopwatch.createStarted();

        LoadFlowParameters lfParameters = securityAnalysisParameters.getLoadFlowParameters();
        OpenLoadFlowParameters lfParametersExt = OpenLoadFlowParameters.get(securityAnalysisParameters.getLoadFlowParameters());
        OpenSecurityAnalysisParameters securityAnalysisParametersExt = OpenSecurityAnalysisParameters.getOrDefault(securityAnalysisParameters);

        network.getVariantManager().setWorkingVariant(workingVariantId);

        // load contingencies
        List<Contingency> contingencies = contingenciesProvider.getContingencies(network);

        LOGGER.info("Running {} security analysis on {} contingencies on {} threads",
                getLoadFlowModel() == LoadFlowModel.AC ? "AC" : "DC", contingencies.size(), securityAnalysisParametersExt.getThreadCount());

        // check actions validity
        checkActions(network, actions);

        // try for find all switches to be operated as actions.
        LfTopoConfig topoConfig = new LfTopoConfig();
        findAllSwitchesToOperate(network, actions, topoConfig);

        // try to find all ptc and rtc to retain because involved in ptc and rtc actions
        findAllPtcToOperate(actions, topoConfig);
        findAllRtcToOperate(actions, topoConfig);
        // try to find all shunts which section can change through actions.
        findAllShuntsToOperate(actions, topoConfig);

        // try to find branches (lines and two windings transformers).
        // tie lines and three windings transformers missing.
        findAllBranchesToClose(network, actions, topoConfig);

        // try to find all switches impacted by at least one contingency and for each contingency the branches impacted
        PropagatedContingencyCreationParameters creationParameters = new PropagatedContingencyCreationParameters()
                .setContingencyPropagation(securityAnalysisParametersExt.isContingencyPropagation())
                .setShuntCompensatorVoltageControlOn(isShuntCompensatorVoltageControlOn(lfParameters))
                .setSlackDistributionOnConformLoad(lfParameters.getBalanceType() == LoadFlowParameters.BalanceType.PROPORTIONAL_TO_CONFORM_LOAD)
                .setHvdcAcEmulation(lfParameters.isHvdcAcEmulation());

        SecurityAnalysisResult finalResult;

        if (securityAnalysisParametersExt.getThreadCount() == 1) {
            List<PropagatedContingency> propagatedContingencies = PropagatedContingency.createList(network, contingencies, topoConfig, creationParameters);

            var parameters = createParameters(lfParameters, lfParametersExt, topoConfig.isBreaker(), isAreaInterchangeControl(lfParametersExt, contingencies));

            // create networks including all necessary switches
            try (LfNetworkList lfNetworks = Networks.load(network, parameters.getNetworkParameters(), topoConfig, saReportNode)) {
                finalResult = runSimulationsOnAllComponents(lfNetworks, propagatedContingencies, parameters,
                        securityAnalysisParameters, operatorStrategies, actions, limitReductions, lfParameters);
            }

        } else {
            var contingenciesPartitions = Lists2.partition(contingencies, securityAnalysisParametersExt.getThreadCount());

            // Check now that every operator strategy references an existing contingency. It will be impossible to do after
            // contingencies are split per partition.
            final Set<String> contingencyIds = contingencies.stream().map(Contingency::getId).collect(Collectors.toSet());
            operatorStrategies.stream()
                    .filter(o -> !hasValidContingency(o, contingencyIds))
                    .findAny()
                    .ifPresent(AbstractSecurityAnalysis::throwMissingOperatorStrategyContingency);
            // Check action ids to report exception to the main thread
            final Set<String> actionIds = actions.stream().map(Action::getId).collect(Collectors.toSet());
            operatorStrategies
                    .forEach(o -> findMissingActionId(o, actionIds)
                            .ifPresent(id -> throwMissingOperatorStrategyAction(o, id)));

            // we pre-allocate the results so that threads can set result in a stable order (using the partition number)
            // so that we always get results in the same order whatever threads completion order is.
            List<SecurityAnalysisResult> partitionResults = Collections.synchronizedList(new ArrayList<>(Collections.nCopies(contingenciesPartitions.size(), createNoResult()))); // init to no result in case of cancel
            List<LfNetworkList> lfNetworksList = new ArrayList<>();
            List<ReportNode> reportNodes = Collections.synchronizedList(new ArrayList<>(Collections.nCopies(contingenciesPartitions.size(), ReportNode.NO_OP)));

            boolean oldAllowVariantMultiThreadAccess = network.getVariantManager().isVariantMultiThreadAccessAllowed();
            network.getVariantManager().allowVariantMultiThreadAccess(true);
            try {
                Lock networkLock = new ReentrantLock();
                List<CompletableFuture<Void>> futures = new ArrayList<>();
                for (int i = 0; i < contingenciesPartitions.size(); i++) {
                    final int partitionNum = i;
                    var contingenciesPartition = contingenciesPartitions.get(i);
                    futures.add(CompletableFutureTask.runAsync(() -> {

                        var partitionTopoConfig = new LfTopoConfig(topoConfig);

                        //  we have to pay attention with IIDM network multi threading even when allowVariantMultiThreadAccess is set:
                        //    - variant cloning and removal is not thread safe
                        //    - we cannot read or write on an exising variant while another thread clone or remove a variant
                        //    - be aware that even after LF network loading, though LF network we get access to original IIDM
                        //      variant (for instance to get reactive capability curve), so allowVariantMultiThreadAccess mode
                        //      is absolutely required
                        //  so in order to be thread safe, we need to:
                        //    - lock LF network creation (which create a working variant, see {@code LfNetworkList})
                        //    - delay {@code LfNetworkList} closing (which remove a working variant) out of worker thread
                        LfNetworkList lfNetworks;
                        List<PropagatedContingency> propagatedContingencies;
                        P parameters;
                        networkLock.lock();
                        try {
                            network.getVariantManager().setWorkingVariant(workingVariantId);

                            propagatedContingencies = PropagatedContingency.createList(network, contingenciesPartition, partitionTopoConfig, creationParameters);

                            parameters = createParameters(lfParameters, lfParametersExt, partitionTopoConfig.isBreaker(), isAreaInterchangeControl(lfParametersExt, contingencies));

                            ReportNode threadRootNode = partitionNum == 0 ? saReportNode : Reports.createRootThreadReport(saReportNode);
                            reportNodes.set(partitionNum, threadRootNode);

                            // create networks including all necessary switches
                            lfNetworks = Networks.load(network, parameters.getNetworkParameters(), partitionTopoConfig, threadRootNode);
                            lfNetworksList.add(0, lfNetworks); // FIXME to workaround variant removal bug, to fix in core
                        } finally {
                            networkLock.unlock();
                        }

                        // run simulation on largest network
                        partitionResults.set(partitionNum, runSimulationsOnAllComponents(
                                lfNetworks, propagatedContingencies, parameters, securityAnalysisParameters, operatorStrategies,
                                actions, limitReductions, lfParameters));

                        return null;
                    }, executor));
                }

                try {
                    CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
                            .get(); // we need to use get instead of join to get an interruption exception
                } catch (InterruptedException e) {
                    // also interrupt worker threads
                    for (var future : futures) {
                        future.cancel(true);
                    }
                    Thread.currentThread().interrupt();
                }
            } finally {
                network.getVariantManager().allowVariantMultiThreadAccess(oldAllowVariantMultiThreadAccess);
            }

            int networkRank = 0;
            for (var lfNetworks : lfNetworksList) {
                if (networkRank != 0) {
                    mergeReportThreadResults(saReportNode, reportNodes.get(networkRank));
                }
                lfNetworks.close();
                networkRank += 1;
            }

            // we just need to merge post contingency and operator strategy results, all pre contingency are the same
            List<PostContingencyResult> postContingencyResults = new ArrayList<>();
            List<OperatorStrategyResult> operatorStrategyResults = new ArrayList<>();
            for (var partitionResult : partitionResults) {
                postContingencyResults.addAll(partitionResult.getPostContingencyResults());
                operatorStrategyResults.addAll(partitionResult.getOperatorStrategyResults());
            }
            finalResult = new SecurityAnalysisResult(partitionResults.get(0).getPreContingencyResult(), postContingencyResults, operatorStrategyResults);
        }

        stopwatch.stop();
        LOGGER.info("Security analysis {} in {} ms", Thread.currentThread().isInterrupted() ? "cancelled" : "done",
                stopwatch.elapsed(TimeUnit.MILLISECONDS));

        return new SecurityAnalysisReport(finalResult);
    }

    private record LfNetworkId(Object numCC, Object numSC) {
    }

    private void mergeReportThreadResults(ReportNode mainReport, ReportNode toMerge) {

        Map<LfNetworkId, ReportNode> mainNodes = mainReport.getChildren().stream()
                .filter(r -> r.getMessageKey().equals(Reports.LF_NETWORK_KEY))
                .collect(Collectors.toMap(
                        n -> new LfNetworkId(n.getValue(Reports.NETWORK_NUM_CC).orElseThrow().getValue(),
                                                       n.getValue(Reports.NETWORK_NUM_SC).orElseThrow().getValue()),
                                  n -> n));

        Map<LfNetworkId, ReportNode> toMergeNodes = toMerge.getChildren().stream()
                .filter(r -> r.getMessageKey().equals(Reports.LF_NETWORK_KEY))
                .collect(Collectors.toMap(
                        n -> new LfNetworkId(n.getValue(Reports.NETWORK_NUM_CC).orElseThrow().getValue(),
                                n.getValue(Reports.NETWORK_NUM_SC).orElseThrow().getValue()),
                        n -> n));

        // By construction all threads should have the same lfNetwork List
        // So the merge is just about appending relevant data to lfNetwork nodes of the
        // main thread

        for (Map.Entry<LfNetworkId, ReportNode> entry : mainNodes.entrySet()) {
            // Both should exist
            ReportNode mainReportNode = entry.getValue();
            ReportNode toMergeNode = toMergeNodes.get(entry.getKey());
            toMergeNode.getChildren().stream()
                    .filter(n -> n.getMessageKey().equals(Reports.POST_CONTINGENCY_SIMULATION_KEY))
                    .forEach(mainReportNode::addCopy);
        }
    }

    SecurityAnalysisResult runSimulationsOnAllComponents(LfNetworkList networks, List<PropagatedContingency> propagatedContingencies, P parameters,
                                                         SecurityAnalysisParameters securityAnalysisParameters, List<OperatorStrategy> operatorStrategies,
                                                         List<Action> actions, List<LimitReduction> limitReductions,
                                                         LoadFlowParameters lfParameters) {

        List<LfNetwork> networkToSimulate = new ArrayList<>(getNetworksToSimulate(networks, lfParameters.getConnectedComponentMode()));
        OpenSecurityAnalysisParameters openSecurityAnalysisParameters = OpenSecurityAnalysisParameters.getOrDefault(securityAnalysisParameters);
        ContingencyActivePowerLossDistribution contingencyActivePowerLossDistribution = ContingencyActivePowerLossDistribution.find(openSecurityAnalysisParameters.getContingencyActivePowerLossDistribution());

        if (networkToSimulate.isEmpty()) {
            return createNoResult();
        }

        // run simulation on first lfNetwork to initialize results structures
        LfNetwork firstNetwork = networkToSimulate.remove(0);
        SecurityAnalysisResult result = runSimulations(firstNetwork, propagatedContingencies, parameters, securityAnalysisParameters,
                operatorStrategies, actions, limitReductions, contingencyActivePowerLossDistribution);

        List<PostContingencyResult> postContingencyResults = result.getPostContingencyResults();
        List<OperatorStrategyResult> operatorStrategyResults = result.getOperatorStrategyResults();
        NetworkResult mergedPreContingencyNetworkResult = result.getPreContingencyResult().getNetworkResult();
        List<LimitViolation> preContingencyViolations = result.getPreContingencyResult().getLimitViolationsResult().getLimitViolations();

        Map<String, PostContingencyResult> postContingencyResultMap = new LinkedHashMap<>();
        Map<String, OperatorStrategyResult> operatorStrategyResultMap = new LinkedHashMap<>();
        postContingencyResults.forEach(r -> postContingencyResultMap.put(r.getContingency().getId(), r));
        operatorStrategyResults.forEach(r -> operatorStrategyResultMap.put(r.getOperatorStrategy().getId(), r));

        // Ensure the lists are writable and can be extended
        preContingencyViolations = new ArrayList<>(preContingencyViolations);

        for (LfNetwork n : networkToSimulate) {
            SecurityAnalysisResult resultOtherComponent = runSimulations(n, propagatedContingencies, parameters, securityAnalysisParameters,
                    operatorStrategies, actions, limitReductions, contingencyActivePowerLossDistribution);

            // Merge into first result
            // PreContingency results first
            preContingencyViolations.addAll(resultOtherComponent.getPreContingencyResult().getLimitViolationsResult().getLimitViolations());
            mergedPreContingencyNetworkResult = mergeNetworkResult(mergedPreContingencyNetworkResult, resultOtherComponent.getPreContingencyResult().getNetworkResult());

            // PostContingency and OperatorStrategies results
            mergeSecurityAnalysisResult(resultOtherComponent, postContingencyResultMap, operatorStrategyResultMap, n.getNumCC());
        }
        postContingencyResults = postContingencyResultMap.values().stream().toList();
        operatorStrategyResults = operatorStrategyResultMap.values().stream().toList();

        PreContingencyResult mergedPrecontingencyResult =
            new PreContingencyResult(result.getPreContingencyResult().getStatus(),
                new LimitViolationsResult(preContingencyViolations),
                mergedPreContingencyNetworkResult);
        return new SecurityAnalysisResult(mergedPrecontingencyResult, postContingencyResults, operatorStrategyResults);
    }

    static List<LfNetwork> getNetworksToSimulate(LfNetworkList networks, LoadFlowParameters.ConnectedComponentMode mode) {

        if (LoadFlowParameters.ConnectedComponentMode.MAIN.equals(mode)) {
            return networks.getList().stream()
                .filter(n -> n.getNumCC() == ComponentConstants.MAIN_NUM && n.getValidity().equals(LfNetwork.Validity.VALID)).toList();
        } else if (LoadFlowParameters.ConnectedComponentMode.ALL.equals(mode)) {
            return networks.getList().stream()
                .filter(n -> n.getValidity().equals(LfNetwork.Validity.VALID)).toList();
        } else {
            throw new PowsyblException("Unsupported ConnectedComponentMode " + mode);
        }
    }

    void mergeSecurityAnalysisResult(SecurityAnalysisResult resultToMerge, Map<String, PostContingencyResult> postContingencyResults,
                                     Map<String, OperatorStrategyResult> operatorStrategyResults, int connectedComponentNum) {
        resultToMerge.getPostContingencyResults().forEach(postContingencyResult -> {
            String contingencyId = postContingencyResult.getContingency().getId();
            PostContingencyResult originalResult = postContingencyResults.get(contingencyId);

            if (originalResult != null) {
                warnDifferentStatus(originalResult.getStatus(), postContingencyResult.getStatus(), connectedComponentNum, "post contingency", postContingencyResult.getContingency().getId());
                NetworkResult mergedNetworkResult = mergeNetworkResult(originalResult.getNetworkResult(), postContingencyResult.getNetworkResult());
                List<LimitViolation> violations = new ArrayList<>(postContingencyResult.getLimitViolationsResult().getLimitViolations());
                violations.addAll(originalResult.getLimitViolationsResult().getLimitViolations());

                PostContingencyResult mergedPostContingencyResult =
                        new PostContingencyResult(originalResult.getContingency(), originalResult.getStatus(),
                                new LimitViolationsResult(violations), mergedNetworkResult, originalResult.getConnectivityResult());

                postContingencyResults.put(contingencyId, mergedPostContingencyResult);
            } else {
                postContingencyResults.put(contingencyId, postContingencyResult);
            }
        });

        resultToMerge.getOperatorStrategyResults().forEach(operatorStrategyResult -> {
            String strategyId = operatorStrategyResult.getOperatorStrategy().getId();
            OperatorStrategyResult originalResult = operatorStrategyResults.get(strategyId);
            if (originalResult != null) {
                List<OperatorStrategyResult.ConditionalActionsResult> conditionalActionsResults = new ArrayList<>();

                operatorStrategyResult.getConditionalActionsResults().forEach(conditionalActionsResult -> {
                    Optional<OperatorStrategyResult.ConditionalActionsResult> originalRes = originalResult.getConditionalActionsResults().stream()
                            .filter(originalConditionalActionResult -> originalConditionalActionResult.getConditionalActionsId().equals(conditionalActionsResult.getConditionalActionsId()))
                            .findAny();
                    if (originalRes.isPresent()) {
                        warnDifferentStatus(originalRes.get().getStatus(), conditionalActionsResult.getStatus(), connectedComponentNum, "conditional actions", conditionalActionsResult.getConditionalActionsId());
                        NetworkResult mergedNetworkResult = mergeNetworkResult(originalRes.get().getNetworkResult(), conditionalActionsResult.getNetworkResult());
                        List<LimitViolation> violations = new ArrayList<>(conditionalActionsResult.getLimitViolationsResult().getLimitViolations());
                        violations.addAll(originalResult.getLimitViolationsResult().getLimitViolations());

                        OperatorStrategyResult.ConditionalActionsResult mergedConditionalActionResult
                                = new OperatorStrategyResult.ConditionalActionsResult(conditionalActionsResult.getConditionalActionsId(),
                                conditionalActionsResult.getStatus(), new LimitViolationsResult(violations), mergedNetworkResult);
                        conditionalActionsResults.add(mergedConditionalActionResult);

                    } else {
                        conditionalActionsResults.add(conditionalActionsResult);
                    }
                });
                operatorStrategyResults.put(strategyId, new OperatorStrategyResult(originalResult.getOperatorStrategy(), conditionalActionsResults));
            } else {
                operatorStrategyResults.put(strategyId, operatorStrategyResult);
            }
        });
    }

    void warnDifferentStatus(PostContingencyComputationStatus mainStatus, PostContingencyComputationStatus subComponentStatus, int subComponentNum, String stage, String stageId) {
        if (mainStatus != subComponentStatus) {
            LOGGER.warn("Component {} {} {} result being merged has status {} while main connected component has status {}." +
                    " Status of component {} will not be represented in the output.",
                subComponentNum, stage, stageId, subComponentStatus, mainStatus, subComponentNum);
        }
    }

    private static <T> ArrayList<T> ensureMutable(List<T> orig) {
        return orig instanceof ArrayList<T> arrayList ? arrayList : new ArrayList<>(orig);
    }

    static NetworkResult mergeNetworkResult(NetworkResult source, NetworkResult target) {
        // Copy the lists if they are not writable
        ArrayList<BranchResult> branchResults = ensureMutable(source.getBranchResults());
        ArrayList<ThreeWindingsTransformerResult> twtResults = ensureMutable(source.getThreeWindingsTransformerResults());
        ArrayList<BusResult> busResults = ensureMutable(source.getBusResults());
        branchResults.addAll(target.getBranchResults());
        twtResults.addAll(target.getThreeWindingsTransformerResults());
        busResults.addAll(target.getBusResults());
        return new NetworkResult(branchResults, busResults, twtResults);
    }

    protected abstract PostContingencyComputationStatus postContingencyStatusFromLoadFlowResult(R result);

    protected static void checkActions(Network network, List<Action> actions) {
        for (Action action : actions) {
            switch (action.getType()) {
                case SwitchAction.NAME: {
                    SwitchAction switchAction = (SwitchAction) action;
                    if (network.getSwitch(switchAction.getSwitchId()) == null) {
                        throw new PowsyblException("Switch '" + switchAction.getSwitchId() + NOT_FOUND);
                    }
                    break;
                }

                case TerminalsConnectionAction.NAME: {
                    TerminalsConnectionAction terminalsConnectionAction = (TerminalsConnectionAction) action;
                    if (network.getBranch(terminalsConnectionAction.getElementId()) == null) {
                        throw new PowsyblException("Branch '" + terminalsConnectionAction.getElementId() + NOT_FOUND);
                    }
                    break;
                }

                case PhaseTapChangerTapPositionAction.NAME,
                     RatioTapChangerTapPositionAction.NAME: {
                    String transformerId = action.getType().equals(PhaseTapChangerTapPositionAction.NAME) ?
                            ((PhaseTapChangerTapPositionAction) action).getTransformerId() : ((RatioTapChangerTapPositionAction) action).getTransformerId();
                    if (network.getTwoWindingsTransformer(transformerId) == null
                            && network.getThreeWindingsTransformer(transformerId) == null) {
                        throw new PowsyblException("Transformer '" + transformerId + NOT_FOUND);
                    }
                    break;
                }

                case LoadAction.NAME: {
                    LoadAction loadAction = (LoadAction) action;
                    if (network.getLoad(loadAction.getLoadId()) == null) {
                        throw new PowsyblException("Load '" + loadAction.getLoadId() + NOT_FOUND);
                    }
                    break;
                }

                case GeneratorAction.NAME: {
                    GeneratorAction generatorAction = (GeneratorAction) action;
                    if (network.getGenerator(generatorAction.getGeneratorId()) == null) {
                        throw new PowsyblException("Generator '" + generatorAction.getGeneratorId() + NOT_FOUND);
                    }
                    break;
                }

                case HvdcAction.NAME: {
                    HvdcAction hvdcAction = (HvdcAction) action;
                    if (network.getHvdcLine(hvdcAction.getHvdcId()) == null) {
                        throw new PowsyblException("Hvdc line '" + hvdcAction.getHvdcId() + NOT_FOUND);
                    }
                    break;
                }

                case ShuntCompensatorPositionAction.NAME: {
                    ShuntCompensatorPositionAction shuntCompensatorPositionAction = (ShuntCompensatorPositionAction) action;
                    if (network.getShuntCompensator(shuntCompensatorPositionAction.getShuntCompensatorId()) == null) {
                        throw new PowsyblException("Shunt compensator '" + shuntCompensatorPositionAction.getShuntCompensatorId() + "' not found");
                    }
                    break;
                }

                case AreaInterchangeTargetAction.NAME: {
                    AreaInterchangeTargetAction areaInterchangeAction = (AreaInterchangeTargetAction) action;
                    if (network.getArea(areaInterchangeAction.getAreaId()) == null) {
                        throw new PowsyblException("Area '" + areaInterchangeAction.getAreaId() + "' not found");
                    }
                    break;
                }

                default:
                    throw new UnsupportedOperationException("Unsupported action type: " + action.getType());
            }
        }
    }

    protected static Map<String, LfAction> createLfActions(LfNetwork lfNetwork, Set<Action> actions, Network network, LfNetworkParameters parameters) {
        return actions.stream()
                .map(action -> LfActionUtils.createLfAction(action, network, parameters.isBreakers(), lfNetwork))
                .collect(Collectors.toMap(LfAction::getId, Function.identity()));
    }

    protected static Map<String, Action> indexActionsById(List<Action> actions) {
        return actions.stream()
                .collect(Collectors.toMap(
                        Action::getId,
                        Function.identity(),
                    (action1, action2) -> {
                        throw new PowsyblException("An action '" + action1.getId() + "' already exist");
                    }
                ));
    }

    private static boolean hasValidContingency(OperatorStrategy operatorStrategy, Set<String> contingencyIds) {
        return contingencyIds.contains(operatorStrategy.getContingencyContext().getContingencyId());
    }

    private static Optional<String> findMissingActionId(OperatorStrategy operatorStrategy, Set<String> actionIds) {
        for (ConditionalActions conditionalActions : operatorStrategy.getConditionalActions()) {
            for (String actionId : conditionalActions.getActionIds()) {
                if (!actionIds.contains(actionId)) {
                    return Optional.of(actionId);
                }
            }
        }
        return Optional.empty();
    }

    private static void throwMissingOperatorStrategyContingency(OperatorStrategy operatorStrategy) {
        throw new PowsyblException("Operator strategy '" + operatorStrategy.getId() + "' is associated to contingency '"
                + operatorStrategy.getContingencyContext().getContingencyId() + "' but this contingency is not present in the list");

    }

    private static void throwMissingOperatorStrategyAction(OperatorStrategy operatorStrategy, String actionId) {
        throw new PowsyblException("Operator strategy '" + operatorStrategy.getId() + "' is associated to action '"
                + actionId + "' but this action is not present in the list");
    }

    protected static Map<String, List<OperatorStrategy>> indexOperatorStrategiesByContingencyId(List<PropagatedContingency> propagatedContingencies,
                                                                                              List<OperatorStrategy> operatorStrategies,
                                                                                              Map<String, Action> actionsById,
                                                                                              Set<Action> neededActions,
                                                                                              boolean checkOperatorStrategies) {

        Set<String> contingencyIds = propagatedContingencies.stream().map(propagatedContingency -> propagatedContingency.getContingency().getId()).collect(Collectors.toSet());
        Map<String, List<OperatorStrategy>> operatorStrategiesByContingencyId = new HashMap<>();
        Set<String> actionIds = actionsById.keySet();
        for (OperatorStrategy operatorStrategy : operatorStrategies) {
            if (hasValidContingency(operatorStrategy, contingencyIds)) {
                if (checkOperatorStrategies) {
                    findMissingActionId(operatorStrategy, actionIds)
                            .ifPresent(id -> throwMissingOperatorStrategyAction(operatorStrategy, id));
                }

                for (ConditionalActions conditionalActions : operatorStrategy.getConditionalActions()) {
                    for (String actionId : conditionalActions.getActionIds()) {
                        Action action = actionsById.get(actionId);
                        neededActions.add(action);
                    }
                }
                operatorStrategiesByContingencyId.computeIfAbsent(operatorStrategy.getContingencyContext().getContingencyId(), key -> new ArrayList<>())
                        .add(operatorStrategy);
            } else {
                if (checkOperatorStrategies) {
                    throwMissingOperatorStrategyContingency(operatorStrategy);
                }
            }
        }
        return operatorStrategiesByContingencyId;
    }

    private static boolean checkCondition(ConditionalActions conditionalActions, Set<String> limitViolationEquipmentIds) {
        switch (conditionalActions.getCondition().getType()) {
            case TrueCondition.NAME:
                return true;
            case AnyViolationCondition.NAME:
                return !limitViolationEquipmentIds.isEmpty();
            case AtLeastOneViolationCondition.NAME: {
                AtLeastOneViolationCondition atLeastCondition = (AtLeastOneViolationCondition) conditionalActions.getCondition();
                Set<String> commonEquipmentIds = atLeastCondition.getViolationIds().stream()
                        .distinct()
                        .filter(limitViolationEquipmentIds::contains)
                        .collect(Collectors.toSet());
                return !commonEquipmentIds.isEmpty();
            }
            case AllViolationCondition.NAME: {
                AllViolationCondition allCondition = (AllViolationCondition) conditionalActions.getCondition();
                Set<String> commonEquipmentIds = allCondition.getViolationIds().stream()
                        .distinct()
                        .filter(limitViolationEquipmentIds::contains)
                        .collect(Collectors.toSet());
                return commonEquipmentIds.equals(new HashSet<>(allCondition.getViolationIds()));
            }
            default:
                throw new UnsupportedOperationException("Unsupported condition type: " + conditionalActions.getCondition().getType());
        }
    }

    protected List<String> checkCondition(OperatorStrategy operatorStrategy, LimitViolationsResult limitViolationsResult) {
        Set<String> limitViolationEquipmentIds = limitViolationsResult.getLimitViolations().stream()
                .map(LimitViolation::getSubjectId)
                .collect(Collectors.toSet());
        List<String> actionsIds = new ArrayList<>();
        for (ConditionalActions conditionalActions : operatorStrategy.getConditionalActions()) {
            if (checkCondition(conditionalActions, limitViolationEquipmentIds)) {
                actionsIds.addAll(conditionalActions.getActionIds());
            }
        }
        return actionsIds;
    }

    protected static void findAllSwitchesToOperate(Network network, List<Action> actions, LfTopoConfig topoConfig) {
        actions.stream().filter(action -> action.getType().equals(SwitchAction.NAME))
                .forEach(action -> {
                    String switchId = ((SwitchAction) action).getSwitchId();
                    Switch sw = network.getSwitch(switchId);
                    boolean toOpen = ((SwitchAction) action).isOpen();
                    if (sw.isOpen() && !toOpen) { // the switch is open and the action will close it.
                        topoConfig.getSwitchesToClose().add(sw);
                    } else if (!sw.isOpen() && toOpen) { // the switch is closed and the action will open it.
                        topoConfig.getSwitchesToOpen().add(sw);
                    }
                });
    }

    protected static void findAllPtcToOperate(List<Action> actions, LfTopoConfig topoConfig) {
        for (Action action : actions) {
            if (PhaseTapChangerTapPositionAction.NAME.equals(action.getType())) {
                PhaseTapChangerTapPositionAction ptcAction = (PhaseTapChangerTapPositionAction) action;
                ptcAction.getSide().ifPresentOrElse(
                        side -> topoConfig.addBranchIdWithPtcToRetain(LfLegBranch.getId(side, ptcAction.getTransformerId())), // T3WT
                        () -> topoConfig.addBranchIdWithPtcToRetain(ptcAction.getTransformerId()) // T2WT
                );
            }
        }
    }

    protected static void findAllRtcToOperate(List<Action> actions, LfTopoConfig topoConfig) {
        for (Action action : actions) {
            if (RatioTapChangerTapPositionAction.NAME.equals(action.getType())) {
                RatioTapChangerTapPositionAction rtcAction = (RatioTapChangerTapPositionAction) action;
                rtcAction.getSide().ifPresentOrElse(
                        side -> topoConfig.addBranchIdWithRtcToRetain(LfLegBranch.getId(side, rtcAction.getTransformerId())), // T3WT
                        () -> topoConfig.addBranchIdWithRtcToRetain(rtcAction.getTransformerId()) // T2WT
                );
            }
        }
    }

    protected static void findAllShuntsToOperate(List<Action> actions, LfTopoConfig topoConfig) {
        actions.stream().filter(action -> action.getType().equals(ShuntCompensatorPositionAction.NAME))
                .forEach(action -> topoConfig.addShuntIdToOperate(((ShuntCompensatorPositionAction) action).getShuntCompensatorId()));
    }

    protected static void findAllBranchesToClose(Network network, List<Action> actions, LfTopoConfig topoConfig) {
        // only branches open at both side or open at one side are visible in the LfNetwork.
        for (Action action : actions) {
            if (TerminalsConnectionAction.NAME.equals(action.getType())) {
                TerminalsConnectionAction terminalsConnectionAction = (TerminalsConnectionAction) action;
                if (terminalsConnectionAction.getSide().isEmpty() && !terminalsConnectionAction.isOpen()) {
                    Branch<?> branch = network.getBranch(terminalsConnectionAction.getElementId());
                    if (branch != null && !(branch instanceof TieLine) &&
                            !branch.getTerminal1().isConnected() && !branch.getTerminal2().isConnected()) {
                        // both terminals must be disconnected. If only one is connected, the branch is present
                        // in the Lf network.
                        topoConfig.getBranchIdsToClose().add(terminalsConnectionAction.getElementId());
                    }
                }
            }
        }
    }

    boolean isAreaInterchangeControl(OpenLoadFlowParameters lfParametersExt, List<Contingency> contingencies) {
        return lfParametersExt.isAreaInterchangeControl() ||
                contingencies.stream()
                        .map(contingency -> contingency.getExtension(ContingencyLoadFlowParameters.class))
                        .filter(Objects::nonNull)
                        .map(ContingencyLoadFlowParameters.class::cast)
                        .anyMatch(contingencyParameters -> contingencyParameters.isAreaInterchangeControl().orElse(false));
    }

    protected abstract C createLoadFlowContext(LfNetwork lfNetwork, P parameters);

    protected abstract LoadFlowEngine<V, E, P, R> createLoadFlowEngine(C context);

    protected void afterPreContingencySimulation(P acParameters) {
    }

    protected SecurityAnalysisResult runSimulations(LfNetwork lfNetwork, List<PropagatedContingency> propagatedContingencies, P acParameters,
                                                    SecurityAnalysisParameters securityAnalysisParameters, List<OperatorStrategy> operatorStrategies,
                                                    List<Action> actions, List<LimitReduction> limitReductions, ContingencyActivePowerLossDistribution contingencyActivePowerLossDistribution) {
        Map<String, Action> actionsById = indexActionsById(actions);
        Set<Action> neededActions = new HashSet<>(actionsById.size());

        // In MT the operator strategy check is performed before running the simulations
        boolean checkOperatorStrategies = OpenSecurityAnalysisParameters.getOrDefault(securityAnalysisParameters).getThreadCount() == 1;

        Map<String, List<OperatorStrategy>> operatorStrategiesByContingencyId =
                indexOperatorStrategiesByContingencyId(propagatedContingencies, operatorStrategies, actionsById, neededActions,
                        checkOperatorStrategies);

        Map<String, LfAction> lfActionById = createLfActions(lfNetwork, neededActions, network, acParameters.getNetworkParameters()); // only convert needed actions

        LoadFlowParameters loadFlowParameters = securityAnalysisParameters.getLoadFlowParameters();
        OpenLoadFlowParameters openLoadFlowParameters = OpenLoadFlowParameters.get(loadFlowParameters);
        OpenSecurityAnalysisParameters openSecurityAnalysisParameters = OpenSecurityAnalysisParameters.getOrDefault(securityAnalysisParameters);
        boolean createResultExtension = openSecurityAnalysisParameters.isCreateResultExtension();

        try (C context = createLoadFlowContext(lfNetwork, acParameters)) {
            ReportNode networkReportNode = lfNetwork.getReportNode();
            ReportNode preContSimReportNode = Reports.createPreContingencySimulation(networkReportNode);
            lfNetwork.setReportNode(preContSimReportNode);

            // run pre-contingency simulation
            R preContingencyLoadFlowResult = createLoadFlowEngine(context)
                    .run();

            boolean preContingencyComputationOk = preContingencyLoadFlowResult.isSuccess();
            var preContingencyLimitViolationManager = new LimitViolationManager(limitReductions);
            List<PostContingencyResult> postContingencyResults = new ArrayList<>();
            var preContingencyNetworkResult = new PreContingencyNetworkResult(lfNetwork, monitorIndex, createResultExtension);
            List<OperatorStrategyResult> operatorStrategyResults = new ArrayList<>();

            // only run post-contingency simulations if pre-contingency simulation is ok
            if (preContingencyComputationOk) {
                afterPreContingencySimulation(acParameters);

                // update network result
                preContingencyNetworkResult.update();

                // detect violations
                preContingencyLimitViolationManager.detectViolations(lfNetwork);

                // save base state for later restoration after each contingency
                NetworkState networkState = NetworkState.save(lfNetwork);

                // Create consumer to reset parameters if they are modified for a contingency
                Consumer<P> parametersResetter = createParametersResetter(acParameters);

                // start a simulation for each of the contingency
                Iterator<PropagatedContingency> contingencyIt = propagatedContingencies.iterator();
                while (contingencyIt.hasNext() && !Thread.currentThread().isInterrupted()) {
                    PropagatedContingency propagatedContingency = contingencyIt.next();
                    propagatedContingency.toLfContingency(lfNetwork)
                            .ifPresent(lfContingency -> { // only process contingencies that impact the network
                                ReportNode postContSimReportNode = Reports.createPostContingencySimulation(networkReportNode, lfContingency.getId());
                                lfNetwork.setReportNode(postContSimReportNode);

                                ContingencyLoadFlowParameters contingencyLoadFlowParameters = propagatedContingency.getContingency().getExtension(ContingencyLoadFlowParameters.class);
                                if (contingencyLoadFlowParameters != null) {
                                    applyContingencyParameters(context.getParameters(), contingencyLoadFlowParameters, loadFlowParameters, openLoadFlowParameters);
                                }

                                lfContingency.apply(loadFlowParameters.getBalanceType());

                                contingencyActivePowerLossDistribution.run(lfNetwork, lfContingency, propagatedContingency.getContingency(), securityAnalysisParameters, contingencyLoadFlowParameters, postContSimReportNode);

                                var postContingencyResult = runPostContingencySimulation(lfNetwork, context, propagatedContingency.getContingency(),
                                                                                         lfContingency, preContingencyLimitViolationManager,
                                                                                         securityAnalysisParameters.getIncreasedViolationsParameters(),
                                                                                         preContingencyNetworkResult, createResultExtension, limitReductions);
                                postContingencyResults.add(postContingencyResult);

                                List<OperatorStrategy> operatorStrategiesForThisContingency = operatorStrategiesByContingencyId.get(lfContingency.getId());
                                if (operatorStrategiesForThisContingency != null) {
                                    // we have at least one operator strategy for this contingency.
                                    if (operatorStrategiesForThisContingency.size() == 1) {
                                        // only one operator strategy, no need to do a complete save of network state,
                                        // but need to set generators initialTargetP positions to the current (=postContingency) targetP
                                        lfNetwork.setGeneratorsInitialTargetPToTargetP();
                                        OperatorStrategy operatorStrategy = operatorStrategiesForThisContingency.get(0);
                                        ReportNode osSimReportNode = Reports.createOperatorStrategySimulation(postContSimReportNode, operatorStrategy.getId());
                                        lfNetwork.setReportNode(osSimReportNode);
                                        runActionSimulation(lfNetwork, context,
                                                operatorStrategy, preContingencyLimitViolationManager,
                                                securityAnalysisParameters.getIncreasedViolationsParameters(), lfActionById,
                                                createResultExtension, lfContingency, postContingencyResult.getLimitViolationsResult(),
                                                acParameters.getNetworkParameters(), limitReductions)
                                                .ifPresent(operatorStrategyResults::add);
                                    } else {
                                        // multiple operator strategies, save post contingency state for later restoration after action
                                        NetworkState postContingencyNetworkState = NetworkState.save(lfNetwork);
                                        for (OperatorStrategy operatorStrategy : operatorStrategiesForThisContingency) {
                                            ReportNode osSimReportNode = Reports.createOperatorStrategySimulation(postContSimReportNode, operatorStrategy.getId());
                                            lfNetwork.setReportNode(osSimReportNode);
                                            runActionSimulation(lfNetwork, context,
                                                    operatorStrategy, preContingencyLimitViolationManager,
                                                    securityAnalysisParameters.getIncreasedViolationsParameters(), lfActionById,
                                                    createResultExtension, lfContingency, postContingencyResult.getLimitViolationsResult(),
                                                    acParameters.getNetworkParameters(), limitReductions)
                                                    .ifPresent(result -> {
                                                        operatorStrategyResults.add(result);
                                                        postContingencyNetworkState.restore();
                                                    });
                                        }
                                    }
                                }
                                if (contingencyIt.hasNext()) {
                                    // restore base state
                                    networkState.restore();
                                    if (contingencyLoadFlowParameters != null) {
                                        // reset parameters
                                        parametersResetter.accept(context.getParameters());
                                    }
                                }
                            });
                }
            }

            return new SecurityAnalysisResult(
                    new PreContingencyResult(
                            preContingencyLoadFlowResult.toComponentResultStatus().status(),
                            new LimitViolationsResult(preContingencyLimitViolationManager.getLimitViolations()),
                            preContingencyNetworkResult.getBranchResults(), preContingencyNetworkResult.getBusResults(),
                            preContingencyNetworkResult.getThreeWindingsTransformerResults()),
                    postContingencyResults, operatorStrategyResults);
        }
    }

    /**
     * @return a consumer for Ac/DcLoadFlowParameters that resets them to their original state, in case they have been modified according
     * to the ContingencyLoadFlowParameters extension with {@link #applyContingencyParameters}.
     */
    protected abstract Consumer<P> createParametersResetter(P parameters);

    /**
     * Applies the custom parameters that are contained in the ContingencyLoadFlowParameters extension for a specific contingency.
     * If the extension is present, modifies the ac/dcLoadFlowParameters contained in the LoadFlowContext accordingly.
     */
    protected abstract void applyContingencyParameters(P parameters, ContingencyLoadFlowParameters contingencyParameters, LoadFlowParameters loadFlowParameters, OpenLoadFlowParameters openLoadFlowParameters);

    private Optional<OperatorStrategyResult> runActionSimulation(LfNetwork network, C context, OperatorStrategy operatorStrategy,
                                                                 LimitViolationManager preContingencyLimitViolationManager,
                                                                 SecurityAnalysisParameters.IncreasedViolationsParameters violationsParameters,
                                                                 Map<String, LfAction> lfActionById, boolean createResultExtension, LfContingency contingency,
                                                                 LimitViolationsResult postContingencyLimitViolations, LfNetworkParameters networkParameters,
                                                                 List<LimitReduction> limitReductions) {
        OperatorStrategyResult operatorStrategyResult = null;

        List<String> actionIds = checkCondition(operatorStrategy, postContingencyLimitViolations);
        if (!actionIds.isEmpty()) {
            operatorStrategyResult = runActionSimulation(network, context, operatorStrategy, actionIds, preContingencyLimitViolationManager,
                    violationsParameters, lfActionById, createResultExtension, contingency, networkParameters, limitReductions);
        }

        return Optional.ofNullable(operatorStrategyResult);
    }

    protected PostContingencyResult runPostContingencySimulation(LfNetwork network, C context, Contingency contingency, LfContingency lfContingency,
                                                                 LimitViolationManager preContingencyLimitViolationManager,
                                                                 SecurityAnalysisParameters.IncreasedViolationsParameters violationsParameters,
                                                                 PreContingencyNetworkResult preContingencyNetworkResult, boolean createResultExtension,
                                                                 List<LimitReduction> limitReductions) {
        logPostContingencyStart(network, lfContingency);

        Stopwatch stopwatch = Stopwatch.createStarted();

        // restart LF on post contingency equation system
        PostContingencyComputationStatus status = runActionLoadFlow(context); // FIXME: change name.
        var postContingencyLimitViolationManager = new LimitViolationManager(preContingencyLimitViolationManager, limitReductions, violationsParameters);
        var postContingencyNetworkResult = new PostContingencyNetworkResult(network, monitorIndex, createResultExtension, preContingencyNetworkResult, contingency);

        if (status.equals(PostContingencyComputationStatus.CONVERGED)) {
            // update network result
            postContingencyNetworkResult.update();

            // detect violations
            postContingencyLimitViolationManager.detectViolations(network);
        }

        stopwatch.stop();
        logPostContingencyEnd(network, lfContingency, stopwatch);

        var connectivityResult = new ConnectivityResult(lfContingency.getCreatedSynchronousComponentsCount(), 0,
                lfContingency.getDisconnectedLoadActivePower() * PerUnit.SB,
                lfContingency.getDisconnectedGenerationActivePower() * PerUnit.SB,
                lfContingency.getDisconnectedElementIds());

        return new PostContingencyResult(contingency, status,
                new LimitViolationsResult(postContingencyLimitViolationManager.getLimitViolations()),
                postContingencyNetworkResult.getBranchResults(),
                postContingencyNetworkResult.getBusResults(),
                postContingencyNetworkResult.getThreeWindingsTransformerResults(),
                connectivityResult);
    }

    protected void logPostContingencyStart(LfNetwork network, LfContingency lfContingency) {
        LOGGER.atLevel(logLevel).log("Start post contingency '{}' simulation on network {}", lfContingency.getId(), network);
        LOGGER.debug("Contingency '{}' impact on network {}: remove {} buses, remove {} branches, remove {} generators, shift {} shunts, shift {} loads",
                lfContingency.getId(), network, lfContingency.getDisabledNetwork().getBuses(), lfContingency.getDisabledNetwork().getBranchesStatus(),
                lfContingency.getLostGenerators(), lfContingency.getShuntsShift(), lfContingency.getLostLoads());
    }

    protected void logPostContingencyEnd(LfNetwork network, LfContingency lfContingency, Stopwatch stopwatch) {
        LOGGER.atLevel(logLevel).log("Post contingency '{}' simulation done on network {} in {} ms", lfContingency.getId(),
                network, stopwatch.elapsed(TimeUnit.MILLISECONDS));
    }

    protected OperatorStrategyResult runActionSimulation(LfNetwork network, C context, OperatorStrategy operatorStrategy,
                                                         List<String> actionsIds,
                                                         LimitViolationManager preContingencyLimitViolationManager,
                                                         SecurityAnalysisParameters.IncreasedViolationsParameters violationsParameters,
                                                         Map<String, LfAction> lfActionById, boolean createResultExtension, LfContingency contingency,
                                                         LfNetworkParameters networkParameters, List<LimitReduction> limitReductions) {
        logActionStart(network, operatorStrategy);

        // get LF action for this operator strategy, as all actions have been previously checked against IIDM
        // network, an empty LF action means it is for another component (so another LF network) so we can
        // skip it
        List<LfAction> operatorStrategyLfActions = actionsIds.stream()
                .map(lfActionById::get)
                .filter(Objects::nonNull)
                .toList();

        LfActionUtils.applyListOfActions(operatorStrategyLfActions, network, contingency, networkParameters);

        Stopwatch stopwatch = Stopwatch.createStarted();

        // restart LF on post contingency and post actions equation system
        PostContingencyComputationStatus status = runActionLoadFlow(context);
        var postActionsViolationManager = new LimitViolationManager(preContingencyLimitViolationManager, limitReductions, violationsParameters);
        var postActionsNetworkResult = new PreContingencyNetworkResult(network, monitorIndex, createResultExtension);

        if (status.equals(PostContingencyComputationStatus.CONVERGED)) {
            // update network result
            postActionsNetworkResult.update();

            // detect violations
            postActionsViolationManager.detectViolations(network);
        }

        stopwatch.stop();

        logActionEnd(network, operatorStrategy, stopwatch);

        return new OperatorStrategyResult(operatorStrategy, status,
                                          new LimitViolationsResult(postActionsViolationManager.getLimitViolations()),
                                          new NetworkResult(postActionsNetworkResult.getBranchResults(),
                                                            postActionsNetworkResult.getBusResults(),
                                                            postActionsNetworkResult.getThreeWindingsTransformerResults()));
    }

    protected void logActionStart(LfNetwork network, OperatorStrategy operatorStrategy) {
        LOGGER.atLevel(logLevel).log("Start operator strategy {} after contingency '{}' simulation on network {}", operatorStrategy.getId(),
                operatorStrategy.getContingencyContext().getContingencyId(), network);
    }

    protected void logActionEnd(LfNetwork network, OperatorStrategy operatorStrategy, Stopwatch stopwatch) {
        LOGGER.atLevel(logLevel).log("Operator strategy {} after contingency '{}' simulation done on network {} in {} ms", operatorStrategy.getId(),
                operatorStrategy.getContingencyContext().getContingencyId(), network, stopwatch.elapsed(TimeUnit.MILLISECONDS));
    }

    protected void beforeActionLoadFlowRun(C context) {
    }

    protected PostContingencyComputationStatus runActionLoadFlow(C context) {
        beforeActionLoadFlowRun(context);
        R result = createLoadFlowEngine(context).run();
        return postContingencyStatusFromLoadFlowResult(result);
    }
}