AcloadFlowEngine.java

/*
 * Copyright (c) 2019-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.ac;

import com.google.common.collect.Lists;
import com.powsybl.commons.report.ReportNode;
import com.powsybl.openloadflow.ac.equations.AcEquationType;
import com.powsybl.openloadflow.ac.equations.AcVariableType;
import com.powsybl.openloadflow.ac.outerloop.AcActivePowerDistributionOuterLoop;
import com.powsybl.openloadflow.ac.outerloop.AcOuterLoop;
import com.powsybl.openloadflow.ac.solver.*;
import com.powsybl.openloadflow.equations.EquationSystem;
import com.powsybl.openloadflow.equations.Variable;
import com.powsybl.openloadflow.lf.LoadFlowEngine;
import com.powsybl.openloadflow.lf.outerloop.OuterLoopResult;
import com.powsybl.openloadflow.lf.outerloop.OuterLoopStatus;
import com.powsybl.openloadflow.network.LfNetwork;
import com.powsybl.openloadflow.network.util.PreviousValueVoltageInitializer;
import com.powsybl.openloadflow.network.util.VoltageInitializer;
import com.powsybl.openloadflow.util.Reports;
import org.apache.commons.lang3.mutable.MutableInt;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;

/**
 * @author Geoffroy Jamgotchian {@literal <geoffroy.jamgotchian at rte-france.com>}
 */
public class AcloadFlowEngine implements LoadFlowEngine<AcVariableType, AcEquationType, AcLoadFlowParameters, AcLoadFlowResult> {

    private static final Logger LOGGER = LoggerFactory.getLogger(AcloadFlowEngine.class);

    private final AcLoadFlowContext context;

    private final AcSolverFactory solverFactory;

    public AcloadFlowEngine(AcLoadFlowContext context) {
        this.context = Objects.requireNonNull(context);
        this.solverFactory = context.getParameters().getSolverFactory();
    }

    @Override
    public AcLoadFlowContext getContext() {
        return context;
    }

    private static class RunningContext {

        private AcSolverResult lastSolverResult;

        private final Map<String, MutableInt> outerLoopIterationByType = new HashMap<>();

        private int outerLoopTotalIterations = 0;

        private final MutableInt nrTotalIterations = new MutableInt();

        private OuterLoopResult lastOuterLoopResult = OuterLoopResult.stable();

        private AcOuterLoop lastUnrealisticStateFixingLoop;
    }

    private void runOuterLoop(AcOuterLoop outerLoop, AcOuterLoopContext outerLoopContext, AcSolver solver, RunningContext runningContext, boolean checkUnrealistic) {
        ReportNode olReportNode = Reports.createOuterLoopReporter(outerLoopContext.getNetwork().getReportNode(), outerLoop.getName());

        // for each outer loop re-run solver until stabilization
        OuterLoopResult outerLoopResult;
        do {
            MutableInt outerLoopIteration = runningContext.outerLoopIterationByType.computeIfAbsent(outerLoop.getName(), k -> new MutableInt());

            // check outer loop status
            outerLoopContext.setIteration(outerLoopIteration.getValue());
            outerLoopContext.setOuterLoopTotalIterations(runningContext.outerLoopTotalIterations);
            outerLoopContext.setLastSolverResult(runningContext.lastSolverResult);
            outerLoopContext.setLoadFlowContext(context);
            outerLoopResult = outerLoop.check(outerLoopContext, olReportNode);
            runningContext.lastOuterLoopResult = outerLoopResult;

            if (outerLoopResult.status() == OuterLoopStatus.UNSTABLE) {
                LOGGER.debug("Start outer loop '{}' iteration {}", outerLoop.getName(), runningContext.outerLoopTotalIterations);

                ReportNode reportNode = context.getNetwork().getReportNode();
                if (context.getParameters().isDetailedReport()) {
                    reportNode = Reports.createDetailedSolverReporterOuterLoop(reportNode,
                            solver.getName(),
                            context.getNetwork().getNumCC(),
                            context.getNetwork().getNumSC(),
                            runningContext.outerLoopTotalIterations + 1,
                            outerLoop.getName());
                }

                // if not yet stable, restart solver
                runningContext.lastSolverResult = runAcSolverAndCheckRealisticState(solver, new PreviousValueVoltageInitializer(), reportNode, checkUnrealistic,
                        outerLoopContext.getLoadFlowContext().getParameters());

                runningContext.nrTotalIterations.add(runningContext.lastSolverResult.getIterations());
                runningContext.outerLoopTotalIterations++;

                outerLoopIteration.increment();
            }
        } while (outerLoopResult.status() == OuterLoopStatus.UNSTABLE
                && runningContext.lastSolverResult.getStatus() == AcSolverStatus.CONVERGED
                && runningContext.outerLoopTotalIterations < context.getParameters().getMaxOuterLoopIterations());

        if (!checkUnrealistic && runningContext.lastUnrealisticStateFixingLoop == outerLoop && runningContext.lastSolverResult.getStatus() == AcSolverStatus.CONVERGED) {

            // This is time to check the unrealistic state and create a report if needed
            boolean isStateUnrealistic = isStateUnrealistic(context.getNetwork().getReportNode(),
                    outerLoopContext.getLoadFlowContext().getParameters().getMinNominalVoltageRealisticVoltageCheck());

            if (isStateUnrealistic) {
                runningContext.lastSolverResult = new AcSolverResult(AcSolverStatus.UNREALISTIC_STATE,
                                                                     runningContext.lastSolverResult.getIterations(),
                                                                     runningContext.lastSolverResult.getSlackBusActivePowerMismatch());
            }
        }

        if (outerLoopResult.status() != OuterLoopStatus.STABLE) {
            Reports.reportUnsuccessfulOuterLoop(olReportNode, outerLoopResult.status().name());
        }
    }

