LfNetwork.java

/**
 * 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;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.google.common.base.Stopwatch;
import com.powsybl.commons.PowsyblException;
import com.powsybl.commons.report.ReportNode;
import com.powsybl.openloadflow.graph.GraphConnectivity;
import com.powsybl.openloadflow.graph.GraphConnectivityFactory;
import com.powsybl.openloadflow.util.PerUnit;
import com.powsybl.openloadflow.util.Reports;
import org.anarres.graphviz.builder.GraphVizGraph;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.stream.Stream;

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

/**
 * @author Geoffroy Jamgotchian {@literal <geoffroy.jamgotchian at rte-france.com>}
 */
public class LfNetwork extends AbstractPropertyBag implements PropertyBag {

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

    private static final SlackBusSelector SLACK_BUS_SELECTOR_FALLBACK = new MostMeshedSlackBusSelector();

    private final int numCC;

    private final int numSC;

    private final SlackBusSelector slackBusSelector;

    private final ReferenceBusSelector referenceBusSelector;

    private final int maxSlackBusCount;

    private final Map<String, LfBus> busesById = new LinkedHashMap<>();

    private final List<LfBus> busesByIndex = new ArrayList<>();

    private LfBus referenceBus;

    private List<LfBus> slackBuses;

    private Set<LfBus> excludedSlackBuses = Collections.emptySet();

    private LfGenerator referenceGenerator;

    private final List<LfBranch> branches = new ArrayList<>();

    private final Map<String, LfBranch> branchesById = new HashMap<>();

    private int shuntCount = 0;

    private final List<LfShunt> shuntsByIndex = new ArrayList<>();

    private final Map<String, LfShunt> shuntsById = new HashMap<>();

    private final Map<String, LfGenerator> generatorsById = new HashMap<>();

    private final Map<String, LfLoad> loadsById = new HashMap<>();

    private final Map<String, LfArea> areasById = new HashMap<>();

    private final List<LfArea> areas = new ArrayList<>();

    private final List<LfHvdc> hvdcs = new ArrayList<>();

    private final Map<String, LfHvdc> hvdcsById = new HashMap<>();

    private final List<LfNetworkListener> listeners = new ArrayList<>();

    private Validity validity = Validity.VALID;

    private final GraphConnectivityFactory<LfBus, LfBranch> connectivityFactory;

    private GraphConnectivity<LfBus, LfBranch> connectivity;

    private final Map<LoadFlowModel, Set<LfZeroImpedanceNetwork>> zeroImpedanceNetworksByModel = new EnumMap<>(LoadFlowModel.class);

    private ReportNode reportNode;

    private final List<LfSecondaryVoltageControl> secondaryVoltageControls = new ArrayList<>();

    private final List<LfVoltageAngleLimit> voltageAngleLimits = new ArrayList<>();

    public enum Validity {
        VALID("Valid"),
        INVALID_NO_GENERATOR("Network has no generator"),
        INVALID_NO_GENERATOR_VOLTAGE_CONTROL("Network has no generator with voltage control enabled");

        private final String description;

        Validity(String description) {
            this.description = description;
        }

        @Override
        public String toString() {
            return this.description;
        }
    }

    public static class LfVoltageAngleLimit {
        private final String id;
        private final LfBus from;
        private final LfBus to;
        private final double highValue;
        private final double lowValue;

        public LfVoltageAngleLimit(String id, LfBus from, LfBus to, double highValue, double lowValue) {
            this.id = Objects.requireNonNull(id);
            this.from = Objects.requireNonNull(from);
            this.to = Objects.requireNonNull(to);
            this.highValue = highValue;
            this.lowValue = lowValue;
        }

        public String getId() {
            return id;
        }

        public LfBus getFrom() {
            return from;
        }

        public LfBus getTo() {
            return to;
        }

        public double getHighValue() {
            return highValue;
        }

        public double getLowValue() {
            return lowValue;
        }
    }

    protected final List<LfOverloadManagementSystem> overloadManagementSystems = new ArrayList<>();

    public LfNetwork(int numCC, int numSC, SlackBusSelector slackBusSelector, int maxSlackBusCount,
                     GraphConnectivityFactory<LfBus, LfBranch> connectivityFactory, ReferenceBusSelector referenceBusSelector, ReportNode reportNode) {
        this.numCC = numCC;
        this.numSC = numSC;
        this.slackBusSelector = Objects.requireNonNull(slackBusSelector);
        this.maxSlackBusCount = maxSlackBusCount;
        this.connectivityFactory = Objects.requireNonNull(connectivityFactory);
        this.referenceBusSelector = referenceBusSelector;
        this.reportNode = Objects.requireNonNull(reportNode);
    }

