All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.powsybl.openloadflow.network.impl.LfNetworkLoaderImpl Maven / Gradle / Ivy

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

import com.google.common.base.Stopwatch;
import com.powsybl.commons.PowsyblException;
import com.powsybl.commons.report.ReportNode;
import com.powsybl.iidm.network.*;
import com.powsybl.iidm.network.extensions.*;
import com.powsybl.openloadflow.network.*;
import com.powsybl.openloadflow.util.DebugUtil;
import com.powsybl.openloadflow.util.PerUnit;
import com.powsybl.openloadflow.util.Reports;
import net.jafama.FastMath;
import org.apache.commons.lang3.mutable.MutableInt;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.file.Path;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static com.powsybl.openloadflow.util.DebugUtil.DATE_TIME_FORMAT;
import static com.powsybl.openloadflow.util.Markers.PERFORMANCE_MARKER;

/**
 * @author Geoffroy Jamgotchian {@literal }
 */
public class LfNetworkLoaderImpl implements LfNetworkLoader {

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

    private static final double TARGET_V_EPSILON = 1e-2;

    private static final double TARGET_Q_EPSILON = 1e-2;

    private static class LoadingContext {

        private final Set> branchSet = new LinkedHashSet<>();

        private final List danglingLines = new ArrayList<>();

        private final Set t3wtSet = new LinkedHashSet<>();

        private final Set shuntSet = new LinkedHashSet<>();

        private final Set hvdcLineSet = new LinkedHashSet<>();

        private final Map areaTerminalMap = new HashMap<>();

        private final Map> areaBusMap = new HashMap<>();

        private final Map> areaBoundaries = new HashMap<>();
    }

    private final Supplier> postProcessorsSupplier;

    public LfNetworkLoaderImpl() {
        this(LfNetworkLoaderPostProcessor::findAll);
    }

    public LfNetworkLoaderImpl(Supplier> postProcessorsSupplier) {
        this.postProcessorsSupplier = Objects.requireNonNull(postProcessorsSupplier);
    }

    private static void createBuses(List buses, LfNetworkParameters parameters, LfNetwork lfNetwork, List lfBuses, LfTopoConfig topoConfig,
                                    LoadingContext loadingContext, LfNetworkLoadingReport report, List postProcessors) {
        for (Bus bus : buses) {
            LfBusImpl lfBus = createBus(bus, parameters, lfNetwork, topoConfig, loadingContext, report, postProcessors);
            postProcessors.forEach(pp -> pp.onBusAdded(bus, lfBus));
            lfNetwork.addBus(lfBus);
            lfBuses.add(lfBus);
        }
    }

    private static void createVoltageControls(List lfBuses, LfNetworkParameters parameters) {
        List voltageControls = new ArrayList<>();

        // set controller -> controlled link
        for (LfBus controllerBus : lfBuses) {

            List voltageControlGenerators = new ArrayList<>(1);
            List voltageMonitoringGenerators = new ArrayList<>(1);
            for (var generator : controllerBus.getGenerators()) {
                if (generator.getGeneratorControlType() == LfGenerator.GeneratorControlType.VOLTAGE) {
                    voltageControlGenerators.add(generator);
                } else if (generator.getGeneratorControlType() == LfGenerator.GeneratorControlType.MONITORING_VOLTAGE) {
                    voltageMonitoringGenerators.add(generator);
                }
            }
            if (voltageMonitoringGenerators.size() > 1) {
                String generatorIds = voltageMonitoringGenerators.stream().map(LfGenerator::getId).collect(Collectors.joining(", "));
                LOGGER.warn("We have several voltage monitors ({}) connected to the same bus: not supported. All switched to voltage control", generatorIds);
                voltageMonitoringGenerators.forEach(gen -> gen.setGeneratorControlType(LfGenerator.GeneratorControlType.VOLTAGE));
            }
            if (!voltageControlGenerators.isEmpty() && !voltageMonitoringGenerators.isEmpty()) {
                String generatorIds = voltageMonitoringGenerators.stream().map(LfGenerator::getId).collect(Collectors.joining(", "));
                LOGGER.warn("We have both voltage controllers and voltage monitors ({}) connected to the same bus: voltage monitoring discarded", generatorIds);
                voltageMonitoringGenerators.forEach(gen -> gen.setGeneratorControlType(LfGenerator.GeneratorControlType.OFF));
                voltageMonitoringGenerators.clear();
            }
            voltageControlGenerators.addAll(voltageMonitoringGenerators);

            if (!voltageControlGenerators.isEmpty()) {

                LfGenerator lfGenerator0 = voltageControlGenerators.get(0);
                LfBus controlledBus = lfGenerator0.getControlledBus();
                double controllerTargetV = lfGenerator0.getTargetV();

                voltageControlGenerators.stream().skip(1).forEach(lfGenerator -> {
                    LfBus generatorControlledBus = lfGenerator.getControlledBus();

                    // check that remote control bus is the same for the generators of current controller bus which have voltage control on
                    if (checkUniqueControlledBus(controlledBus, generatorControlledBus, controllerBus)) {
                        // check that target voltage is the same for the generators of current controller bus which have voltage control on
                        checkUniqueTargetVControllerBus(lfGenerator, controllerTargetV, controllerBus, generatorControlledBus);
                    }
                });

                if (parameters.isGeneratorVoltageRemoteControl() || controlledBus == controllerBus) {
                    controlledBus.getGeneratorVoltageControl().ifPresentOrElse(
                        vc -> updateGeneratorVoltageControl(vc, controllerBus, controllerTargetV),
                        () -> createGeneratorVoltageControl(controlledBus, controllerBus, controllerTargetV, voltageControls, parameters));
                } else {
                    // if voltage remote control deactivated and remote control, set local control instead
                    LOGGER.warn("Remote voltage control is not activated. The voltage target of {} with remote control is rescaled from {} to {}",
                            controllerBus.getId(), controllerTargetV, controllerTargetV * controllerBus.getNominalV() / controlledBus.getNominalV());
                    controlledBus.getGeneratorVoltageControl().ifPresentOrElse(
                        vc -> updateGeneratorVoltageControl(vc, controllerBus, controllerTargetV), // updating only to check targetV uniqueness
                        () -> createGeneratorVoltageControl(controllerBus, controllerBus, controllerTargetV, voltageControls, parameters));
                }
            }
        }

        if (parameters.isVoltagePerReactivePowerControl()) {
            voltageControls.forEach(LfNetworkLoaderImpl::checkGeneratorsWithSlope);
        }
    }

    private static void createGeneratorVoltageControl(LfBus controlledBus, LfBus controllerBus, double controllerTargetV, List voltageControls,
                                                      LfNetworkParameters parameters) {
        GeneratorVoltageControl voltageControl = new GeneratorVoltageControl(controlledBus, parameters.getVoltageTargetPriority(VoltageControl.Type.GENERATOR), controllerTargetV);
        voltageControl.addControllerElement(controllerBus);
        controlledBus.setGeneratorVoltageControl(voltageControl);
        if (parameters.isVoltagePerReactivePowerControl()) {
            voltageControls.add(voltageControl);
        }
        if (controllerBus.getGenerators().stream().anyMatch(gen -> gen.getGeneratorControlType() == LfGenerator.GeneratorControlType.MONITORING_VOLTAGE)) {
            controllerBus.setGeneratorVoltageControlEnabled(false);
        }
    }

    private static void updateGeneratorVoltageControl(GeneratorVoltageControl voltageControl, LfBus controllerBus, double controllerTargetV) {
        voltageControl.addControllerElement(controllerBus);
        checkUniqueTargetVControlledBus(controllerTargetV, controllerBus, voltageControl);
    }

    private static void checkGeneratorsWithSlope(GeneratorVoltageControl voltageControl) {
        List generatorsWithSlope = voltageControl.getControllerElements().stream()
                .filter(LfBus::hasGeneratorsWithSlope)
                .flatMap(lfBus -> lfBus.getGeneratorsControllingVoltageWithSlope().stream())
                .collect(Collectors.toList());

        if (!generatorsWithSlope.isEmpty()) {
            if (voltageControl.isSharedControl()) {
                generatorsWithSlope.forEach(generator -> generator.getBus().removeGeneratorSlopes());
                LOGGER.warn("Non supported: shared control on bus {} with {} generator(s) controlling voltage with slope. Slope set to 0 on all those generators",
                        voltageControl.getControlledBus(), generatorsWithSlope.size());
            } else if (!voltageControl.isLocalControl()) {
                generatorsWithSlope.forEach(generator -> generator.getBus().removeGeneratorSlopes());
                LOGGER.warn("Non supported: remote control on bus {} with {} generator(s) controlling voltage with slope",
                        voltageControl.getControlledBus(), generatorsWithSlope.size());
            }
        }
    }