    private boolean isStateUnrealistic(ReportNode reportNode, double minNominalVoltageRealisticVoltageCheck) {

        EquationSystem<AcVariableType, AcEquationType> equationSystem = context.getEquationSystem();
        AcLoadFlowParameters parameters = context.getParameters();
        LfNetwork network = context.getNetwork();
        Map<String, Double> busesOutOfNormalVoltageRange = new LinkedHashMap<>();
        for (Variable<AcVariableType> v : equationSystem.getIndex().getSortedVariablesToFind()) {
            if (v.getType() == AcVariableType.BUS_V && !network.getBus(v.getElementNum()).isFictitious()) {
                double value = equationSystem.getStateVector().get(v.getRow());
                if (value < parameters.getMinRealisticVoltage() || value > parameters.getMaxRealisticVoltage()) {
                    // only consider unrealistic if nominal voltage high enough
                    if (network.getBus(v.getElementNum()).getNominalV() >= minNominalVoltageRealisticVoltageCheck) {
                        busesOutOfNormalVoltageRange.put(network.getBus(v.getElementNum()).getId(), value);
                    }
                }
            }
        }
        if (!busesOutOfNormalVoltageRange.isEmpty()) {
            if (LOGGER.isTraceEnabled()) {
                for (var e : busesOutOfNormalVoltageRange.entrySet()) {
                    LOGGER.trace("Bus '{}' has an unrealistic voltage magnitude: {} pu", e.getKey(), e.getValue());
                }
            }
            LOGGER.error("{} buses have a voltage magnitude out of range [{}, {}]: {}",
                    busesOutOfNormalVoltageRange.size(), parameters.getMinRealisticVoltage(), parameters.getMaxRealisticVoltage(), busesOutOfNormalVoltageRange);

            Reports.reportNewtonRaphsonBusesOutOfRealisticVoltageRange(reportNode, busesOutOfNormalVoltageRange, parameters.getMinRealisticVoltage(), parameters.getMaxRealisticVoltage());
        }
        return !busesOutOfNormalVoltageRange.isEmpty();
    }

    private AcSolverResult runAcSolverAndCheckRealisticState(AcSolver solver, VoltageInitializer voltageInitializer,
                                                             ReportNode reportNode, boolean checkUnrealistic, AcLoadFlowParameters parameters) {
        AcSolverResult result = solver.run(voltageInitializer, reportNode);

        if (checkUnrealistic && result.getStatus() == AcSolverStatus.CONVERGED && isStateUnrealistic(reportNode, parameters.getMinNominalVoltageRealisticVoltageCheck())) {
            result = new AcSolverResult(AcSolverStatus.UNREALISTIC_STATE, result.getIterations(), result.getSlackBusActivePowerMismatch());
        }

        return result;
    }