    public LfNetwork(int numCC, int numSC, SlackBusSelector slackBusSelector, int maxSlackBusCount,
                     GraphConnectivityFactory<LfBus, LfBranch> connectivityFactory, ReferenceBusSelector referenceBusSelector) {
        this(numCC, numSC, slackBusSelector, maxSlackBusCount, connectivityFactory, referenceBusSelector, ReportNode.NO_OP);
    }

    public int getNumCC() {
        return numCC;
    }

    public int getNumSC() {
        return numSC;
    }

    public ReportNode getReportNode() {
        return reportNode;
    }

    public void setReportNode(ReportNode reportNode) {
        this.reportNode = Objects.requireNonNull(reportNode);
    }

    public LfElement getElement(ElementType elementType, int num) {
        return switch (elementType) {
            case BUS -> getBus(num);
            case BRANCH -> getBranch(num);
            case SHUNT_COMPENSATOR -> getShunt(num);
            case HVDC -> getHvdc(num);
            case AREA -> getArea(num);
        };
    }

    private void invalidateSlackAndReference() {
        if (slackBuses != null) {
            for (var slackBus : slackBuses) {
                slackBus.setSlack(false);
            }
        }
        slackBuses = null;
        if (referenceBus != null) {
            referenceBus.setReference(false);
        }
        referenceBus = null;
        if (referenceGenerator != null) {
            referenceGenerator.setReference(false);
        }
        referenceGenerator = null;
    }

    public void updateSlackBusesAndReferenceBus() {
        if (slackBuses == null && referenceBus == null) {
            List<LfBus> selectableBuses =
                excludedSlackBuses.isEmpty() ? busesByIndex :
                    busesByIndex.stream().filter(bus -> !excludedSlackBuses.contains(bus)).toList();
            SelectedSlackBus selectedSlackBus = slackBusSelector.select(selectableBuses, maxSlackBusCount);
            slackBuses = selectedSlackBus.getBuses();
            if (slackBuses.isEmpty()) { // ultimate fallback
                selectedSlackBus = SLACK_BUS_SELECTOR_FALLBACK.select(selectableBuses, maxSlackBusCount);
                if (selectedSlackBus.getBuses().isEmpty()) {
                    throw new PowsyblException("No slack bus could be selected");
                }
                slackBuses = selectedSlackBus.getBuses();
            }
            LOGGER.info("Network {}, slack buses are {} (method='{}')", this, slackBuses, selectedSlackBus.getSelectionMethod());
            for (var slackBus : slackBuses) {
                slackBus.setSlack(true);
            }
            // reference bus must be selected after slack bus, because of ReferenceBusFirstSlackSelector implementation requiring slackBuses
            SelectedReferenceBus selectedReferenceBus = referenceBusSelector.select(this);
            referenceBus = selectedReferenceBus.getLfBus();
            LOGGER.info("Network {}, reference bus is {} (method='{}')", this, referenceBus, selectedReferenceBus.getSelectionMethod());
            referenceBus.setReference(true);
            if (selectedReferenceBus instanceof SelectedGeneratorReferenceBus generatorReferenceBus) {
                referenceGenerator = generatorReferenceBus.getLfGenerator();
                LOGGER.info("Network {}, reference generator is {}", this, referenceGenerator.getId());
                referenceGenerator.setReference(true);
            }
            if (connectivity != null) {
                connectivity.setMainComponentVertex(slackBuses.get(0));
            }
        }
    }

    private void invalidateZeroImpedanceNetworks() {
        zeroImpedanceNetworksByModel.clear();
    }

    public void addBranch(LfBranch branch) {
        Objects.requireNonNull(branch);
        branch.setNum(branches.size());
        branches.add(branch);
        branchesById.put(branch.getId(), branch);
        invalidateSlackAndReference();
        connectivity = null;
        invalidateZeroImpedanceNetworks();

        // create bus -> branches link
        if (branch.getBus1() != null) {
            branch.getBus1().addBranch(branch);
        }
        if (branch.getBus2() != null) {
            branch.getBus2().addBranch(branch);
        }
    }

    public List<LfBranch> getBranches() {
        return branches;
    }

    public LfBranch getBranch(int num) {
        return branches.get(num);
    }