    private static void checkUniqueTargetVControlledBus(double controllerTargetV, LfBus controllerBus, GeneratorVoltageControl vc) {
        // check if target voltage is consistent with other already existing controller buses
        double voltageControlTargetV = vc.getTargetValue();
        double deltaTargetV = FastMath.abs(voltageControlTargetV - controllerTargetV);
        LfBus controlledBus = vc.getControlledBus();
        if (deltaTargetV * controlledBus.getNominalV() > TARGET_V_EPSILON) {
            String busesId = vc.getControllerElements().stream().map(LfBus::getId).collect(Collectors.joining(", "));
            LOGGER.error("Bus '{}' control voltage of bus '{}' which is already controlled by buses '{}' with a different target voltage: {} (kept) and {} (ignored)",
                    controllerBus.getId(), controlledBus.getId(), busesId, controllerTargetV * controlledBus.getNominalV(),
                    voltageControlTargetV * controlledBus.getNominalV());
            Reports.reportBusAlreadyControlledWithDifferentTargetV(controllerBus.getNetwork().getReportNode(), controllerBus.getId(), controlledBus.getId(), busesId, controllerTargetV * controlledBus.getNominalV(),
                    voltageControlTargetV * controlledBus.getNominalV());
        }
    }

    private static boolean checkUniqueControlledBus(LfBus controlledBus, LfBus controlledBusGen, LfBus controller) {
        Objects.requireNonNull(controlledBus);
        Objects.requireNonNull(controlledBusGen);
        if (controlledBus.getNum() != controlledBusGen.getNum()) {
            String generatorIds = controller.getGenerators().stream().map(LfGenerator::getId).collect(Collectors.joining(", "));
            LOGGER.warn("Generators [{}] are connected to the same bus '{}' but control the voltage of different buses: {} (kept) and {} (rejected)",
                    generatorIds, controller.getId(), controlledBus.getId(), controlledBusGen.getId());
            Reports.reportNotUniqueControlledBus(controlledBus.getNetwork().getReportNode(), generatorIds, controller.getId(), controlledBus.getId(), controlledBusGen.getId());
            return false;
        }
        return true;
    }

    private static void checkUniqueTargetVControllerBus(LfGenerator lfGenerator, double previousTargetV, LfBus controllerBus, LfBus controlledBus) {
        double targetV = lfGenerator.getTargetV();
        if (FastMath.abs(previousTargetV - targetV) > TARGET_V_EPSILON) {
            String generatorIds = controllerBus.getGenerators().stream().map(LfGenerator::getId).collect(Collectors.joining(", "));
            LOGGER.error("Generators [{}] are connected to the same bus '{}' with different target voltages: {} (kept) and {} (rejected)",
                generatorIds, controllerBus.getId(), previousTargetV * controlledBus.getNominalV(), targetV * controlledBus.getNominalV());
            Reports.reportNotUniqueTargetVControllerBus(controllerBus.getNetwork().getReportNode(), generatorIds, controllerBus.getId(), previousTargetV * controlledBus.getNominalV(), targetV * controlledBus.getNominalV());
        }
    }

    private static void createGeneratorReactivePowerControls(List lfBuses) {
        for (LfBus controllerBus : lfBuses) {
            List generators = controllerBus.getGenerators().stream()
                    .filter(LfGenerator::hasRemoteReactivePowerControl).collect(Collectors.toList());
            if (!generators.isEmpty()) {
                Optional voltageControl = controllerBus.getGeneratorVoltageControl();
                if (voltageControl.isPresent()) {
                    LOGGER.warn("Bus {} has both voltage and remote reactive power controls: only voltage control is kept", controllerBus.getId());
                    continue;
                }
                LfGenerator generator = generators.get(0);
                if (checkControllerBusGenerators(generators, controllerBus.getId())) {
                    createGeneratorReactivePowerControl(generator.getControlledBranch(), generator.getControlledBranchSide(), generator.getRemoteTargetQ(), controllerBus);
                }
            }
        }
    }

    private static void createGeneratorReactivePowerControl(LfBranch controlledBranch, TwoSides side, double targetQ, LfBus controllerBus) {
        if (!controlledBranch.isConnectedAtBothSides()) {
            LOGGER.warn("Controlled branch '{}' must be connected at both sides: generator remote reactive power control discarded", controlledBranch.getId());
            return;
        }
        controlledBranch.getGeneratorReactivePowerControl().ifPresentOrElse(
                rpc -> {
                    if (checkUniqueControlledSide(rpc, side)) {
                        updateGeneratorReactivePowerControl(rpc, controllerBus, targetQ);
                    }
                },
                () -> createGeneratorReactivePowerControl(controlledBranch, controllerBus, side, targetQ)
        );

    }

    private static void createGeneratorReactivePowerControl(LfBranch controlledBranch, LfBus controllerBus, TwoSides controlledSide, double controllerTargetQ) {
        GeneratorReactivePowerControl generatorReactivePowerControl = new GeneratorReactivePowerControl(controlledBranch, controlledSide, controllerTargetQ);
        generatorReactivePowerControl.addControllerBus(controllerBus);
        controlledBranch.setGeneratorReactivePowerControl(generatorReactivePowerControl);
    }

    private static void updateGeneratorReactivePowerControl(GeneratorReactivePowerControl generatorReactivePowerControl, LfBus controllerBus, double controllerTargetQ) {
        checkUniqueTargetQControlledBranch(controllerTargetQ, controllerBus, generatorReactivePowerControl);
        generatorReactivePowerControl.addControllerBus(controllerBus);
    }

    private static boolean checkControllerBusGenerators(List lfGenerators, String controllerBusId) {
        LfGenerator lfGenerator = lfGenerators.get(0);
        LfBranch controlledBranch = lfGenerator.getControlledBranch();
        if (controlledBranch == null) {
            LOGGER.warn("Controlled branch is out of voltage or in a different synchronous component: remote reactive power control of generator {} discarded", lfGenerator.getId());
            return false;
        }
        TwoSides side = lfGenerator.getControlledBranchSide();
        double targetQ = lfGenerator.getRemoteTargetQ();
        for (int i = 1; i < lfGenerators.size(); i++) {
            LfGenerator otherGenerator = lfGenerators.get(i);
            if (otherGenerator.getControlledBranch() == null) {
                LOGGER.warn("Controlled branch is out of voltage or in a different synchronous component: remote reactive power control of generator {} discarded", otherGenerator.getId());
                return false;
            }
            if (!(controlledBranch.getId().equals(otherGenerator.getControlledBranch().getId())
                    && side.equals(otherGenerator.getControlledBranchSide())
                    && Math.abs(targetQ - otherGenerator.getRemoteTargetQ()) < TARGET_Q_EPSILON)) {
                LOGGER.error("Controller Bus '{}' has multiple generators with remote reactive power control." +
                                "But controls are not coherent between them: controls discarded",
                        controllerBusId);
                return false;
            }
        }
        return true;
    }

    private static boolean checkUniqueControlledSide(GeneratorReactivePowerControl generatorReactivePowerControl, TwoSides side) {
        if (!side.equals(generatorReactivePowerControl.getControlledSide())) {
            LOGGER.error("Controlled branch '{}' is controlled at both sides. Controlled side {} (kept) {} (rejected).",
                    generatorReactivePowerControl.getControlledBranch().getId(), generatorReactivePowerControl.getControlledSide(), side);
            Reports.reportBranchControlledAtBothSides(generatorReactivePowerControl.getControlledBranch().getNetwork().getReportNode(),
                    generatorReactivePowerControl.getControlledBranch().getId(), generatorReactivePowerControl.getControlledSide().name(), side.name());
            return false;
        }
        return true;
    }

    private static void checkUniqueTargetQControlledBranch(double controllerTargetQ, LfBus controllerBus, GeneratorReactivePowerControl generatorReactivePowerControl) {
        // check if targetQ is consistent with other already existing controller buses
        double reactivePowerControlTargetQ = generatorReactivePowerControl.getTargetValue();
        double deltaTargetQ = FastMath.abs(reactivePowerControlTargetQ - controllerTargetQ);
        if (deltaTargetQ > TARGET_Q_EPSILON) {
            String busesId = generatorReactivePowerControl.getControllerBuses().stream().map(LfBus::getId).collect(Collectors.joining(", "));
            LOGGER.error("Bus '{}' controls reactive power of a branch which is already controlled by buses '{}' with a different targetQ: {} (kept) and {} (ignored)",
                    controllerBus.getId(), busesId, reactivePowerControlTargetQ, controllerTargetQ);
        }
    }