    @Override
    public AcLoadFlowResult run() {
        LOGGER.info("Start AC loadflow on network {}", context.getNetwork());

        VoltageInitializer voltageInitializer = context.getParameters().getVoltageInitializer();
        // in case of a DC voltage initializer, an DC equation system in created and equations are attached
        // to the network. It is important that DC init is done before AC equation system is created by
        // calling ACLoadContext.getEquationSystem to avoid DC equations overwrite AC ones in the network.
        voltageInitializer.prepare(context.getNetwork());

        RunningContext runningContext = new RunningContext();
        double distributedActivePower = 0.0;
        ReportNode reportNode = context.getNetwork().getReportNode();

        // Verify whether a regulated bus voltage exists.
        // If not, then fail immediately with SOLVER_FAILED status.
        // Note that this approach is not perfect and could be improved in the future, in particular in
        // security analysis and remedial actions context.
        // For example: a contingency may cause the last PV node to disappear. In this case here we would
        // just report SOLVER_FAILED. However, there could be other generators blocked at MinQ or MaxQ that
        // _could potentially_ recover the situation, but this will not be tried at all...
        boolean hasVoltageRegulatedBus = context.getNetwork().getBuses().stream()
                .anyMatch(b -> b.isGeneratorVoltageControlEnabled() && !b.isDisabled() && !b.getGeneratorVoltageControl().orElseThrow().isDisabled());
        if (!hasVoltageRegulatedBus) {
            LOGGER.info("Network must have at least one bus with generator voltage control enabled");
            Reports.reportNetworkMustHaveAtLeastOneBusGeneratorVoltageControlEnabled(reportNode);
            runningContext.lastSolverResult = new AcSolverResult(AcSolverStatus.SOLVER_FAILED, 0, Double.NaN);
            return buildAcLoadFlowResult(runningContext, OuterLoopResult.stable(), distributedActivePower);
        }

        AcSolver solver = solverFactory.create(context.getNetwork(),
                                               context.getParameters(),
                                               context.getEquationSystem(),
                                               context.getJacobianMatrix(),
                                               context.getTargetVector(),
                                               context.getEquationVector());

        List<AcOuterLoop> outerLoops = context.getParameters().getOuterLoops();
        List<Pair<AcOuterLoop, AcOuterLoopContext>> outerLoopsAndContexts = outerLoops.stream()
                .map(outerLoop -> Pair.of(outerLoop, new AcOuterLoopContext(context.getNetwork())))
                .toList();

        // outer loops initialization
        for (var outerLoopAndContext : outerLoopsAndContexts) {
            var outerLoop = outerLoopAndContext.getLeft();
            var outerLoopContext = outerLoopAndContext.getRight();
            outerLoop.initialize(outerLoopContext);
        }

        if (context.getParameters().isDetailedReport()) {
            reportNode = Reports.createDetailedSolverReporter(reportNode,
                    solver.getName(),
                    context.getNetwork().getNumCC(),
                    context.getNetwork().getNumSC());
        }

        // If in remote voltage control robust mode, find the latest outerloop that can fix unrealistic state
        // to apply the check after that loop only
        runningContext.lastUnrealisticStateFixingLoop = context.getParameters().isVoltageRemoteControlRobustMode() ?
                outerLoopsAndContexts.stream()
                .map(Pair::getLeft)
                .filter(AcOuterLoop::canFixUnrealisticState)
                .reduce((first, second) -> second).orElse(null)
            :
            null;

        // Don't check unrealistic voltage yet if an outer loop can fix them
        boolean checkUnrealisticStates = runningContext.lastUnrealisticStateFixingLoop == null;

        // initial solver run
        runningContext.lastSolverResult = runAcSolverAndCheckRealisticState(solver, voltageInitializer, reportNode, checkUnrealisticStates, context.getParameters());

        runningContext.nrTotalIterations.add(runningContext.lastSolverResult.getIterations());

        // continue with outer loops only if solver succeed
        if (runningContext.lastSolverResult.getStatus() == AcSolverStatus.CONVERGED) {

            // re-run all outer loops until solver failed or no more solver iterations are needed
            int oldNrTotalIterations;
            do {
                // Restore the flag before each outerloop iteration
                checkUnrealisticStates = runningContext.lastUnrealisticStateFixingLoop == null;

                oldNrTotalIterations = runningContext.nrTotalIterations.getValue();

                // outer loops are nested: innermost loop first in the list, outermost loop last
                for (var outerLoopAndContext : outerLoopsAndContexts) {
                    runOuterLoop(outerLoopAndContext.getLeft(), outerLoopAndContext.getRight(), solver, runningContext, checkUnrealisticStates);

                    if (outerLoopAndContext.getLeft() == runningContext.lastUnrealisticStateFixingLoop) {
                        checkUnrealisticStates = true;
                    }

                    // continue with next outer loop only if:
                    // - last solver run succeed,
                    // - last OuterLoopStatus is not FAILED
                    // - we have not reached max number of outer loop iteration
                    if (runningContext.lastSolverResult.getStatus() != AcSolverStatus.CONVERGED
                            || runningContext.lastOuterLoopResult.status() == OuterLoopStatus.FAILED
                            || runningContext.outerLoopTotalIterations >= context.getParameters().getMaxOuterLoopIterations()) {
                        break;
                    }
                }
            } while (runningContext.nrTotalIterations.getValue() > oldNrTotalIterations
                    && runningContext.lastSolverResult.getStatus() == AcSolverStatus.CONVERGED
                    && runningContext.lastOuterLoopResult.status() != OuterLoopStatus.FAILED
                    && runningContext.outerLoopTotalIterations < context.getParameters().getMaxOuterLoopIterations());
        }

        // outer loops finalization (in reverse order to allow correct cleanup)
        for (var outerLoopAndContext : Lists.reverse(outerLoopsAndContexts)) {
            var outerLoop = outerLoopAndContext.getLeft();
            var outerLoopContext = outerLoopAndContext.getRight();
            if (outerLoop instanceof AcActivePowerDistributionOuterLoop activePowerDistributionOuterLoop) {
                distributedActivePower = activePowerDistributionOuterLoop.getDistributedActivePower(outerLoopContext);
            }
            outerLoop.cleanup(outerLoopContext);
        }

        if (runningContext.outerLoopTotalIterations >= context.getParameters().getMaxOuterLoopIterations()) {
            Reports.reportMaxOuterLoopIterations(reportNode, runningContext.outerLoopTotalIterations, true, LOGGER);
        }

        final OuterLoopResult outerLoopFinalResult;
        if (runningContext.lastOuterLoopResult.status() == OuterLoopStatus.FAILED) {
            outerLoopFinalResult = runningContext.lastOuterLoopResult;
        } else {
            outerLoopFinalResult = runningContext.outerLoopTotalIterations < context.getParameters().getMaxOuterLoopIterations()
                    ? new OuterLoopResult(runningContext.lastOuterLoopResult.outerLoopName(), OuterLoopStatus.STABLE, runningContext.lastOuterLoopResult.statusText()) :
                    new OuterLoopResult(runningContext.lastOuterLoopResult.outerLoopName(), OuterLoopStatus.UNSTABLE, runningContext.lastOuterLoopResult.statusText());
        }

        return buildAcLoadFlowResult(runningContext, outerLoopFinalResult, distributedActivePower);
    }