    public LfBranch getBranchById(String branchId) {
        Objects.requireNonNull(branchId);
        return branchesById.get(branchId);
    }

    private void addShunt(LfShunt shunt) {
        shunt.setNum(shuntCount++);
        shuntsByIndex.add(shunt);
        shunt.getOriginalIds().forEach(id -> shuntsById.put(id, shunt));
    }

    public void addBus(LfBus bus) {
        Objects.requireNonNull(bus);
        bus.setNum(busesByIndex.size());
        busesByIndex.add(bus);
        busesById.put(bus.getId(), bus);
        invalidateSlackAndReference();
        connectivity = null;

        bus.getShunt().ifPresent(this::addShunt);
        bus.getControllerShunt().ifPresent(this::addShunt);
        bus.getSvcShunt().ifPresent(this::addShunt);
        bus.getGenerators().forEach(gen -> generatorsById.put(gen.getId(), gen));
        bus.getLoads().forEach(load -> load.getOriginalIds().forEach(id -> loadsById.put(id, load)));
    }

    public void addArea(LfArea area) {
        Objects.requireNonNull(area);
        areasById.put(area.getId(), area);
        area.setNum(areas.size());
        areas.add(area);
    }

    public List<LfBus> getBuses() {
        return busesByIndex;
    }

    public LfBus getBusById(String id) {
        Objects.requireNonNull(id);
        return busesById.get(id);
    }

    public LfBus getBus(int num) {
        return busesByIndex.get(num);
    }

    public LfBus getReferenceBus() {
        updateSlackBusesAndReferenceBus();
        return referenceBus;
    }

    public LfBus getSlackBus() {
        return getSlackBuses().get(0);
    }

    public List<LfBus> getSlackBuses() {
        updateSlackBusesAndReferenceBus();
        return slackBuses;
    }

    public Set<LfBus> getExcludedSlackBuses() {
        return excludedSlackBuses;
    }

    public void setExcludedSlackBuses(Set<LfBus> excludedSlackBuses) {
        Objects.requireNonNull(excludedSlackBuses);
        if (!excludedSlackBuses.equals(this.excludedSlackBuses)) {
            this.excludedSlackBuses = excludedSlackBuses;
            invalidateSlackAndReference();
        }
    }

    public LfGenerator getReferenceGenerator() {
        updateSlackBusesAndReferenceBus();
        return referenceGenerator;
    }

    public List<LfShunt> getShunts() {
        return shuntsByIndex;
    }

    public LfShunt getShunt(int num) {
        return shuntsByIndex.get(num);
    }

    public LfShunt getShuntById(String id) {
        Objects.requireNonNull(id);
        return shuntsById.get(id);
    }

    public LfGenerator getGeneratorById(String id) {
        Objects.requireNonNull(id);
        return generatorsById.get(id);
    }

    public LfLoad getLoadById(String id) {
        Objects.requireNonNull(id);
        return loadsById.get(id);
    }

    public Stream<LfArea> getAreaStream() {
        return areasById.values().stream();
    }

    public boolean hasArea() {
        return !areasById.isEmpty();
    }

    public LfArea getAreaById(String id) {
        Objects.requireNonNull(id);
        return areasById.get(id);
    }

    public LfArea getArea(int num) {
        return areas.get(num);
    }

    public List<LfArea> getAreas() {
        return areas;
    }

    public void addHvdc(LfHvdc hvdc) {
        Objects.requireNonNull(hvdc);
        hvdc.setNum(hvdcs.size());
        hvdcs.add(hvdc);
        hvdcsById.put(hvdc.getId(), hvdc);

        // create bus -> branches link
        if (hvdc.getBus1() != null) {
            hvdc.getBus1().addHvdc(hvdc);
        }
        if (hvdc.getBus2() != null) {
            hvdc.getBus2().addHvdc(hvdc);
        }
    }

    public List<LfHvdc> getHvdcs() {
        return hvdcs;
    }

    public LfHvdc getHvdc(int num) {
        return hvdcs.get(num);
    }

    public LfHvdc getHvdcById(String id) {
        Objects.requireNonNull(id);
        return hvdcsById.get(id);
    }