    private static LfBusImpl createBus(Bus bus, LfNetworkParameters parameters, LfNetwork lfNetwork, LfTopoConfig topoConfig, LoadingContext loadingContext,
                                       LfNetworkLoadingReport report, List postProcessors) {
        LfBusImpl lfBus = LfBusImpl.create(bus, lfNetwork, parameters, participateToSlackDistribution(parameters, bus));

        List shuntCompensators = new ArrayList<>();

        updateArea(bus, lfBus, parameters, loadingContext);

        bus.visitConnectedEquipments(new DefaultTopologyVisitor() {

            private void visitBranch(Branch branch) {
                loadingContext.branchSet.add(branch);
            }

            @Override
            public void visitLine(Line line, TwoSides side) {
                visitBranch(line);
            }

            @Override
            public void visitTwoWindingsTransformer(TwoWindingsTransformer transformer, TwoSides side) {
                visitBranch(transformer);
            }

            @Override
            public void visitThreeWindingsTransformer(ThreeWindingsTransformer transformer, ThreeSides side) {
                loadingContext.t3wtSet.add(transformer);
            }

            @Override
            public void visitGenerator(Generator generator) {
                lfBus.addGenerator(generator, parameters, report);
                postProcessors.forEach(pp -> pp.onInjectionAdded(generator, lfBus));
            }

            @Override
            public void visitLoad(Load load) {
                lfBus.addLoad(load, parameters);
                postProcessors.forEach(pp -> pp.onInjectionAdded(load, lfBus));
            }

            @Override
            public void visitShuntCompensator(ShuntCompensator sc) {
                shuntCompensators.add(sc);
                postProcessors.forEach(pp -> pp.onInjectionAdded(sc, lfBus));
                if (parameters.isShuntVoltageControl()) {
                    loadingContext.shuntSet.add(sc);
                }
            }

            @Override
            public void visitDanglingLine(DanglingLine danglingLine) {
                loadingContext.danglingLines.add(danglingLine);
                postProcessors.forEach(pp -> pp.onInjectionAdded(danglingLine, lfBus));
            }

            @Override
            public void visitStaticVarCompensator(StaticVarCompensator staticVarCompensator) {
                lfBus.addStaticVarCompensator(staticVarCompensator, parameters, report);
                postProcessors.forEach(pp -> pp.onInjectionAdded(staticVarCompensator, lfBus));
            }

            @Override
            public void visitBattery(Battery battery) {
                lfBus.addBattery(battery, parameters, report);
                postProcessors.forEach(pp -> pp.onInjectionAdded(battery, lfBus));
            }

            @Override
            public void visitHvdcConverterStation(HvdcConverterStation converterStation) {
                switch (converterStation.getHvdcType()) {
                    case VSC:
                        VscConverterStation vscConverterStation = (VscConverterStation) converterStation;
                        lfBus.addVscConverterStation(vscConverterStation, parameters, report);
                        if (converterStation.getHvdcLine() != null) {
                            loadingContext.hvdcLineSet.add(converterStation.getHvdcLine());
                        }
                        break;
                    case LCC:
                        lfBus.addLccConverterStation((LccConverterStation) converterStation, parameters);
                        if (converterStation.getHvdcLine() != null) {
                            loadingContext.hvdcLineSet.add(converterStation.getHvdcLine());
                        }
                        break;
                    default:
                        throw new IllegalStateException("Unknown HVDC converter station type: " + converterStation.getHvdcType());
                }
                postProcessors.forEach(pp -> pp.onInjectionAdded(converterStation, lfBus));
            }
        });

        if (!shuntCompensators.isEmpty()) {
            lfBus.setShuntCompensators(shuntCompensators, parameters, topoConfig, report);
        }

        return lfBus;
    }

    private static void addBranch(LfNetwork lfNetwork, LfBranch lfBranch, LfNetworkLoadingReport report) {
        boolean connectedToSameBus = lfBranch.getBus1() == lfBranch.getBus2();
        if (connectedToSameBus) {
            LOGGER.trace("Discard branch '{}' because connected to same bus at both ends", lfBranch.getId());
            report.branchesDiscardedBecauseConnectedToSameBusAtBothEnds++;
        } else {
            if (Arrays.stream(LoadFlowModel.values()).anyMatch(lfBranch::isZeroImpedance)) {
                LOGGER.trace("Branch {} is non impedant", lfBranch.getId());
                report.nonImpedantBranches++;
            }
            lfNetwork.addBranch(lfBranch);
        }
    }

    private static void createBranches(List lfBuses, LfNetwork lfNetwork, LfTopoConfig topoConfig, LoadingContext loadingContext,
                                       LfNetworkLoadingReport report, LfNetworkParameters parameters,
                                       List postProcessors) {
        for (Branch branch : loadingContext.branchSet) {
            LfBus lfBus1 = getLfBus(branch.getTerminal1(), lfNetwork, parameters.isBreakers());
            LfBus lfBus2 = getLfBus(branch.getTerminal2(), lfNetwork, parameters.isBreakers());
            LfBranchImpl lfBranch = LfBranchImpl.create(branch, lfNetwork, lfBus1, lfBus2, topoConfig, parameters);
            addBranch(lfNetwork, lfBranch, report);
            addBranchAreaBoundaries(branch, lfBranch, loadingContext);
            postProcessors.forEach(pp -> pp.onBranchAdded(branch, lfBranch));
        }

        Set visitedDanglingLinesIds = new HashSet<>();
        for (DanglingLine danglingLine : loadingContext.danglingLines) {
            danglingLine.getTieLine().ifPresentOrElse(tieLine -> {
                if (!visitedDanglingLinesIds.contains(danglingLine.getId())) {
                    LfBus lfBus1 = getLfBus(tieLine.getDanglingLine1().getTerminal(), lfNetwork, parameters.isBreakers());
                    LfBus lfBus2 = getLfBus(tieLine.getDanglingLine2().getTerminal(), lfNetwork, parameters.isBreakers());
                    LfBranch lfBranch = LfTieLineBranch.create(tieLine, lfNetwork, lfBus1, lfBus2, parameters);
                    addBranch(lfNetwork, lfBranch, report);
                    addBranchAreaBoundaries(tieLine, lfBranch, loadingContext);
                    postProcessors.forEach(pp -> pp.onBranchAdded(tieLine, lfBranch));
                    visitedDanglingLinesIds.add(tieLine.getDanglingLine1().getId());
                    visitedDanglingLinesIds.add(tieLine.getDanglingLine2().getId());
                }
            }, () -> {
                    LfDanglingLineBus lfBus2 = new LfDanglingLineBus(lfNetwork, danglingLine, parameters, report);
                    lfNetwork.addBus(lfBus2);
                    lfBuses.add(lfBus2);
                    LfBus lfBus1 = getLfBus(danglingLine.getTerminal(), lfNetwork, parameters.isBreakers());
                    LfBranch lfBranch = LfDanglingLineBranch.create(danglingLine, lfNetwork, lfBus1, lfBus2, parameters);
                    addBranch(lfNetwork, lfBranch, report);
                    addDanglingLineAreaBoundary(danglingLine, lfBranch, loadingContext);
                    postProcessors.forEach(pp -> {
                        pp.onBusAdded(danglingLine, lfBus2);
                        pp.onBranchAdded(danglingLine, lfBranch);
                    });
            });
        }

        for (ThreeWindingsTransformer t3wt : loadingContext.t3wtSet) {
            LfStarBus lfBus0 = new LfStarBus(lfNetwork, t3wt, parameters);
            lfNetwork.addBus(lfBus0);
            postProcessors.forEach(pp -> pp.onBusAdded(t3wt, lfBus0));
            for (ThreeSides side : ThreeSides.values()) {
                ThreeWindingsTransformer.Leg leg = t3wt.getLeg(side);
                LfBus lfBus = getLfBus(leg.getTerminal(), lfNetwork, parameters.isBreakers());
                LfLegBranch lfBranch = LfLegBranch.create(lfNetwork, lfBus, lfBus0, t3wt, leg, topoConfig, parameters);
                addBranch(lfNetwork, lfBranch, report);
                postProcessors.forEach(pp -> pp.onBranchAdded(t3wt, lfBranch));
            }
        }

        if (parameters.isPhaseControl()) {
            for (Branch branch : loadingContext.branchSet) {
                if (branch instanceof TwoWindingsTransformer t2wt) {
                    // Create phase controls which link controller -> controlled
                    PhaseTapChanger ptc = t2wt.getPhaseTapChanger();
                    createPhaseControl(lfNetwork, ptc, t2wt.getId(), parameters);
                }
            }
            for (ThreeWindingsTransformer t3wt : loadingContext.t3wtSet) {
                // Create phase controls which link controller -> controlled
                for (ThreeSides side : ThreeSides.values()) {
                    PhaseTapChanger ptc = t3wt.getLeg(side).getPhaseTapChanger();
                    createPhaseControl(lfNetwork, ptc, LfLegBranch.getId(side, t3wt.getId()), parameters);
                }
            }
        }

        for (HvdcLine hvdcLine : loadingContext.hvdcLineSet) {
            LfBus lfBus1 = getLfBus(hvdcLine.getConverterStation1().getTerminal(), lfNetwork, parameters.isBreakers());
            LfBus lfBus2 = getLfBus(hvdcLine.getConverterStation2().getTerminal(), lfNetwork, parameters.isBreakers());
            LfVscConverterStationImpl cs1 = (LfVscConverterStationImpl) lfNetwork.getGeneratorById(hvdcLine.getConverterStation1().getId());
            LfVscConverterStationImpl cs2 = (LfVscConverterStationImpl) lfNetwork.getGeneratorById(hvdcLine.getConverterStation2().getId());
            if (cs1 != null && cs2 != null) {
                LfHvdc lfHvdc = new LfHvdcImpl(hvdcLine.getId(), lfBus1, lfBus2, lfNetwork, hvdcLine, parameters.isHvdcAcEmulation());
                lfHvdc.setConverterStation1(cs1);
                lfHvdc.setConverterStation2(cs2);
                lfNetwork.addHvdc(lfHvdc);
            } else {
                LOGGER.warn("The converter stations of hvdc line {} are not in the same synchronous component: no hvdc link created to model active power flow.", hvdcLine.getId());
            }
        }
    }