    private AcLoadFlowResult buildAcLoadFlowResult(RunningContext runningContext, OuterLoopResult outerLoopFinalResult, double distributedActivePower) {
        AcLoadFlowResult result = new AcLoadFlowResult(context.getNetwork(),
                                                       runningContext.outerLoopTotalIterations,
                                                       runningContext.nrTotalIterations.getValue(),
                                                       runningContext.lastSolverResult.getStatus(),
                                                       outerLoopFinalResult,
                                                       runningContext.lastSolverResult.getSlackBusActivePowerMismatch(),
                                                       distributedActivePower
                                                       );

        LOGGER.info("AC loadflow complete on network {} (result={})", context.getNetwork(), result);

        Reports.reportAcLfComplete(context.getNetwork().getReportNode(), result.isSuccess(), result.getSolverStatus().name(), result.getOuterLoopResult().status().name());

        context.setResult(result);

        return result;
    }

    public static List<AcLoadFlowResult> run(List<LfNetwork> lfNetworks, AcLoadFlowParameters parameters) {
        return lfNetworks.stream()
                .map(n -> {
                    if (n.getValidity() == LfNetwork.Validity.VALID) {
                        try (AcLoadFlowContext context = new AcLoadFlowContext(n, parameters)) {
                            return new AcloadFlowEngine(context)
                                    .run();
                        }
                    }
                    return AcLoadFlowResult.createNoCalculationResult(n);
                })
                .toList();
    }
}