    public void updateState(LfNetworkStateUpdateParameters parameters) {
        Stopwatch stopwatch = Stopwatch.createStarted();

        LfNetworkUpdateReport updateReport = new LfNetworkUpdateReport();

        for (LfBus bus : busesById.values()) {
            bus.updateState(parameters);
            for (LfGenerator generator : bus.getGenerators()) {
                generator.updateState(parameters);
            }
            bus.getShunt().ifPresent(shunt -> shunt.updateState(parameters));
            bus.getControllerShunt().ifPresent(shunt -> shunt.updateState(parameters));
        }
        branches.forEach(branch -> branch.updateState(parameters, updateReport));
        hvdcs.forEach(LfHvdc::updateState);

        if (updateReport.closedSwitchCount + updateReport.openedSwitchCount > 0) {
            LOGGER.debug("Switches status update: {} closed and {} opened", updateReport.closedSwitchCount, updateReport.openedSwitchCount);
        }
        if (updateReport.connectedBranchSide1Count + updateReport.disconnectedBranchSide1Count
                + updateReport.connectedBranchSide2Count + updateReport.disconnectedBranchSide2Count > 0) {
            LOGGER.debug("Branches connection status update: {} connected side 1, {} disconnected side1, {} connected side 2, {} disconnected side 2",
                    updateReport.connectedBranchSide1Count, updateReport.disconnectedBranchSide1Count, updateReport.connectedBranchSide2Count, updateReport.disconnectedBranchSide2Count);
        }

        stopwatch.stop();
        LOGGER.debug(PERFORMANCE_MARKER, "Network {}, IIDM network updated in {} ms", this, stopwatch.elapsed(TimeUnit.MILLISECONDS));
    }