    private static void updateArea(Bus bus, LfBus lfBus, LfNetworkParameters parameters, LoadingContext loadingContext) {
        if (parameters.isAreaInterchangeControl()) {
            // Consider only the area type that should be used for area interchange control
            Optional areaOpt = bus.getVoltageLevel().getArea(parameters.getAreaInterchangeControlAreaType());
            areaOpt.ifPresent(area ->
                loadingContext.areaBusMap.computeIfAbsent(area, k -> {
                    area.getAreaBoundaryStream().forEach(boundary -> {
                        boundary.getTerminal().ifPresent(t -> loadingContext.areaTerminalMap.put(t, area));
                        boundary.getBoundary().ifPresent(b -> loadingContext.areaTerminalMap.put(b.getDanglingLine().getTerminal(), area));
                    });
                    return new HashSet<>();
                }).add(lfBus)
            );
        }
    }

    /**
     * Add the terminals active power to the calculation of their Area's interchange (load convention) if they are boundaries.
     * For simple branches (lines, transformers, switches), the side declared as the boundary must be the one which active power is used.
     * For tie lines, the side declared as the boundary must be the one on the side of the concerned area.
     */
    private static void addBranchAreaBoundaries(Branch branch, LfBranch lfBranch, LoadingContext loadingContext) {
        addAreaBoundary(branch.getTerminal1(), lfBranch, TwoSides.ONE, loadingContext);
        addAreaBoundary(branch.getTerminal2(), lfBranch, TwoSides.TWO, loadingContext);
    }

    /**
     * Adds the active power of the terminal of the dangling line to the calculation of the Area's interchange (load convention) if it is a boundary.
     * The equivalent injection of the dangling line lfBranch model is P2;
     */
    private static void addDanglingLineAreaBoundary(DanglingLine danglingLine, LfBranch lfDanglingLineBranch, LoadingContext loadingContext) {
        addAreaBoundary(danglingLine.getTerminal(), lfDanglingLineBranch, TwoSides.TWO, loadingContext);
    }

    private static void addAreaBoundary(Terminal terminal, LfBranch branch, TwoSides side, LoadingContext loadingContext) {
        if (loadingContext.areaTerminalMap.containsKey(terminal)) {
            Area area = loadingContext.areaTerminalMap.get(terminal);
            loadingContext.areaBoundaries.computeIfAbsent(area, k -> new HashSet<>()).add(new LfAreaImpl.BoundaryImpl(branch, side));
        }
    }

    private static void createAreas(LfNetwork network, LoadingContext loadingContext, List postProcessors, LfNetworkParameters parameters) {
        if (parameters.isAreaInterchangeControl()) {
            loadingContext.areaBusMap
                    .entrySet()
                    .stream()
                    .filter(e -> {
                        if (e.getKey().getAreaBoundaryStream().findAny().isEmpty()) {
                            Reports.reportAreaNoInterchangeControl(network.getReportNode(), e.getKey().getId(), "Area does not have any area boundary");
                            LOGGER.warn("Network {}: Area {} does not have any area boundary. The area will not be considered for area interchange control", network, e.getKey().getId());
                            return false;
                        }
                        return true;
                    })
                    .filter(e -> {
                        if (e.getKey().getInterchangeTarget().isEmpty()) {
                            Reports.reportAreaNoInterchangeControl(network.getReportNode(), e.getKey().getId(), "Area does not have an interchange target");
                            LOGGER.warn("Network {}: Area {} does not have an interchange target. The area will not be considered for area interchange control", network, e.getKey().getId());
                            return false;
                        }
                        return true;
                    })
                    .filter(e -> checkBoundariesComponent(network, e.getKey()))
                    .forEach(e -> {
                        Area area = e.getKey();
                        Set lfBuses = e.getValue();
                        Set boundaries = loadingContext.areaBoundaries.getOrDefault(area, new HashSet<>());
                        LfArea lfArea = LfAreaImpl.create(area, lfBuses, boundaries, network, parameters);
                        network.addArea(lfArea);
                        postProcessors.forEach(pp -> pp.onAreaAdded(area, lfArea));
                    });
        }
    }

    private static boolean checkBoundariesComponent(LfNetwork network, Area area) {
        final int numCC = network.getNumCC();
        final int numSC = network.getNumSC();
        List boundaryBuses = area.getAreaBoundaryStream()
                .map(areaBoundary -> areaBoundary.getTerminal()
                        .orElseGet(() -> areaBoundary.getBoundary().orElseThrow().getDanglingLine().getTerminal()))
                .map(t -> t.getBusView().getBus())
                .filter(Objects::nonNull)
                .toList();

        List connectedComponents = boundaryBuses.stream()
                .map(Bus::getConnectedComponent)
                .filter(Objects::nonNull)
                .map(Component::getNum)
                .distinct()
                .sorted()
                .toList();

        List synchronousComponents = boundaryBuses.stream()
                .map(Bus::getSynchronousComponent)
                .filter(Objects::nonNull)
                .map(Component::getNum)
                .distinct()
                .sorted()
                .toList();

        if (connectedComponents.size() > 1 && synchronousComponents.size() > 1) {
            if (connectedComponents.get(0) == numCC && synchronousComponents.get(0) == numSC) {
                // to avoid logging the same warn multiple times
                Reports.reportAreaNoInterchangeControl(network.getReportNode(), area.getId(), "Area does not have all its boundary buses in the same connected component or synchronous component");
                LOGGER.warn("Network {}: Area {} does not have all its boundary buses in the same connected component or synchronous component. The area will not be considered for area interchange control", network, area.getId());
            }
            return false;
        } else if (!connectedComponents.contains(numCC) || !synchronousComponents.contains(numSC)) {
            // only debug level and do report here, this is very common on real networks and would just clutter the logs/reports
            LOGGER.debug("Network {}: Area {} has buses in component ({}, {}) but has no boundary in it, this part of the area will not be considered for area interchange control", network, area.getId(), numCC, numSC);
            return false;
        }
        return true;
    }

    private static void createTransformersVoltageControls(LfNetwork lfNetwork, LfNetworkParameters parameters, LoadingContext loadingContext,
                                                          LfNetworkLoadingReport report) {
        // Create discrete voltage controls which link controller -> controlled
        for (Branch branch : loadingContext.branchSet) {
            if (branch instanceof TwoWindingsTransformer t2wt) {
                RatioTapChanger rtc = t2wt.getRatioTapChanger();
                createTransformerVoltageControl(lfNetwork, rtc, t2wt.getId(), parameters, report);
            }
        }
        for (ThreeWindingsTransformer t3wt : loadingContext.t3wtSet) {
            for (ThreeSides side : ThreeSides.values()) {
                RatioTapChanger rtc = t3wt.getLeg(side).getRatioTapChanger();
                createTransformerVoltageControl(lfNetwork, rtc, LfLegBranch.getId(side, t3wt.getId()), parameters, report);
            }
        }
    }

    private static void createTransformerReactivePowerControls(LfNetwork lfNetwork, LfNetworkParameters parameters, LoadingContext loadingContext,
                                                               LfNetworkLoadingReport report) {
        // Create reactive power control which link controller -> controlled
        for (Branch branch : loadingContext.branchSet) {
            if (branch instanceof TwoWindingsTransformer t2wt) {
                RatioTapChanger rtc = t2wt.getRatioTapChanger();
                createTransformerReactivePowerControl(lfNetwork, rtc, t2wt.getId(), parameters, report);
            }
        }
        for (ThreeWindingsTransformer t3wt : loadingContext.t3wtSet) {
            for (ThreeSides side : ThreeSides.values()) {
                RatioTapChanger rtc = t3wt.getLeg(side).getRatioTapChanger();
                createTransformerReactivePowerControl(lfNetwork, rtc, LfLegBranch.getId(side, t3wt.getId()), parameters, report);
            }
        }
    }

    private static void createSwitches(List switches, LfNetwork lfNetwork, List postProcessors,
                                       LfNetworkParameters parameters, LfNetworkLoadingReport report) {
        if (switches != null) {
            for (Switch sw : switches) {
                VoltageLevel vl = sw.getVoltageLevel();
                Bus bus1 = vl.getBusBreakerView().getBus1(sw.getId());
                Bus bus2 = vl.getBusBreakerView().getBus2(sw.getId());
                LfBus lfBus1 = lfNetwork.getBusById(bus1.getId());
                LfBus lfBus2 = lfNetwork.getBusById(bus2.getId());
                LfSwitch lfSwitch = new LfSwitch(lfNetwork, lfBus1, lfBus2, sw, parameters);
                addBranch(lfNetwork, lfSwitch, report);
                postProcessors.forEach(pp -> pp.onBranchAdded(sw, lfSwitch));
            }
        }
    }

    private static void createPhaseControl(LfNetwork lfNetwork, PhaseTapChanger ptc, String controllerBranchId,
                                           LfNetworkParameters parameters) {
        if (ptc != null && ptc.isRegulating() && ptc.getRegulationMode() != PhaseTapChanger.RegulationMode.FIXED_TAP) {
            String controlledBranchId = ptc.getRegulationTerminal().getConnectable().getId();
            if (ptc.getRegulationTerminal().getConnectable() instanceof ThreeWindingsTransformer twt) {
                controlledBranchId = LfLegBranch.getId(twt.getSide(ptc.getRegulationTerminal()), controlledBranchId);
            }
            LfBranch controlledBranch = lfNetwork.getBranchById(controlledBranchId);
            if (controlledBranch == null) {
                LOGGER.warn("Phase controlled branch '{}' is out of voltage or in a different synchronous component: phase control discarded", controlledBranchId);
                return;
            }
            if (!controlledBranch.isConnectedAtBothSides()) { // FIXME, should be managed later.
                LOGGER.warn("Phase controlled branch '{}' is open: phase control discarded", controlledBranch.getId());
                return;
            }
            LfBranch controllerBranch = lfNetwork.getBranchById(controllerBranchId);
            if (controllerBranch.getBus1() == null || controllerBranch.getBus2() == null) {
                LOGGER.warn("Phase controller branch '{}' is open: phase control discarded", controllerBranch.getId());
                return;
            }
            if (ptc.getRegulationTerminal().getBusView().getBus() == null) {
                LOGGER.warn("Regulating terminal of phase controller branch '{}' is out of voltage: phase control discarded", controllerBranch.getId());
                return;
            }
            LfBus controlledBus = getLfBus(ptc.getRegulationTerminal(), lfNetwork, parameters.isBreakers());
            TwoSides controlledSide = controlledBus == controlledBranch.getBus1() ? TwoSides.ONE : TwoSides.TWO;
            if (controlledBranch instanceof LfLegBranch && controlledBus == controlledBranch.getBus2()) {
                throw new IllegalStateException("Leg " + controlledBranch.getId() + " has a non supported control at star bus side");
            }
            double targetValue;
            double targetDeadband;
            TransformerPhaseControl phaseControl = null;
            if (ptc.getRegulationMode() == PhaseTapChanger.RegulationMode.CURRENT_LIMITER) {
                if (controlledBranch == controllerBranch && controlledBus != null) {
                    targetValue = ptc.getRegulationValue() / PerUnit.ib(controlledBus.getNominalV());
                    targetDeadband = ptc.getTargetDeadband() / PerUnit.ib(controlledBus.getNominalV());
                    phaseControl = new TransformerPhaseControl(controllerBranch, controlledBranch, controlledSide,
                            TransformerPhaseControl.Mode.LIMITER, targetValue, targetDeadband, TransformerPhaseControl.Unit.A);
                } else {
                    LOGGER.warn("Branch {} limits current limiter on remote branch {}: not supported yet", controllerBranch.getId(), controlledBranch.getId());
                }
            } else if (ptc.getRegulationMode() == PhaseTapChanger.RegulationMode.ACTIVE_POWER_CONTROL) {
                targetValue = ptc.getRegulationValue() / PerUnit.SB;
                targetDeadband = ptc.getTargetDeadband() / PerUnit.SB;
                phaseControl = new TransformerPhaseControl(controllerBranch, controlledBranch, controlledSide,
                        TransformerPhaseControl.Mode.CONTROLLER, targetValue, targetDeadband, TransformerPhaseControl.Unit.MW);
            }
            controllerBranch.setPhaseControl(phaseControl);
            controlledBranch.setPhaseControl(phaseControl);
        }
    }

    private static void createTransformerVoltageControl(LfNetwork lfNetwork, RatioTapChanger rtc, String controllerBranchId,
                                                        LfNetworkParameters parameters, LfNetworkLoadingReport report) {
        if (rtc == null || !rtc.isRegulating() || !rtc.hasLoadTapChangingCapabilities() || rtc.getRegulationMode() != RatioTapChanger.RegulationMode.VOLTAGE) {
            return;
        }
        LfBranch controllerBranch = lfNetwork.getBranchById(controllerBranchId);
        if (!controllerBranch.isConnectedAtBothSides()) { // FIXME, should be managed later.
            LOGGER.trace("Voltage controller branch '{}' is open: voltage control discarded", controllerBranch.getId());
            report.transformerVoltageControlDiscardedBecauseControllerBranchIsOpen++;
            return;
        }
        LfBus controlledBus = getLfBus(rtc.getRegulationTerminal(), lfNetwork, parameters.isBreakers());
        if (controlledBus == null) {
            LOGGER.warn("Regulating terminal of voltage controller branch '{}' is out of voltage or in a different synchronous component: voltage control discarded", controllerBranch.getId());
            return;
        }

        double regulatingTerminalNominalV = rtc.getRegulationTerminal().getVoltageLevel().getNominalV();
        double targetValue = rtc.getTargetV() / regulatingTerminalNominalV;
        Double targetDeadband = rtc.getTargetDeadband() > 0 ? rtc.getTargetDeadband() / regulatingTerminalNominalV : null;

        if (!VoltageControl.checkTargetV(targetValue, controlledBus.getNominalV(), parameters)) {
            LOGGER.trace("RatioTapChanger on transformer '{}' has an inconsistent target voltage: {} pu: transformer voltage control discarded", controllerBranchId, targetValue);
            if (report != null) {
                report.transformersWithInconsistentTargetVoltage++;
            }
            return;
        }

        controlledBus.getTransformerVoltageControl().ifPresentOrElse(vc -> {
            LOGGER.trace("Controlled bus '{}' already has a transformer voltage control: a shared control is created", controlledBus.getId());
            if (FastMath.abs(vc.getTargetValue() - targetValue) > TARGET_V_EPSILON) {
                LOGGER.warn("Controlled bus '{}' already has a transformer voltage control with a different target voltage: {} and {}",
                        controlledBus.getId(), vc.getTargetValue(), targetValue);
                Reports.reportTransformerControlAlreadyExistsWithDifferentTargetV(controlledBus.getNetwork().getReportNode(),
                        vc.getControllerElements().get(0).getId(), controllerBranch.getId(), controlledBus.getId(),
                        controlledBus.getNominalV() * vc.getTargetValue(), controlledBus.getNominalV() * targetValue);
            }
            vc.addControllerElement(controllerBranch);
            controllerBranch.setVoltageControl(vc);
            if (targetDeadband != null) {
                Double oldTargetDeadband = vc.getTargetDeadband().orElse(null);
                // merge target deadbands by taking minimum
                double newTargetDeadband = oldTargetDeadband == null ? targetDeadband : Math.min(oldTargetDeadband, targetDeadband);
                vc.setTargetDeadband(newTargetDeadband);
                if (oldTargetDeadband == null || newTargetDeadband != oldTargetDeadband) {
                    Reports.reportTransformerControlAlreadyExistsUpdateDeadband(controlledBus.getNetwork().getReportNode(),
                            vc.getControllerElements().get(0).getId(), controllerBranch.getId(), controlledBus.getId(),
                            controlledBus.getNominalV() * newTargetDeadband, oldTargetDeadband == null ? null : controlledBus.getNominalV() * oldTargetDeadband);
                }
            }
        }, () -> {
                TransformerVoltageControl voltageControl = new TransformerVoltageControl(controlledBus, parameters.getVoltageTargetPriority(VoltageControl.Type.TRANSFORMER), targetValue, targetDeadband);
                voltageControl.addControllerElement(controllerBranch);
                controllerBranch.setVoltageControl(voltageControl);
                controlledBus.setTransformerVoltageControl(voltageControl);
            });
    }

    private static void createTransformerReactivePowerControl(LfNetwork lfNetwork, RatioTapChanger rtc, String controllerBranchId,
                                                              LfNetworkParameters parameters, LfNetworkLoadingReport report) {
        if (rtc == null || !rtc.isRegulating() || !rtc.hasLoadTapChangingCapabilities() || rtc.getRegulationMode() != RatioTapChanger.RegulationMode.REACTIVE_POWER) {
            return;
        }
        // Check on controller branch
        LfBranch controllerBranch = lfNetwork.getBranchById(controllerBranchId);
        if (!controllerBranch.isConnectedAtBothSides()) {
            LOGGER.trace("Reactive power controller branch '{}' is open: transformer reactive power control discarded", controllerBranchId);
            report.transformerReactivePowerControlDiscardedBecauseControllerBranchIsOpen++;
            return;
        }

        // Get controlled branch
        String controlledBranchId = rtc.getRegulationTerminal().getConnectable().getId();
        if (rtc.getRegulationTerminal().getConnectable() instanceof ThreeWindingsTransformer twt) {
            controlledBranchId = LfLegBranch.getId(twt.getSide(rtc.getRegulationTerminal()), controlledBranchId);
        }
        LfBranch controlledBranch = lfNetwork.getBranchById(controlledBranchId);

        // Check on controlled branch
        if (controlledBranch == null) {
            LOGGER.warn("Reactive power controlled branch '{}' is out of voltage or in a different synchronous component: transformer reactive power control discarded", controlledBranchId);
            return;
        }

        if (!controlledBranch.isConnectedAtBothSides()) {
            LOGGER.warn("Reactive power controlled branch '{}' is open: transformer reactive power control discarded", controlledBranchId);
            return;
        }

        TwoSides controlledSide = getLfBus(rtc.getRegulationTerminal(), lfNetwork, parameters.isBreakers()) == controlledBranch.getBus1() ? TwoSides.ONE : TwoSides.TWO;
        double targetValue = rtc.getRegulationValue() / PerUnit.SB;
        double targetDeadband = rtc.getTargetDeadband() / PerUnit.SB;

        controlledBranch.getTransformerReactivePowerControl().ifPresentOrElse(transformerReactivePowerControl ->
            LOGGER.warn("Controlled branch '{}' already has a transformer reactive power control: not implemented yet.", controlledBranch.getId()),
                () -> {
                    TransformerReactivePowerControl reactivePowerControl = new TransformerReactivePowerControl(controlledBranch, controlledSide, controllerBranch, targetValue, targetDeadband);
                    controllerBranch.setTransformerReactivePowerControl(reactivePowerControl);
                    controlledBranch.setTransformerReactivePowerControl(reactivePowerControl);
                });
    }