    public void writeJson(Path file) {
        try (Writer writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) {
            writeJson(writer);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    private void writeJson(LfBus bus, JsonGenerator jsonGenerator) throws IOException {
        jsonGenerator.writeStringField("id", bus.getId());
        jsonGenerator.writeNumberField("num", bus.getNum());
        if (bus.getGenerationTargetQ() != 0) {
            jsonGenerator.writeNumberField("generationTargetQ", bus.getGenerationTargetQ());
        }
        if (bus.getLoadTargetP() != 0) {
            jsonGenerator.writeNumberField("loadTargetP", bus.getLoadTargetP());
        }
        if (bus.getLoadTargetQ() != 0) {
            jsonGenerator.writeNumberField("loadTargetQ", bus.getLoadTargetQ());
        }
        bus.getGeneratorVoltageControl().ifPresent(vc -> {
            if (bus.isGeneratorVoltageControlEnabled()) {
                try {
                    if (vc.getControlledBus() != bus) {
                        jsonGenerator.writeNumberField("remoteControlTargetBus", vc.getControlledBus().getNum());
                    }
                    jsonGenerator.writeNumberField("targetV", vc.getTargetValue());
                } catch (IOException e) {
                    throw new UncheckedIOException(e);
                }
            }
        });
        if (!Double.isNaN(bus.getV())) {
            jsonGenerator.writeNumberField("v", bus.getV());
        }
        if (!Double.isNaN(bus.getAngle())) {
            jsonGenerator.writeNumberField("angle", bus.getAngle());
        }
    }

    private void writeJson(LfBranch branch, JsonGenerator jsonGenerator) throws IOException {
        jsonGenerator.writeStringField("id", branch.getId());
        jsonGenerator.writeNumberField("num", branch.getNum());
        LfBus bus1 = branch.getBus1();
        LfBus bus2 = branch.getBus2();
        if (bus1 != null) {
            jsonGenerator.writeNumberField("num1", bus1.getNum());
        }
        if (bus2 != null) {
            jsonGenerator.writeNumberField("num2", bus2.getNum());
        }
        PiModel piModel = branch.getPiModel();
        jsonGenerator.writeNumberField("r", piModel.getR());
        jsonGenerator.writeNumberField("x", piModel.getX());
        if (piModel.getG1() != 0) {
            jsonGenerator.writeNumberField("g1", piModel.getG1());
        }
        if (piModel.getG2() != 0) {
            jsonGenerator.writeNumberField("g2", piModel.getG2());
        }
        if (piModel.getB1() != 0) {
            jsonGenerator.writeNumberField("b1", piModel.getB1());
        }
        if (piModel.getB2() != 0) {
            jsonGenerator.writeNumberField("b2", piModel.getB2());
        }
        if (piModel.getR1() != 1) {
            jsonGenerator.writeNumberField("r1", piModel.getR1());
        }
        if (piModel.getA1() != 0) {
            jsonGenerator.writeNumberField("a1", piModel.getA1());
        }
        branch.getPhaseControl().filter(dpc -> branch.isPhaseController()).ifPresent(dpc -> {
            try {
                jsonGenerator.writeFieldName("discretePhaseControl");
                jsonGenerator.writeStartObject();
                jsonGenerator.writeStringField("controller", dpc.getControllerBranch().getId());
                jsonGenerator.writeStringField("controlled", dpc.getControlledBranch().getId());
                jsonGenerator.writeStringField("mode", dpc.getMode().name());
                jsonGenerator.writeStringField("unit", dpc.getUnit().name());
                jsonGenerator.writeStringField("controlledSide", dpc.getControlledSide().name());
                jsonGenerator.writeNumberField("targetValue", dpc.getTargetValue());
                jsonGenerator.writeEndObject();
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        });
    }

    private void writeJson(LfShunt shunt, JsonGenerator jsonGenerator) throws IOException {
        jsonGenerator.writeStringField("id", shunt.getId());
        jsonGenerator.writeNumberField("num", shunt.getNum());
        jsonGenerator.writeNumberField("b", shunt.getB());
    }

    private void writeJson(LfGenerator generator, JsonGenerator jsonGenerator) throws IOException {
        jsonGenerator.writeStringField("id", generator.getId());
        jsonGenerator.writeNumberField("targetP", generator.getTargetP());
        if (!Double.isNaN(generator.getTargetQ())) {
            jsonGenerator.writeNumberField("targetQ", generator.getTargetQ());
        }
        jsonGenerator.writeBooleanField("voltageControl", generator.getGeneratorControlType() == LfGenerator.GeneratorControlType.VOLTAGE);
        jsonGenerator.writeNumberField("minP", generator.getMinP());
        jsonGenerator.writeNumberField("maxP", generator.getMaxP());
        jsonGenerator.writeNumberField("minTargetP", generator.getMinTargetP());
        jsonGenerator.writeNumberField("maxTargetP", generator.getMaxTargetP());
    }

    public void writeJson(Writer writer) {
        Objects.requireNonNull(writer);
        updateSlackBusesAndReferenceBus();
        try (JsonGenerator jsonGenerator = new JsonFactory()
                .createGenerator(writer)
                .useDefaultPrettyPrinter()) {
            jsonGenerator.writeStartObject();

            jsonGenerator.writeFieldName("buses");
            jsonGenerator.writeStartArray();
            List<LfBus> sortedBuses = busesById.values().stream().sorted(Comparator.comparing(LfBus::getId)).toList();
            for (LfBus bus : sortedBuses) {
                jsonGenerator.writeStartObject();

                writeJson(bus, jsonGenerator);

                bus.getShunt().ifPresent(shunt -> {
                    try {
                        jsonGenerator.writeFieldName("shunt");
                        jsonGenerator.writeStartObject();

                        writeJson(shunt, jsonGenerator);

                        jsonGenerator.writeEndObject();
                    } catch (IOException e) {
                        throw new UncheckedIOException(e);
                    }
                });

                List<LfGenerator> sortedGenerators = bus.getGenerators().stream().sorted(Comparator.comparing(LfGenerator::getId)).toList();
                if (!sortedGenerators.isEmpty()) {
                    jsonGenerator.writeFieldName("generators");
                    jsonGenerator.writeStartArray();
                    for (LfGenerator generator : sortedGenerators) {
                        jsonGenerator.writeStartObject();

                        writeJson(generator, jsonGenerator);

                        jsonGenerator.writeEndObject();
                    }
                    jsonGenerator.writeEndArray();
                }

                jsonGenerator.writeEndObject();
            }
            jsonGenerator.writeEndArray();

            jsonGenerator.writeFieldName("branches");
            jsonGenerator.writeStartArray();
            List<LfBranch> sortedBranches = branches.stream().sorted(Comparator.comparing(LfBranch::getId)).toList();
            for (LfBranch branch : sortedBranches) {
                jsonGenerator.writeStartObject();

                writeJson(branch, jsonGenerator);

                jsonGenerator.writeEndObject();
            }
            jsonGenerator.writeEndArray();

            jsonGenerator.writeEndObject();
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    private void reportSize(ReportNode reportNode) {
        Reports.reportNetworkSize(reportNode, busesById.values().size(), branches.size());
        LOGGER.info("Network {} has {} buses and {} branches",
            this, busesById.values().size(), branches.size());
    }

    public void reportBalance(ReportNode reportNode) {
        double activeGeneration = 0;
        double reactiveGeneration = 0;
        double activeLoad = 0;
        double reactiveLoad = 0;
        for (LfBus b : busesById.values()) {
            activeGeneration += b.getGenerationTargetP() * PerUnit.SB;
            reactiveGeneration += b.getGenerationTargetQ() * PerUnit.SB;
            activeLoad += b.getLoadTargetP() * PerUnit.SB;
            reactiveLoad += b.getLoadTargetQ() * PerUnit.SB;
        }

        Reports.reportNetworkBalance(reportNode, activeGeneration, activeLoad, reactiveGeneration, reactiveLoad);
        LOGGER.info("Network {} balance: active generation={} MW, active load={} MW, reactive generation={} MVar, reactive load={} MVar",
            this, activeGeneration, activeLoad, reactiveGeneration, reactiveLoad);
    }

    public void fix(boolean minImpedance, double lowImpedanceThreshold) {
        if (minImpedance) {
            for (LfBranch branch : branches) {
                branch.setMinZ(lowImpedanceThreshold);
            }
        } else {
            // zero impedance phase shifter controller or controlled branch is not supported
            branches.stream()
                    .filter(b -> b.isPhaseController() || b.isPhaseControlled()
                            || b.isTransformerReactivePowerController() || b.isTransformerReactivePowerControlled()
                            || b.getGeneratorReactivePowerControl().isPresent())
                    .forEach(branch -> branch.setMinZ(lowImpedanceThreshold));
            // zero impedance boundary branch is not supported
            areas.stream()
                    .flatMap(a -> a.getBoundaries().stream())
                    .map(LfArea.Boundary::getBranch)
                    .forEach(branch -> branch.setMinZ(lowImpedanceThreshold));
        }
    }

    private void validateBuses(LoadFlowModel loadFlowModel, ReportNode reportNode) {
        // DC or AC, if no generator, network is dead
        boolean hasAtLeastOneBusGenerator = false;
        for (LfBus bus : busesByIndex) {
            if (!bus.getGenerators().isEmpty()) {
                hasAtLeastOneBusGenerator = true;
                break;
            }
        }
        if (!hasAtLeastOneBusGenerator) {
            // we don't report because this is too much on real networks
            LOGGER.debug("Network {} has no generator and will be considered dead", this);
            validity = Validity.INVALID_NO_GENERATOR;
            return;
        }
        // AC requires at least one bus under voltage control
        if (loadFlowModel == LoadFlowModel.AC) {
            boolean hasAtLeastOneBusGeneratorVoltageControlEnabled = false;
            for (LfBus bus : busesByIndex) {
                if (bus.isGeneratorVoltageControlEnabled()) {
                    hasAtLeastOneBusGeneratorVoltageControlEnabled = true;
                    break;
                }
            }
            if (!hasAtLeastOneBusGeneratorVoltageControlEnabled) {
                LOGGER.warn("Network {} must have at least one bus with generator voltage control enabled", this);
                if (reportNode != null) {
                    Reports.reportNetworkMustHaveAtLeastOneBusGeneratorVoltageControlEnabled(reportNode);
                }
                validity = Validity.INVALID_NO_GENERATOR_VOLTAGE_CONTROL;
            }
        }
    }

    public void validate(LoadFlowModel loadFlowModel, ReportNode reportNode) {
        validity = Validity.VALID;
        validateBuses(loadFlowModel, reportNode);
    }

    public static <T> List<LfNetwork> load(T network, LfNetworkLoader<T> networkLoader, SlackBusSelector slackBusSelector) {
        return load(network, networkLoader, new LfNetworkParameters().setSlackBusSelector(slackBusSelector), ReportNode.NO_OP);
    }

    public static <T> List<LfNetwork> load(T network, LfNetworkLoader<T> networkLoader, LfNetworkParameters parameters) {
        return load(network, networkLoader, parameters, ReportNode.NO_OP);
    }

    public static <T> List<LfNetwork> load(T network, LfNetworkLoader<T> networkLoader, LfNetworkParameters parameters, ReportNode reportNode) {
        return load(network, networkLoader, new LfTopoConfig(), parameters, reportNode);
    }

    public static <T> List<LfNetwork> load(T network, LfNetworkLoader<T> networkLoader, LfTopoConfig topoConfig, LfNetworkParameters parameters, ReportNode reportNode) {
        Objects.requireNonNull(network);
        Objects.requireNonNull(networkLoader);
        Objects.requireNonNull(parameters);
        List<LfNetwork> lfNetworks = networkLoader.load(network, topoConfig, parameters, reportNode);
        int deadComponentsCount = 0;
        for (LfNetwork lfNetwork : lfNetworks) {
            ReportNode networkReport = Reports.createNetworkInfoReporter(lfNetwork.getReportNode());
            lfNetwork.fix(parameters.isMinImpedance(), parameters.getLowImpedanceThreshold());
            lfNetwork.validate(parameters.getLoadFlowModel(), networkReport);
            switch (lfNetwork.getValidity()) {
                case VALID -> {
                    lfNetwork.reportSize(networkReport);
                    lfNetwork.reportBalance(networkReport);
                    Reports.reportAngleReferenceBusAndSlackBuses(networkReport, lfNetwork.getReferenceBus().getId(), lfNetwork.getSlackBuses().stream().map(LfBus::getId).toList());
                    lfNetwork.setReportNode(Reports.includeLfNetworkReportNode(reportNode, lfNetwork.getReportNode()));
                }
                case INVALID_NO_GENERATOR_VOLTAGE_CONTROL -> {
                    LOGGER.info("Network {} is invalid, no calculation will be done", lfNetwork);
                    // we want to report this
                    lfNetwork.setReportNode(Reports.includeLfNetworkReportNode(reportNode, lfNetwork.getReportNode()));
                }
                case INVALID_NO_GENERATOR -> deadComponentsCount++; // will be reported later on altogether
            }
        }
        if (deadComponentsCount > 0) {
            Reports.reportComponentsWithoutGenerators(reportNode, deadComponentsCount);
            LOGGER.info("No calculation will be done on {} network(s) that have no generators", deadComponentsCount);
        }
        return lfNetworks;
    }

    public void updateZeroImpedanceCache(LoadFlowModel loadFlowModel) {
        zeroImpedanceNetworksByModel.computeIfAbsent(loadFlowModel, m -> LfZeroImpedanceNetwork.create(this, loadFlowModel));
    }

    public Set<LfZeroImpedanceNetwork> getZeroImpedanceNetworks(LoadFlowModel loadFlowModel) {
        updateZeroImpedanceCache(loadFlowModel);
        return zeroImpedanceNetworksByModel.get(loadFlowModel);
    }

    public GraphConnectivity<LfBus, LfBranch> getConnectivity() {
        if (connectivity == null) {
            connectivity = Objects.requireNonNull(connectivityFactory.create());
            getBuses().forEach(connectivity::addVertex);
            getBranches().stream()
                    .filter(b -> b.getBus1() != null && b.getBus2() != null)
                    .forEach(b -> connectivity.addEdge(b.getBus1(), b.getBus2(), b));
            connectivity.setMainComponentVertex(getSlackBuses().get(0));
            // this is necessary to create a first temporary changes level in order to allow
            // some outer loop to change permanently the connectivity (with automation systems for instance)
            // this one will never be reverted
            if (connectivity.supportTemporaryChangesNesting()) {
                connectivity.startTemporaryChanges();
            }
        }
        return connectivity;
    }

    public void addListener(LfNetworkListener listener) {
        listeners.add(listener);
    }

    public void removeListener(LfNetworkListener listener) {
        listeners.remove(listener);
    }

    public List<LfNetworkListener> getListeners() {
        return listeners;
    }

    public Validity getValidity() {
        return validity;
    }

    /**
     * Disable transformer voltage control when there is no generator controlling voltage on the connected component
     * that belong to the not controlled side of the transformer. Note that branches in contingency are not taken into
     * account.
     */
    public void fixTransformerVoltageControls() {
        List<LfBranch> controllerBranches = new ArrayList<>(1);
        getConnectivity().startTemporaryChanges();
        for (LfBranch branch : branches) {
            if (!branch.isDisabled() && branch.isVoltageController() && branch.isVoltageControlEnabled()) {
                controllerBranches.add(branch);
            }
            if (branch.isDisabled() && branch.getBus1() != null && branch.getBus2() != null) {
                // apply contingency (in case we are inside a security analysis)
                getConnectivity().removeEdge(branch);
            }
        }
        for (LfBranch branch : controllerBranches) {
            getConnectivity().removeEdge(branch);
        }
        int disabledTransformerCount = 0;
        Map<Integer, Boolean> componentNoPVBusesMap = new HashMap<>();
        for (LfBranch branch : controllerBranches) {
            var voltageControl = branch.getVoltageControl().orElseThrow();
            LfBus notControlledSide;
            if (voltageControl.getControlledBus() == branch.getBus1()) {
                notControlledSide = branch.getBus2();
            } else if (voltageControl.getControlledBus() == branch.getBus2()) {
                notControlledSide = branch.getBus1();
            } else {
                continue;
            }
            boolean noPvBusesInComponent = componentNoPVBusesMap.computeIfAbsent(getConnectivity().getComponentNumber(notControlledSide),
                k -> getConnectivity().getConnectedComponent(notControlledSide).stream()
                        .noneMatch(bus -> bus.isGeneratorVoltageControlled() && bus.isGeneratorVoltageControlEnabled()));
            if (noPvBusesInComponent) {
                branch.setVoltageControlEnabled(false);
                LOGGER.trace("Transformer {} voltage control has been disabled because no PV buses on not controlled side connected component",
                        branch.getId());
                disabledTransformerCount++;
            }
        }
        getConnectivity().undoTemporaryChanges();
        if (disabledTransformerCount > 0) {
            LOGGER.warn("{} transformer voltage controls have been disabled because no PV buses on not controlled side connected component",
                    disabledTransformerCount);
        }
    }

    public void writeGraphViz(Path file, LoadFlowModel loadFlowModel) {
        try (Writer writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) {
            writeGraphViz(writer, loadFlowModel);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    public void writeGraphViz(Writer writer, LoadFlowModel loadFlowModel) {
        try {
            GraphVizGraph gvGraph = new GraphVizGraphBuilder(this).build(loadFlowModel);
            gvGraph.writeTo(writer);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    public String getId() {
        return "{CC" + numCC + " SC" + numSC + '}';
    }

    public void addSecondaryVoltageControl(LfSecondaryVoltageControl secondaryVoltageControl) {
        secondaryVoltageControls.add(Objects.requireNonNull(secondaryVoltageControl));
    }

    public List<LfSecondaryVoltageControl> getSecondaryVoltageControls() {
        return secondaryVoltageControls;
    }

    public Optional<LfSecondaryVoltageControl> getSecondaryVoltageControl(String controlZoneName) {
        Objects.requireNonNull(controlZoneName);
        return secondaryVoltageControls.stream()
                .filter(lfSvc -> lfSvc.getZoneName().equals(controlZoneName))
                .findFirst();
    }

    private static boolean filterSecondaryVoltageControl(LfSecondaryVoltageControl secondaryVoltageControl) {
        return !secondaryVoltageControl.getPilotBus().isDisabled();
    }

    public List<LfSecondaryVoltageControl> getEnabledSecondaryVoltageControls() {
        return secondaryVoltageControls.stream()
                .filter(LfNetwork::filterSecondaryVoltageControl)
                .toList();
    }

    public void addVoltageAngleLimit(LfVoltageAngleLimit limit) {
        voltageAngleLimits.add(Objects.requireNonNull(limit));
    }

    public List<LfVoltageAngleLimit> getVoltageAngleLimits() {
        return voltageAngleLimits;
    }

    @SuppressWarnings("unchecked")
    public <E extends LfElement> List<E> getControllerElements(VoltageControl.Type type) {
        return busesByIndex.stream()
                .filter(bus -> bus.isVoltageControlled(type))
                .filter(bus -> bus.getVoltageControl(type).orElseThrow().getMergeStatus() == VoltageControl.MergeStatus.MAIN)
                .filter(bus -> bus.getVoltageControl(type).orElseThrow().isVisible())
                .flatMap(bus -> bus.getVoltageControl(type).orElseThrow().getMergedControllerElements().stream())
                .filter(Predicate.not(LfElement::isDisabled))
                .map(element -> (E) element)
                .toList();
    }

    public List<LfBus> getControlledBuses(VoltageControl.Type type) {
        return busesByIndex.stream()
                .filter(bus -> bus.isVoltageControlled(type))
                .filter(bus -> bus.getVoltageControl(type).orElseThrow().getMergeStatus() == VoltageControl.MergeStatus.MAIN)
                .filter(bus -> bus.getVoltageControl(type).orElseThrow().isVisible())
                .toList();
    }

    public void addOverloadManagementSystem(LfOverloadManagementSystem overloadManagementSystem) {
        overloadManagementSystems.add(Objects.requireNonNull(overloadManagementSystem));
    }

    public List<LfOverloadManagementSystem> getOverloadManagementSystems() {
        return overloadManagementSystems;
    }

    public void setGeneratorsInitialTargetPToTargetP() {
        getBuses().stream().flatMap(b -> b.getGenerators().stream()).forEach(LfGenerator::setInitialTargetPToTargetP);
    }

    @Override
    public String toString() {
        return getId();
    }
}