    private static void createShuntVoltageControl(LfNetwork lfNetwork, ShuntCompensator shuntCompensator, LfNetworkParameters parameters) {
        if (!shuntCompensator.isVoltageRegulatorOn()) {
            return;
        }
        LfBus controllerBus = getLfBus(shuntCompensator.getTerminal(), lfNetwork, parameters.isBreakers());
        if (controllerBus == null) {
            LOGGER.warn("Voltage controller shunt {} is out of voltage: no voltage control created", shuntCompensator.getId());
            return;
        }
        controllerBus.getControllerShunt().ifPresent(controllerShunt -> {
            LfBus controlledBus = getLfBus(shuntCompensator.getRegulatingTerminal(), lfNetwork, parameters.isBreakers());
            if (controlledBus == null) {
                LOGGER.warn("Regulating terminal of voltage controller shunt {} is out of voltage: no voltage control created", shuntCompensator.getId());
                controllerShunt.setVoltageControlCapability(false);
                return;
            }
            if (controllerShunt.getVoltageControl().isPresent()) {
                // if a controller shunt is already in a shunt voltage control, the number of equations will not equal the
                // number of variables. We have only one B variable for more than one bus target V equations.
                if (!controllerShunt.getVoltageControl().orElseThrow().getControlledBus().getId().equals(controlledBus.getId())) {
                    LOGGER.error("Controller shunt {} is already in a shunt voltage control. The second controlled bus {} is ignored", controllerShunt.getId(), controlledBus.getId());
                    Reports.reportControllerShuntAlreadyInVoltageControl(controllerBus.getNetwork().getReportNode(), controllerShunt.getId(), controlledBus.getId());
                }
                return;
            }

            double regulatingTerminalNominalV = shuntCompensator.getRegulatingTerminal().getVoltageLevel().getNominalV();
            double targetValue = shuntCompensator.getTargetV() / regulatingTerminalNominalV;
            Double targetDeadband = shuntCompensator.getTargetDeadband() > 0 ? shuntCompensator.getTargetDeadband() / regulatingTerminalNominalV : null;

            controlledBus.getShuntVoltageControl().ifPresentOrElse(voltageControl -> {
                LOGGER.trace("Controlled bus {} has already a shunt voltage control: a shared control is created", controlledBus.getId());
                if (FastMath.abs(voltageControl.getTargetValue() - targetValue) > TARGET_V_EPSILON) {
                    LOGGER.warn("Controlled bus {} already has a shunt voltage control with a different target voltage: {} and {}",
                            controlledBus.getId(), voltageControl.getTargetValue(), targetValue);
                }
                if (!voltageControl.getControllerElements().contains(controllerShunt)) {
                    voltageControl.addControllerElement(controllerShunt);
                    controllerShunt.setVoltageControl(voltageControl);
                    controlledBus.setShuntVoltageControl(voltageControl);
                    if (targetDeadband != null) {
                        Double oldTargetDeadband = voltageControl.getTargetDeadband().orElse(null);
                        if (oldTargetDeadband == null) {
                            voltageControl.setTargetDeadband(targetDeadband);
                        } else {
                            // merge target deadbands by taking minimum
                            voltageControl.setTargetDeadband(Math.min(oldTargetDeadband, targetDeadband));
                        }
                    }
                }
            }, () -> {
                // we create a new shunt voltage control.
                ShuntVoltageControl voltageControl = new ShuntVoltageControl(controlledBus, parameters.getVoltageTargetPriority(VoltageControl.Type.SHUNT), targetValue, targetDeadband);
                voltageControl.addControllerElement(controllerShunt);
                controllerShunt.setVoltageControl(voltageControl);
                controlledBus.setShuntVoltageControl(voltageControl);
            });
        });
    }

    private static LfBus getLfBus(Terminal terminal, LfNetwork lfNetwork, boolean breakers) {
        Bus bus = Networks.getBus(terminal, breakers);
        return bus != null ? lfNetwork.getBusById(bus.getId()) : null;
    }

    private LfNetwork create(int numCC, int numSC, Network network, List buses, List switches, LfTopoConfig topoConfig, LfNetworkParameters parameters, ReportNode reportNode) {
        LfNetwork lfNetwork = new LfNetwork(numCC, numSC, parameters.getSlackBusSelector(), parameters.getMaxSlackBusCount(),
                parameters.getConnectivityFactory(), parameters.getReferenceBusSelector(), reportNode);

        LoadingContext loadingContext = new LoadingContext();
        LfNetworkLoadingReport report = new LfNetworkLoadingReport();
        List postProcessors = postProcessorsSupplier.get().stream()
                .filter(pp -> pp.getLoadingPolicy() == LfNetworkLoaderPostProcessor.LoadingPolicy.ALWAYS
                        || pp.getLoadingPolicy() == LfNetworkLoaderPostProcessor.LoadingPolicy.SELECTION && parameters.getLoaderPostProcessorSelection().contains(pp.getName()))
                .collect(Collectors.toList());

        List lfBuses = new ArrayList<>();
        createBuses(buses, parameters, lfNetwork, lfBuses, topoConfig, loadingContext, report, postProcessors);
        createBranches(lfBuses, lfNetwork, topoConfig, loadingContext, report, parameters, postProcessors);
        createAreas(lfNetwork, loadingContext, postProcessors, parameters);

        if (parameters.getLoadFlowModel() == LoadFlowModel.AC) {
            createVoltageControls(lfBuses, parameters);
            if (parameters.isGeneratorReactivePowerRemoteControl()) {
                createGeneratorReactivePowerControls(lfBuses);
            }
            if (parameters.isTransformerVoltageControl()) {
                // Discrete voltage controls need to be created after voltage controls (to test if both generator and transformer voltage control are on)
                createTransformersVoltageControls(lfNetwork, parameters, loadingContext, report);
            }
            if (parameters.isTransformerReactivePowerControl()) {
                createTransformerReactivePowerControls(lfNetwork, parameters, loadingContext, report);
            }
            if (parameters.isShuntVoltageControl()) {
                for (ShuntCompensator shunt : loadingContext.shuntSet) {
                    createShuntVoltageControl(lfNetwork, shunt, parameters);
                }
            }
        }

        if (parameters.isBreakers()) {
            createSwitches(switches, lfNetwork, postProcessors, parameters, report);
        }

        // secondary voltage controls
        createSecondaryVoltageControls(network, parameters, lfNetwork);

        // voltage angle limits
        createVoltageAngleLimits(network, lfNetwork, parameters);

        if (parameters.isSimulateAutomationSystems()) {
            createAutomationSystems(network, lfNetwork);
        }

        if (report.generatorsDiscardedFromVoltageControlBecauseNotStarted > 0) {
            Reports.reportGeneratorsDiscardedFromVoltageControlBecauseNotStarted(reportNode, report.generatorsDiscardedFromVoltageControlBecauseNotStarted);
            LOGGER.warn("Network {}: {} generators have been discarded from voltage control because not started",
                    lfNetwork, report.generatorsDiscardedFromVoltageControlBecauseNotStarted);
        }
        if (report.generatorsDiscardedFromVoltageControlBecauseReactiveRangeIsTooSmall > 0) {
            Reports.reportGeneratorsDiscardedFromVoltageControlBecauseReactiveRangeIsTooSmall(reportNode, report.generatorsDiscardedFromVoltageControlBecauseReactiveRangeIsTooSmall);
            LOGGER.warn("Network {}: {} generators have been discarded from voltage control because of a too small reactive range",
                    lfNetwork, report.generatorsDiscardedFromVoltageControlBecauseReactiveRangeIsTooSmall);
        }
        if (report.generatorsDiscardedFromVoltageControlBecauseTargetPIsOutsideActiveLimits > 0) {
            Reports.reportGeneratorsDiscardedFromVoltageControlBecauseTargetPIsOutsideActiveLimits(reportNode, report.generatorsDiscardedFromVoltageControlBecauseTargetPIsOutsideActiveLimits);
            LOGGER.warn("Network {}: {} generators have been discarded from voltage control because targetP is outside active power limits",
                    lfNetwork, report.generatorsDiscardedFromVoltageControlBecauseTargetPIsOutsideActiveLimits);
        }
        if (report.generatorsDiscardedFromActivePowerControlBecauseTargetEqualsToZero > 0) {
            LOGGER.warn("Network {}: {} generators have been discarded from active power control because of a targetP equals 0",
                    lfNetwork, report.generatorsDiscardedFromActivePowerControlBecauseTargetEqualsToZero);
        }
        if (report.generatorsDiscardedFromActivePowerControlBecauseTargetPGreaterThanMaxP > 0) {
            LOGGER.warn("Network {}: {} generators have been discarded from active power control because of a targetP > maxP",
                    lfNetwork, report.generatorsDiscardedFromActivePowerControlBecauseTargetPGreaterThanMaxP);
        }
        if (report.generatorsDiscardedFromActivePowerControlBecauseMaxPNotPlausible > 0) {
            LOGGER.warn("Network {}: {} generators have been discarded from active power control because of maxP not plausible",
                    lfNetwork, report.generatorsDiscardedFromActivePowerControlBecauseMaxPNotPlausible);
        }
        if (report.generatorsDiscardedFromActivePowerControlBecauseMaxPEqualsMinP > 0) {
            LOGGER.warn("Network {}: {} generators have been discarded from active power control because of maxP equals to minP",
                    lfNetwork, report.generatorsDiscardedFromActivePowerControlBecauseMaxPEqualsMinP);
        }
        if (report.branchesDiscardedBecauseConnectedToSameBusAtBothEnds > 0) {
            LOGGER.warn("Network {}: {} branches have been discarded because connected to same bus at both ends",
                    lfNetwork, report.branchesDiscardedBecauseConnectedToSameBusAtBothEnds);
        }
        if (report.nonImpedantBranches > 0) {
            LOGGER.warn("Network {}: {} branches are non impedant", lfNetwork, report.nonImpedantBranches);
        }

        if (report.generatorsWithInconsistentTargetVoltage > 0) {
            Reports.reportGeneratorsDiscardedFromVoltageControlBecauseTargetVIsInconsistent(reportNode, report.generatorsWithInconsistentTargetVoltage);
            LOGGER.warn("Network {}: {} generators have an inconsistent target voltage and have been discarded from voltage control",
                    lfNetwork, report.generatorsWithInconsistentTargetVoltage);
        }

        if (report.generatorsWithZeroRemoteVoltageControlReactivePowerKey > 0) {
            LOGGER.warn("Network {}: {} generators have a zero remote voltage control reactive power key",
                    lfNetwork, report.generatorsWithZeroRemoteVoltageControlReactivePowerKey);
        }

        if (report.transformerVoltageControlDiscardedBecauseControllerBranchIsOpen > 0) {
            LOGGER.warn("Network {}: {} transformer voltage controls have been discarded because controller branch is open",
                    lfNetwork, report.transformerVoltageControlDiscardedBecauseControllerBranchIsOpen);
        }

        if (report.transformerReactivePowerControlDiscardedBecauseControllerBranchIsOpen > 0) {
            LOGGER.warn("Network {}: {} transformer reactive power controls have been discarded because controller branch is open",
                    lfNetwork, report.transformerReactivePowerControlDiscardedBecauseControllerBranchIsOpen);
        }

        if (report.transformersWithInconsistentTargetVoltage > 0) {
            Reports.reportTransformersDiscardedFromVoltageControlBecauseTargetVIsInconsistent(reportNode, report.transformersWithInconsistentTargetVoltage);
            LOGGER.warn("Network {}: {} transformer voltage controls have an inconsistent target voltage and have been discarded from voltage control",
                    lfNetwork, report.transformersWithInconsistentTargetVoltage);
        }

        if (report.shuntsWithInconsistentTargetVoltage > 0) {
            Reports.reportShuntsDiscardedFromVoltageControlBecauseTargetVIsInconsistent(reportNode, report.shuntsWithInconsistentTargetVoltage);
            LOGGER.warn("Network {}: {} shunt voltage controls have an inconsistent target voltage and have been discarded from voltage control",
                    lfNetwork, report.shuntsWithInconsistentTargetVoltage);
        }

        if (parameters.getDebugDir() != null) {
            Path debugDir = DebugUtil.getDebugDir(parameters.getDebugDir());
            String dateStr = ZonedDateTime.now().format(DATE_TIME_FORMAT);
            lfNetwork.writeJson(debugDir.resolve("lfnetwork-" + dateStr + ".json"));
            lfNetwork.writeGraphViz(debugDir.resolve("lfnetwork-" + dateStr + ".dot"), parameters.getLoadFlowModel());
        }

        postProcessors.forEach(pp -> pp.onLfNetworkLoaded(network, lfNetwork));
        return lfNetwork;
    }

    private static void checkControlZonesAreDisjoints(LfNetwork lfNetwork) {
        Map generatorVoltageControlCount = new HashMap<>();
        for (LfSecondaryVoltageControl lfSvc : lfNetwork.getSecondaryVoltageControls()) {
            for (GeneratorVoltageControl generatorVoltageControl : lfSvc.getGeneratorVoltageControls()) {
                generatorVoltageControlCount.computeIfAbsent(generatorVoltageControl, k -> new MutableInt())
                        .increment();
            }
        }
        for (var e : generatorVoltageControlCount.entrySet()) {
            if (e.getValue().intValue() > 1) {
                throw new PowsyblException("Generator voltage control of controlled bus '" + e.getKey().getControlledBus().getId() + "' is present in more that one control zone");
            }
        }
    }

    private static Set findControlZoneGeneratorVoltageControl(Network network, LfNetworkParameters parameters, LfNetwork lfNetwork, ControlZone controlZone) {
        Set generatorVoltageControls = new LinkedHashSet<>();
        Set controlUnitsNotFound = new LinkedHashSet<>();
        Set controlledBusesOfNotFoundVoltageControls = new LinkedHashSet<>();
        for (ControlUnit controlUnit : controlZone.getControlUnits()) {
            Identifiable identifiable = network.getIdentifiable(controlUnit.getId());
            if (identifiable == null) {
                controlUnitsNotFound.add(controlUnit.getId());
                continue;
            }
            if (identifiable.getType() != IdentifiableType.GENERATOR && !HvdcConverterStations.isVsc(identifiable)) {
                throw new PowsyblException("Control unit '" + controlUnit.getId() + "' of zone '"
                        + controlZone.getName() + "' is expected to be either a generator or a VSC converter station");
            }
            Terminal regulatingTerminal = Networks.getEquipmentRegulatingTerminal(identifiable).orElse(null);
            if (regulatingTerminal == null) {
                continue;
            }
            LfBus controlledBus = getLfBus(regulatingTerminal, lfNetwork, parameters.isBreakers());
            if (controlledBus == null) {
                // might happen if controlled bus is not in same component that controller buses
                continue;
            }
            if (!controlledBus.isGeneratorVoltageControlled()) {
                // might happen to be false if all generators of the voltage control are disconnected or have been
                // discarded from voltage control because of inconsistency (like small reactive limit range)
                controlledBusesOfNotFoundVoltageControls.add(controlledBus.getId());
                continue;
            }
            GeneratorVoltageControl generatorVoltageControl = controlledBus.getGeneratorVoltageControl().orElseThrow();
            generatorVoltageControls.add(generatorVoltageControl);
        }
        LOGGER.debug("{} control units of control zone '{}' have been mapped to {} generator voltage controls (controlled buses: {}, controlled buses without voltage control: {}, control units not found: {})",
                controlZone.getControlUnits().size(), controlZone.getName(), generatorVoltageControls.size(),
                generatorVoltageControls.stream().map(VoltageControl::getControlledBus).map(LfElement::getId).toList(),
                controlledBusesOfNotFoundVoltageControls,
                controlUnitsNotFound);
        return generatorVoltageControls;
    }

    private static void createSecondaryVoltageControls(Network network, LfNetworkParameters parameters, LfNetwork lfNetwork) {
        if (!parameters.isSecondaryVoltageControl()) {
            return;
        }
        SecondaryVoltageControl control = network.getExtension(SecondaryVoltageControl.class);
        if (control == null) {
            return;
        }
        for (ControlZone controlZone : control.getControlZones()) {
            PilotPoint pilotPoint = controlZone.getPilotPoint();
            // only keep control zone if its pilot bus is in this LfNetwork
            findPilotBus(network, parameters.isBreakers(), pilotPoint.getBusbarSectionsOrBusesIds()).ifPresentOrElse(pilotBus -> {
                LfBus lfPilotBus = lfNetwork.getBusById(pilotBus.getId());
                if (lfPilotBus != null) { // could be in another LfNetwork (another component)
                    double targetV = pilotPoint.getTargetV() / lfPilotBus.getNominalV();
                    // filter missing control units and find corresponding primary voltage control, controlled bus
                    Set generatorVoltageControls = findControlZoneGeneratorVoltageControl(network, parameters, lfNetwork, controlZone);
                    if (!generatorVoltageControls.isEmpty()) {
                        Set participatingControlUnitIds = controlZone.getControlUnits().stream()
                                .filter(ControlUnit::isParticipate)
                                .map(ControlUnit::getId).collect(Collectors.toSet());
                        var lfSvc = new LfSecondaryVoltageControl(controlZone.getName(), lfPilotBus, targetV,
                                participatingControlUnitIds, generatorVoltageControls);
                        lfNetwork.addSecondaryVoltageControl(lfSvc);
                    }
                }
            }, () -> LOGGER.warn("None of the pilot buses of control zone '{}' are valid", controlZone.getName()));
        }

        checkControlZonesAreDisjoints(lfNetwork);

        LOGGER.info("Network {}: {} secondary control zones have been created ({})", lfNetwork, lfNetwork.getSecondaryVoltageControls().size(),
                lfNetwork.getSecondaryVoltageControls().stream().map(LfSecondaryVoltageControl::getZoneName).toList());
    }

    private static Optional findPilotBus(Network network, boolean breaker, List busbarSectionsOrBusesId) {
        for (String busbarSectionOrBusId : busbarSectionsOrBusesId) {
            // node/breaker case
            BusbarSection bbs = network.getBusbarSection(busbarSectionOrBusId);
            if (bbs != null) {
                return Optional.ofNullable(Networks.getBus(bbs.getTerminal(), breaker));
            }
            // bus/breaker case
            Bus configuredBus = network.getBusBreakerView().getBus(busbarSectionOrBusId);
            if (configuredBus != null) {
                return breaker ? Optional.of(configuredBus)
                        : Optional.ofNullable(configuredBus.getVoltageLevel().getBusView().getMergedBus(configuredBus.getId()));
            }
        }
        return Optional.empty();
    }

    private static void createVoltageAngleLimits(Network network, LfNetwork lfNetwork, LfNetworkParameters parameters) {
        network.getVoltageAngleLimits().forEach(voltageAngleLimit -> {
            LfBus from = getLfBus(voltageAngleLimit.getTerminalFrom(), lfNetwork, parameters.isBreakers());
            LfBus to = getLfBus(voltageAngleLimit.getTerminalTo(), lfNetwork, parameters.isBreakers());
            if (from != null && to != null) {
                lfNetwork.addVoltageAngleLimit(new LfNetwork.LfVoltageAngleLimit(voltageAngleLimit.getId(), from, to,
                        Math.toRadians(voltageAngleLimit.getHighLimit().orElse(Double.NaN)), Math.toRadians(voltageAngleLimit.getLowLimit().orElse(Double.NaN))));
            }
        });
    }

    private static String getTrippingLfBranchId(OverloadManagementSystem.Tripping tripping) {
        String branchToOperateId = null;
        if (tripping.getType() == OverloadManagementSystem.Tripping.Type.SWITCH_TRIPPING) {
            OverloadManagementSystem.SwitchTripping switchTripping = (OverloadManagementSystem.SwitchTripping) tripping;
            branchToOperateId = switchTripping.getSwitchToOperateId();
        } else if (tripping.getType() == OverloadManagementSystem.Tripping.Type.BRANCH_TRIPPING) {
            OverloadManagementSystem.BranchTripping branchTripping = (OverloadManagementSystem.BranchTripping) tripping;
            branchToOperateId = branchTripping.getBranchToOperateId();
        }
        return branchToOperateId;
    }

    private static void addTripping(LfNetwork lfNetwork, LfOverloadManagementSystem lfOverloadManagementSystem, OverloadManagementSystem.Tripping tripping) {
        String branchToOperateId = getTrippingLfBranchId(tripping);
        LfBranch lfBranchToOperate = lfNetwork.getBranchById(branchToOperateId);
        if (lfBranchToOperate != null) {
            LfBus bus = lfOverloadManagementSystem.getMonitoredSide().equals(TwoSides.ONE) ?
                    lfOverloadManagementSystem.getMonitoredBranch().getBus1() : lfOverloadManagementSystem.getMonitoredBranch().getBus2();
            double threshold = tripping.getCurrentLimit() / PerUnit.ib(bus.getNominalV());
            lfOverloadManagementSystem.addLfBranchTripping(lfBranchToOperate, tripping.isOpenAction(), threshold);
        } else {
            LOGGER.warn("Invalid overload management system: branch to operate is '{}'", branchToOperateId);
        }
    }

    private static void createOverloadManagementSystem(LfNetwork lfNetwork, OverloadManagementSystem system) {
        if (system.isEnabled()) {
            LfBranch lfMonitoredElement = lfNetwork.getBranchById(system.getMonitoredElementId());
            if (system.getTrippings().stream().map(OverloadManagementSystem.Tripping::getType)
                    .anyMatch(type -> type == OverloadManagementSystem.Tripping.Type.THREE_WINDINGS_TRANSFORMER_TRIPPING)) {
                LOGGER.warn("Unsupported overload management system {}: three windings transformer tripping supported", system.getId());
                return;
            }
            if (lfMonitoredElement != null) {
                LfOverloadManagementSystem lfOverloadManagementSystem = new LfOverloadManagementSystem(lfMonitoredElement, system.getMonitoredSide().toTwoSides());
                system.getTrippings().forEach(tripping -> addTripping(lfNetwork, lfOverloadManagementSystem, tripping));
                if (!lfOverloadManagementSystem.getBranchTrippingList().isEmpty()) {
                    lfNetwork.addOverloadManagementSystem(lfOverloadManagementSystem);
                }
            } else {
                LOGGER.warn("Invalid overload management system: element to monitor is '{}'", system.getMonitoredElementId());
            }
        }
    }

    private void createAutomationSystems(Network network, LfNetwork lfNetwork) {
        for (Substation substation : network.getSubstations()) {
            for (OverloadManagementSystem system : substation.getOverloadManagementSystems()) {
                createOverloadManagementSystem(lfNetwork, system);
            }
        }
    }

    @Override
    public List load(Network network, LfTopoConfig topoConfig, LfNetworkParameters parameters, ReportNode reportNode) {
        Objects.requireNonNull(network);
        Objects.requireNonNull(parameters);

        if (!network.getValidationLevel().equals(ValidationLevel.STEADY_STATE_HYPOTHESIS)) {
            throw new PowsyblException("Only STEADY STATE HYPOTHESIS validation level of the network is supported");
        }

        Stopwatch stopwatch = Stopwatch.createStarted();

        Map, List> busesByCc = new TreeMap<>();
        Iterable buses = Networks.getBuses(network, parameters.isBreakers());
        for (Bus bus : buses) {
            Component cc = bus.getConnectedComponent();
            Component sc = bus.getSynchronousComponent();
            if (cc != null && sc != null) {
                busesByCc.computeIfAbsent(Pair.of(cc.getNum(), sc.getNum()), k -> new ArrayList<>()).add(bus);
            }
        }

        Map, List> switchesByCc = new HashMap<>();
        if (parameters.isBreakers()) {
            for (VoltageLevel vl : network.getVoltageLevels()) {
                for (Switch sw : vl.getBusBreakerView().getSwitches()) {
                    if (!sw.isOpen()) { // only create closed switches as in security analysis we can only open switches
                        Bus bus1 = vl.getBusBreakerView().getBus1(sw.getId());
                        Component cc = bus1.getConnectedComponent();
                        Component sc = bus1.getSynchronousComponent();
                        if (cc != null && sc != null) {
                            switchesByCc.computeIfAbsent(Pair.of(cc.getNum(), sc.getNum()), k -> new ArrayList<>()).add(sw);
                        }
                    }
                }
            }
        }

        Stream, List>> filteredBusesByCcStream = parameters.isComputeMainConnectedComponentOnly()
            ? busesByCc.entrySet().stream().filter(e -> e.getKey().getLeft() == ComponentConstants.MAIN_NUM)
            : busesByCc.entrySet().stream();

        List lfNetworks = filteredBusesByCcStream
                .map(e -> {
                    var networkKey = e.getKey();
                    int numCc = networkKey.getLeft();
                    int numSc = networkKey.getRight();
                    List lfBuses = e.getValue();
                    return create(numCc, numSc, network, lfBuses, switchesByCc.get(networkKey), topoConfig,
                            parameters, Reports.createRootLfNetworkReportNode(numCc, numSc));
                })
                .collect(Collectors.toList());

        stopwatch.stop();

        LOGGER.debug(PERFORMANCE_MARKER, "LF networks created in {} ms", stopwatch.elapsed(TimeUnit.MILLISECONDS));

        return lfNetworks;
    }

    static boolean participateToSlackDistribution(LfNetworkParameters parameters, Bus b) {
        return parameters.getCountriesToBalance().isEmpty()
               || b.getVoltageLevel().getSubstation().flatMap(Substation::getCountry)
                   .map(country -> parameters.getCountriesToBalance().contains(country))
                   .orElse(false);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy