NetworkImpl.java

/**
 * Copyright (c) 2016, All partners of the iTesla project (http://www.itesla-project.eu/consortium)
 * 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.iidm.network.impl;

import com.google.common.base.Functions;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimap;
import com.google.common.primitives.Ints;
import com.powsybl.commons.PowsyblException;
import com.powsybl.commons.report.ReportNode;
import com.powsybl.iidm.network.*;
import com.powsybl.iidm.network.components.AbstractConnectedComponentsManager;
import com.powsybl.iidm.network.components.AbstractSynchronousComponentsManager;
import com.powsybl.commons.ref.RefChain;
import com.powsybl.commons.ref.RefObj;
import com.powsybl.iidm.network.util.Identifiables;
import com.powsybl.iidm.network.util.NetworkReports;
import com.powsybl.iidm.network.util.Networks;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.ZonedDateTime;
import java.util.*;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static com.powsybl.iidm.network.util.TieLineUtil.*;

/**
 * @author Geoffroy Jamgotchian {@literal <geoffroy.jamgotchian at rte-france.com>}
 */
public class NetworkImpl extends AbstractNetwork implements VariantManagerHolder, MultiVariantObject {

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

    /**
     * Reference to current network. This is used to easily update the root network reference in all equipments
     * when merging several networks.
     */
    private final RefChain<NetworkImpl> ref = new RefChain<>(new RefObj<>(this));

    /**
     * Reference to the subnetwork, hence the contained subnetwork is always null until a merge occurs
     * This is used to easily update the subnetwork in all substations and voltage levels during the merge.
     */
    private RefChain<SubnetworkImpl> subnetworkRef = new RefChain<>(new RefObj<>(null));

    private ValidationLevel validationLevel = ValidationLevel.STEADY_STATE_HYPOTHESIS;
    private ValidationLevel minValidationLevel = ValidationLevel.STEADY_STATE_HYPOTHESIS;

    private final NetworkIndex index = new NetworkIndex();

    private final Map<String, VoltageAngleLimit> voltageAngleLimitsIndex = new LinkedHashMap<>();

    private final VariantManagerImpl variantManager;

    private AbstractReportNodeContext reportNodeContext;

    private final NetworkListenerList listeners = new NetworkListenerList();

    private final Map<String, SubnetworkImpl> subnetworks = new LinkedHashMap<>();

    @Override
    public Collection<Network> getSubnetworks() {
        return subnetworks.values().stream().map(Network.class::cast).toList();
    }

    @Override
    public Network getSubnetwork(String id) {
        return subnetworks.get(id);
    }

    void removeFromSubnetworks(String subnetworkId) {
        subnetworks.remove(subnetworkId);
    }

    class BusBreakerViewImpl extends AbstractNetwork.AbstractBusBreakerViewImpl {

        @Override
        public Bus getBus(String id) {
            Bus bus = index.get(id, Bus.class);
            if (bus != null) {
                return bus;
            }
            return variants.get().busBreakerViewCache.getBus(id);
        }

        void invalidateCache() {
            variants.get().busBreakerViewCache.invalidate();
        }
    }

    private final BusBreakerViewImpl busBreakerView = new BusBreakerViewImpl();

    class BusViewImpl extends AbstractNetwork.AbstractBusViewImpl {

        @Override
        public Collection<Component> getConnectedComponents() {
            return Collections.unmodifiableList(variants.get().connectedComponentsManager.getConnectedComponents());
        }

        @Override
        public Collection<Component> getSynchronousComponents() {
            return Collections.unmodifiableList(variants.get().synchronousComponentsManager.getConnectedComponents());
        }

        @Override
        public Bus getBus(String id) {
            return variants.get().busViewCache.getBus(id);
        }

        void invalidateCache() {
            variants.get().busViewCache.invalidate();
        }
    }

    private final BusViewImpl busView = new BusViewImpl();

    NetworkImpl(String id, String name, String sourceFormat) {
        super(id, name, sourceFormat);
        ref.setRef(new RefObj<>(this));
        this.reportNodeContext = new SimpleReportNodeContext();
        variantManager = new VariantManagerImpl(this);
        variants = new VariantArray<>(ref, VariantImpl::new);
        // add the network the object list as it is a multi variant object
        // and it needs to be notified when and extension or a reduction of
        // the variant array is requested
        index.checkAndAdd(this);
    }

    static Network merge(String id, String name, Network... networks) {
        if (networks == null || networks.length < 2) {
            throw new IllegalArgumentException("At least 2 networks are expected");
        }

        NetworkImpl mergedNetwork = new NetworkImpl(id, name, networks[0].getSourceFormat());
        setValidationLevels(mergedNetwork, networks);
        setCommonCaseDate(mergedNetwork, networks);
        for (Network other : networks) {
            mergedNetwork.merge(other);
        }

        return mergedNetwork;
    }

    private static void setValidationLevels(NetworkImpl mergedNetwork, Network[] networks) {
        // Use the less restrictive validation level for the merged network
        ValidationLevel minLevel = mergedNetwork.getMinValidationLevel(); // default min validation level
        ValidationLevel validationLevel = mergedNetwork.getValidationLevel(); // default min validation level
        for (Network n : networks) {
            if (n instanceof NetworkImpl networkImpl) {
                minLevel = ValidationLevel.min(minLevel, networkImpl.getMinValidationLevel());
                validationLevel = ValidationLevel.min(validationLevel, networkImpl.getValidationLevel());
            }
        }
        mergedNetwork.setMinimumAcceptableValidationLevel(minLevel);
        mergedNetwork.setValidationLevelIfGreaterThan(validationLevel);
    }

    private static void setCommonCaseDate(NetworkImpl mergedNetwork, Network[] networks) {
        //if all subnetworks have same case date then apply it to merged network
        ZonedDateTime caseDate = networks[0].getCaseDate();
        for (Network n : networks) {
            if (!Objects.equals(caseDate, n.getCaseDate())) {
                return;
            }
        }
        mergedNetwork.setCaseDate(caseDate);
    }

    RefChain<NetworkImpl> getRef() {
        return ref;
    }

    @Override
    public RefChain<NetworkImpl> getRootNetworkRef() {
        return getRef();
    }

    public NetworkListenerList getListeners() {
        return listeners;
    }

    public NetworkIndex getIndex() {
        return index;
    }

    public Map<String, VoltageAngleLimit> getVoltageAngleLimitsIndex() {
        return voltageAngleLimitsIndex;
    }

    @Override
    public VoltageAngleLimit getVoltageAngleLimit(String id) {
        return voltageAngleLimitsIndex.get(id);
    }

    @Override
    public Stream<VoltageAngleLimit> getVoltageAngleLimitsStream() {
        return voltageAngleLimitsIndex.values().stream();
    }

    @Override
    public Iterable<VoltageAngleLimit> getVoltageAngleLimits() {
        return voltageAngleLimitsIndex.values();
    }

    @Override
    public NetworkImpl getNetwork() {
        return this;
    }

    @Override
    public Network getParentNetwork() {
        return this;
    }

    @Override
    public VariantManagerImpl getVariantManager() {
        return variantManager;
    }

    @Override
    public void allowReportNodeContextMultiThreadAccess(boolean allow) {
        this.reportNodeContext = Networks.allowReportNodeContextMultiThreadAccess(this.reportNodeContext, allow);
    }

    @Override
    public ReportNodeContext getReportNodeContext() {
        return this.reportNodeContext;
    }

    @Override
    public int getVariantIndex() {
        return variantManager.getVariantContext().getVariantIndex();
    }

    @Override
    public Set<Country> getCountries() {
        return getSubstationStream()
                .map(Substation::getCountry)
                .filter(Optional::isPresent).map(Optional::get)
                .collect(Collectors.toCollection(() -> EnumSet.noneOf(Country.class)));
    }

    @Override
    public int getCountryCount() {
        return getCountries().size();
    }

    @Override
    public Iterable<String> getAreaTypes() {
        return getAreaTypeStream().toList();
    }

    @Override
    public Stream<String> getAreaTypeStream() {
        return getAreaStream().map(Area::getAreaType).distinct();
    }

    @Override
    public int getAreaTypeCount() {
        return (int) getAreaTypeStream().count();
    }

    @Override
    public AreaAdder newArea() {
        return new AreaAdderImpl(ref, subnetworkRef);
    }

    @Override
    public Iterable<Area> getAreas() {
        return Collections.unmodifiableCollection(index.getAll(AreaImpl.class));
    }

    @Override
    public Stream<Area> getAreaStream() {
        return index.getAll(AreaImpl.class).stream().map(Function.identity());
    }

    @Override
    public Area getArea(String id) {
        return index.get(id, AreaImpl.class);
    }

    @Override
    public int getAreaCount() {
        return index.getAll(AreaImpl.class).size();
    }

    @Override
    public SubstationAdder newSubstation() {
        return new SubstationAdderImpl(ref, subnetworkRef);
    }

    @Override
    public Iterable<Substation> getSubstations() {
        return Collections.unmodifiableCollection(index.getAll(SubstationImpl.class));
    }

    @Override
    public Stream<Substation> getSubstationStream() {
        return index.getAll(SubstationImpl.class).stream().map(Function.identity());
    }

    @Override
    public int getSubstationCount() {
        return index.getAll(SubstationImpl.class).size();
    }

    @Override
    public Iterable<Substation> getSubstations(Country country, String tsoId, String... geographicalTags) {
        return getSubstations(Optional.ofNullable(country).map(Country::getName).orElse(null), tsoId, geographicalTags);
    }

    @Override
    public Iterable<Substation> getSubstations(String country, String tsoId, String... geographicalTags) {
        return Substations.filter(getSubstations(), country, tsoId, geographicalTags);
    }

    @Override
    public SubstationImpl getSubstation(String id) {
        return index.get(id, SubstationImpl.class);
    }

    @Override
    public VoltageLevelAdder newVoltageLevel() {
        return new VoltageLevelAdderImpl(ref, subnetworkRef);
    }

    @Override
    public Iterable<VoltageLevel> getVoltageLevels() {
        return Collections.unmodifiableCollection(index.getAll(VoltageLevelImpl.class));
    }

    @Override
    public Stream<VoltageLevel> getVoltageLevelStream() {
        return index.getAll(VoltageLevelImpl.class).stream().map(Function.identity());
    }

    @Override
    public int getVoltageLevelCount() {
        return index.getAll(VoltageLevelImpl.class).size();
    }

    @Override
    public VoltageLevelExt getVoltageLevel(String id) {
        return index.get(id, VoltageLevelExt.class);
    }

    @Override
    public LineAdderImpl newLine() {
        return newLine((String) null);
    }

    LineAdderImpl newLine(String subnetwork) {
        return new LineAdderImpl(this, subnetwork);
    }

    @Override
    public LineAdderImpl newLine(Line copiedLine) {
        return newLine(null, copiedLine);
    }

    LineAdderImpl newLine(String subnetwork, Line copiedLine) {
        return new LineAdderImpl(this, subnetwork, copiedLine);
    }

    @Override
    public Iterable<Line> getLines() {
        return Collections.unmodifiableCollection(index.getAll(LineImpl.class));
    }

    @Override
    public Iterable<TieLine> getTieLines() {
        return Collections.unmodifiableCollection(index.getAll(TieLineImpl.class));
    }

    @Override
    public Branch getBranch(String branchId) {
        Objects.requireNonNull(branchId);
        Branch branch = getLine(branchId);
        if (branch == null) {
            branch = getTwoWindingsTransformer(branchId);
            if (branch == null) {
                branch = getTieLine(branchId);
            }
        }
        return branch;
    }

    @Override
    public Iterable<Branch> getBranches() {
        return Iterables.concat(getLines(), getTwoWindingsTransformers(), getTieLines());
    }

    @Override
    public Stream<Branch> getBranchStream() {
        return Stream.of(getLineStream(), getTwoWindingsTransformerStream(), getTieLineStream()).flatMap(Function.identity());
    }

    @Override
    public int getBranchCount() {
        return getLineCount() + getTwoWindingsTransformerCount() + getTieLineCount();
    }

    @Override
    public Stream<Line> getLineStream() {
        return index.getAll(LineImpl.class).stream().map(Function.identity());
    }

    @Override
    public Stream<TieLine> getTieLineStream() {
        return index.getAll(TieLineImpl.class).stream().map(Function.identity());
    }

    @Override
    public int getLineCount() {
        return index.getAll(LineImpl.class).size();
    }

    @Override
    public int getTieLineCount() {
        return index.getAll(TieLineImpl.class).size();
    }

    @Override
    public Line getLine(String id) {
        return index.get(id, LineImpl.class);
    }

    @Override
    public TieLine getTieLine(String id) {
        return index.get(id, TieLineImpl.class);
    }

    @Override
    public TieLineAdderImpl newTieLine() {
        return newTieLine(null);
    }

    TieLineAdderImpl newTieLine(String subnetwork) {
        return new TieLineAdderImpl(this, subnetwork);
    }

    @Override
    public Iterable<TwoWindingsTransformer> getTwoWindingsTransformers() {
        return Collections.unmodifiableCollection(index.getAll(TwoWindingsTransformerImpl.class));
    }

    @Override
    public Stream<TwoWindingsTransformer> getTwoWindingsTransformerStream() {
        return index.getAll(TwoWindingsTransformerImpl.class).stream().map(Function.identity());
    }

    @Override
    public int getTwoWindingsTransformerCount() {
        return index.getAll(TwoWindingsTransformerImpl.class).size();
    }

    @Override
    public TwoWindingsTransformer getTwoWindingsTransformer(String id) {
        return index.get(id, TwoWindingsTransformerImpl.class);
    }

    @Override
    public Iterable<ThreeWindingsTransformer> getThreeWindingsTransformers() {
        return Collections.unmodifiableCollection(index.getAll(ThreeWindingsTransformerImpl.class));
    }

    @Override
    public Stream<ThreeWindingsTransformer> getThreeWindingsTransformerStream() {
        return index.getAll(ThreeWindingsTransformerImpl.class).stream().map(Function.identity());
    }

    @Override
    public int getThreeWindingsTransformerCount() {
        return index.getAll(ThreeWindingsTransformerImpl.class).size();
    }

    @Override
    public ThreeWindingsTransformer getThreeWindingsTransformer(String id) {
        return index.get(id, ThreeWindingsTransformerImpl.class);
    }

    @Override
    public Iterable<OverloadManagementSystem> getOverloadManagementSystems() {
        return Collections.unmodifiableCollection(index.getAll(OverloadManagementSystemImpl.class));
    }

    @Override
    public Stream<OverloadManagementSystem> getOverloadManagementSystemStream() {
        return index.getAll(OverloadManagementSystemImpl.class).stream().map(Function.identity());
    }

    @Override
    public int getOverloadManagementSystemCount() {
        return index.getAll(OverloadManagementSystemImpl.class).size();
    }

    @Override
    public OverloadManagementSystem getOverloadManagementSystem(String id) {
        return index.get(id, OverloadManagementSystemImpl.class);
    }

    @Override
    public Iterable<Generator> getGenerators() {
        return Collections.unmodifiableCollection(index.getAll(GeneratorImpl.class));
    }

    @Override
    public Stream<Generator> getGeneratorStream() {
        return index.getAll(GeneratorImpl.class).stream().map(Function.identity());
    }

    @Override
    public int getGeneratorCount() {
        return index.getAll(GeneratorImpl.class).size();
    }

    @Override
    public GeneratorImpl getGenerator(String id) {
        return index.get(id, GeneratorImpl.class);
    }

    @Override
    public Iterable<Battery> getBatteries() {
        return Collections.unmodifiableCollection(index.getAll(BatteryImpl.class));
    }

    @Override
    public Stream<Battery> getBatteryStream() {
        return index.getAll(BatteryImpl.class).stream().map(Function.identity());
    }

    @Override
    public int getBatteryCount() {
        return index.getAll(BatteryImpl.class).size();
    }

    @Override
    public BatteryImpl getBattery(String id) {
        return index.get(id, BatteryImpl.class);
    }

    @Override
    public Iterable<Load> getLoads() {
        return Collections.unmodifiableCollection(index.getAll(LoadImpl.class));
    }

    @Override
    public Stream<Load> getLoadStream() {
        return index.getAll(LoadImpl.class).stream().map(Function.identity());
    }

    @Override
    public int getLoadCount() {
        return index.getAll(LoadImpl.class).size();
    }

    @Override
    public LoadImpl getLoad(String id) {
        return index.get(id, LoadImpl.class);
    }

    @Override
    public Iterable<ShuntCompensator> getShuntCompensators() {
        return Collections.unmodifiableCollection(index.getAll(ShuntCompensatorImpl.class));
    }

    @Override
    public Stream<ShuntCompensator> getShuntCompensatorStream() {
        return index.getAll(ShuntCompensatorImpl.class).stream().map(Function.identity());
    }

    @Override
    public int getShuntCompensatorCount() {
        return index.getAll(ShuntCompensatorImpl.class).size();
    }

    @Override
    public ShuntCompensatorImpl getShuntCompensator(String id) {
        return index.get(id, ShuntCompensatorImpl.class);
    }

    @Override
    public Iterable<DanglingLine> getDanglingLines(DanglingLineFilter danglingLineFilter) {
        return getDanglingLineStream(danglingLineFilter).collect(Collectors.toList());
    }

    @Override
    public Stream<DanglingLine> getDanglingLineStream(DanglingLineFilter danglingLineFilter) {
        return index.getAll(DanglingLineImpl.class).stream().filter(danglingLineFilter.getPredicate()).map(Function.identity());
    }

    @Override
    public int getDanglingLineCount() {
        return index.getAll(DanglingLineImpl.class).size();
    }

    @Override
    public DanglingLineImpl getDanglingLine(String id) {
        return index.get(id, DanglingLineImpl.class);
    }

    @Override
    public Iterable<StaticVarCompensator> getStaticVarCompensators() {
        return Collections.unmodifiableCollection(index.getAll(StaticVarCompensatorImpl.class));
    }

    @Override
    public Stream<StaticVarCompensator> getStaticVarCompensatorStream() {
        return index.getAll(StaticVarCompensatorImpl.class).stream().map(Function.identity());
    }

    @Override
    public int getStaticVarCompensatorCount() {
        return index.getAll(StaticVarCompensatorImpl.class).size();
    }

    @Override
    public StaticVarCompensatorImpl getStaticVarCompensator(String id) {
        return index.get(id, StaticVarCompensatorImpl.class);
    }

    @Override
    public Switch getSwitch(String id) {
        return index.get(id, SwitchImpl.class);
    }

    @Override
    public Iterable<Switch> getSwitches() {
        return Collections.unmodifiableCollection(index.getAll(SwitchImpl.class));
    }

    @Override
    public Stream<Switch> getSwitchStream() {
        return index.getAll(SwitchImpl.class).stream().map(Function.identity());
    }

    @Override
    public int getSwitchCount() {
        return index.getAll(SwitchImpl.class).size();
    }

    @Override
    public BusbarSection getBusbarSection(String id) {
        return index.get(id, BusbarSectionImpl.class);
    }

    @Override
    public Iterable<BusbarSection> getBusbarSections() {
        return Collections.unmodifiableCollection(index.getAll(BusbarSectionImpl.class));
    }

    @Override
    public Stream<BusbarSection> getBusbarSectionStream() {
        return index.getAll(BusbarSectionImpl.class).stream().map(Function.identity());
    }

    @Override
    public int getBusbarSectionCount() {
        return index.getAll(BusbarSectionImpl.class).size();
    }

    @Override
    public AbstractHvdcConverterStation<?> getHvdcConverterStation(String id) {
        AbstractHvdcConverterStation<?> converterStation = getLccConverterStation(id);
        if (converterStation == null) {
            converterStation = getVscConverterStation(id);
        }
        return converterStation;
    }

    @Override
    public int getHvdcConverterStationCount() {
        return getLccConverterStationCount() + getVscConverterStationCount();
    }

    @Override
    public Iterable<HvdcConverterStation<?>> getHvdcConverterStations() {
        return Iterables.concat(getLccConverterStations(), getVscConverterStations());
    }

    @Override
    public Stream<HvdcConverterStation<?>> getHvdcConverterStationStream() {
        return Stream.concat(getLccConverterStationStream(), getVscConverterStationStream());
    }

    @Override
    public Iterable<LccConverterStation> getLccConverterStations() {
        return Collections.unmodifiableCollection(index.getAll(LccConverterStationImpl.class));
    }

    @Override
    public Stream<LccConverterStation> getLccConverterStationStream() {
        return index.getAll(LccConverterStationImpl.class).stream().map(Function.identity());
    }

    @Override
    public int getLccConverterStationCount() {
        return index.getAll(LccConverterStationImpl.class).size();
    }

    @Override
    public LccConverterStationImpl getLccConverterStation(String id) {
        return index.get(id, LccConverterStationImpl.class);
    }

    @Override
    public Iterable<VscConverterStation> getVscConverterStations() {
        return Collections.unmodifiableCollection(index.getAll(VscConverterStationImpl.class));
    }

    @Override
    public Stream<VscConverterStation> getVscConverterStationStream() {
        return index.getAll(VscConverterStationImpl.class).stream().map(Function.identity());
    }

    @Override
    public int getVscConverterStationCount() {
        return index.getAll(VscConverterStationImpl.class).size();
    }

    @Override
    public VscConverterStationImpl getVscConverterStation(String id) {
        return index.get(id, VscConverterStationImpl.class);
    }

    @Override
    public HvdcLine getHvdcLine(String id) {
        return index.get(id, HvdcLineImpl.class);
    }

    @Override
    public HvdcLine getHvdcLine(HvdcConverterStation converterStation) {
        return getHvdcLineStream()
                .filter(l -> l.getConverterStation1() == converterStation || l.getConverterStation2() == converterStation)
                .findFirst()
                .orElse(null);
    }

    @Override
    public int getHvdcLineCount() {
        return index.getAll(HvdcLineImpl.class).size();
    }

    @Override
    public Iterable<HvdcLine> getHvdcLines() {
        return Collections.unmodifiableCollection(index.getAll(HvdcLineImpl.class));
    }

    @Override
    public Stream<HvdcLine> getHvdcLineStream() {
        return index.getAll(HvdcLineImpl.class).stream().map(Function.identity());
    }

    @Override
    public HvdcLineAdder newHvdcLine() {
        return newHvdcLine(null);
    }

    @Override
    public Ground getGround(String id) {
        return index.get(id, GroundImpl.class);
    }

    @Override
    public Iterable<Ground> getGrounds() {
        return Collections.unmodifiableCollection(index.getAll(GroundImpl.class));
    }

    @Override
    public Stream<Ground> getGroundStream() {
        return index.getAll(GroundImpl.class).stream().map(Function.identity());
    }

    @Override
    public int getGroundCount() {
        return index.getAll(GroundImpl.class).size();
    }

    HvdcLineAdder newHvdcLine(String subnetwork) {
        return new HvdcLineAdderImpl(this, subnetwork);
    }

    @Override
    public Identifiable<?> getIdentifiable(String id) {
        return index.get(id, Identifiable.class);
    }

    @Override
    public Collection<Identifiable<?>> getIdentifiables() {
        return index.getAll();
    }

    @Override
    public <C extends Connectable> Iterable<C> getConnectables(Class<C> clazz) {
        return getConnectableStream(clazz).collect(Collectors.toList());
    }

    @Override
    public <C extends Connectable> Stream<C> getConnectableStream(Class<C> clazz) {
        return index.getAll().stream().filter(clazz::isInstance).map(clazz::cast);
    }

    @Override
    public <C extends Connectable> int getConnectableCount(Class<C> clazz) {
        return Ints.checkedCast(getConnectableStream(clazz).count());
    }

    @Override
    public Iterable<Connectable> getConnectables() {
        return getConnectables(Connectable.class);
    }

    @Override
    public Stream<Connectable> getConnectableStream() {
        return getConnectableStream(Connectable.class);
    }

    @Override
    public Connectable<?> getConnectable(String id) {
        return index.get(id, Connectable.class);
    }

    @Override
    public int getConnectableCount() {
        return Ints.checkedCast(getConnectableStream().count());
    }

    @Override
    public VoltageAngleLimitAdder newVoltageAngleLimit() {
        return newVoltageAngleLimit(null);
    }

    VoltageAngleLimitAdder newVoltageAngleLimit(String subnetwork) {
        return new VoltageAngleLimitAdderImpl(this, subnetwork);
    }

    @Override
    public BusBreakerViewImpl getBusBreakerView() {
        return busBreakerView;
    }

    @Override
    public BusViewImpl getBusView() {
        return busView;
    }

    static final class ConnectedComponentsManager extends AbstractConnectedComponentsManager<ConnectedComponentImpl> {

        private final NetworkImpl network;

        private ConnectedComponentsManager(NetworkImpl network) {
            this.network = Objects.requireNonNull(network);
        }

        @Override
        protected Network getNetwork() {
            return network;
        }

        @Override
        protected void setComponentNumber(Bus bus, int num) {
            Objects.requireNonNull(bus);
            ((BusExt) bus).setConnectedComponentNumber(num);
        }

        @Override
        protected ConnectedComponentImpl createComponent(int num, int size) {
            return new ConnectedComponentImpl(num, size, network.ref);
        }
    }

    static final class SynchronousComponentsManager extends AbstractSynchronousComponentsManager<SynchronousComponentImpl> {

        private final NetworkImpl network;

        private SynchronousComponentsManager(NetworkImpl network) {
            this.network = Objects.requireNonNull(network);
        }

        @Override
        protected Network getNetwork() {
            return network;
        }

        @Override
        protected void setComponentNumber(Bus bus, int num) {
            Objects.requireNonNull(bus);
            ((BusExt) bus).setSynchronousComponentNumber(num);
        }

        @Override
        protected SynchronousComponentImpl createComponent(int num, int size) {
            return new SynchronousComponentImpl(num, size, network.ref);
        }
    }

    /**
     * Caching buses by their ID :
     * the cache is fully builts on first call to {@link BusCache#getBus(String)},
     * and must be invalidated on any topology change.
     */
    private static final class BusCache {

        private final Supplier<Stream<Bus>> busStream;
        private Map<String, Bus> cache;

        private BusCache(Supplier<Stream<Bus>> busStream) {
            this.busStream = busStream;
        }

        private void buildCache() {
            cache = busStream.get().collect(ImmutableMap.toImmutableMap(Bus::getId, Functions.identity()));
        }

        synchronized void invalidate() {
            cache = null;
        }

        private synchronized Map<String, Bus> getCache() {
            if (cache == null) {
                buildCache();
            }
            return cache;
        }

        Bus getBus(String id) {
            return getCache().get(id);
        }
    }

    private class VariantImpl implements Variant {

        private final ConnectedComponentsManager connectedComponentsManager
                = new ConnectedComponentsManager(NetworkImpl.this);

        private final SynchronousComponentsManager synchronousComponentsManager
                = new SynchronousComponentsManager(NetworkImpl.this);

        private final BusCache busViewCache = new BusCache(() -> getVoltageLevelStream().flatMap(vl -> vl.getBusView().getBusStream()));

        //For bus breaker view, we exclude bus breaker topologies from the cache,
        //because thoses buses are already indexed in the NetworkIndex
        private final BusCache busBreakerViewCache = new BusCache(() -> getVoltageLevelStream()
                .filter(vl -> vl.getTopologyKind() != TopologyKind.BUS_BREAKER)
                .flatMap(vl -> vl.getBusBreakerView().getBusStream()));

        @Override
        public VariantImpl copy() {
            return new VariantImpl();
        }

    }

    private final VariantArray<VariantImpl> variants;

    ConnectedComponentsManager getConnectedComponentsManager() {
        return variants.get().connectedComponentsManager;
    }

    SynchronousComponentsManager getSynchronousComponentsManager() {
        return variants.get().synchronousComponentsManager;
    }

    @Override
    public void extendVariantArraySize(int initVariantArraySize, int number, final int sourceIndex) {
        super.extendVariantArraySize(initVariantArraySize, number, sourceIndex);

        variants.push(number, () -> variants.copy(sourceIndex));
    }

    @Override
    public void reduceVariantArraySize(int number) {
        super.reduceVariantArraySize(number);

        variants.pop(number);
    }

    @Override
    public void deleteVariantArrayElement(int index) {
        super.deleteVariantArrayElement(index);

        variants.delete(index);
    }

    @Override
    public void allocateVariantArrayElement(int[] indexes, final int sourceIndex) {
        super.allocateVariantArrayElement(indexes, sourceIndex);

        variants.allocate(indexes, () -> variants.copy(sourceIndex));
    }

    private static void checkIndependentNetwork(Network network) {
        if (network instanceof SubnetworkImpl) {
            throw new IllegalArgumentException("The network " + network.getId() + " is already a subnetwork");
        }
        if (!network.getSubnetworks().isEmpty()) {
            throw new IllegalArgumentException("The network " + network.getId() + " already contains subnetworks: not supported");
        }
    }

    private void merge(Network other) {
        checkIndependentNetwork(other);
        NetworkImpl otherNetwork = (NetworkImpl) other;

        // this check must not be done on the number of variants but on the size
        // of the internal variant array because the network can have only
        // one variant but an internal array with a size greater than one and
        // some re-usable variants
        if (variantManager.getVariantArraySize() != 1 || otherNetwork.variantManager.getVariantArraySize() != 1) {
            throw new PowsyblException("Merging of multi-variants network is not supported");
        }

        long start = System.currentTimeMillis();

        checkMergeability(otherNetwork);

        // try to find dangling lines couples
        List<DanglingLinePair> lines = new ArrayList<>();
        Map<String, List<DanglingLine>> dl1byPairingKey = new HashMap<>();

        for (DanglingLine dl1 : getDanglingLines(DanglingLineFilter.ALL)) {
            if (dl1.getPairingKey() != null) {
                dl1byPairingKey.computeIfAbsent(dl1.getPairingKey(), k -> new ArrayList<>()).add(dl1);
            }
        }
        for (DanglingLine dl2 : findCandidateDanglingLines(other, dl1byPairingKey::containsKey)) {
            findAndAssociateDanglingLines(dl2, dl1byPairingKey::get, (dll1, dll2) -> pairDanglingLines(lines, dll1, dll2, dl1byPairingKey));
        }

        // create a subnetwork for the other network
        createSubnetwork(this, otherNetwork);

        // do not forget to remove the other network from its index!!!
        otherNetwork.index.remove(otherNetwork);

        // merge the indexes
        index.merge(otherNetwork.index);

        replaceDanglingLineByTieLine(lines);

        other.getVoltageAngleLimits().forEach(l -> getVoltageAngleLimitsIndex().put(l.getId(), l));

        // update the source format
        if (!sourceFormat.equals(otherNetwork.sourceFormat)) {
            sourceFormat = "hybrid";
        }

        LOGGER.info("Merging of {} done in {} ms", id, System.currentTimeMillis() - start);
    }

    private void checkMergeability(NetworkImpl otherNetwork) {
        // Check if the intersection of identifiable ids is empty
        Multimap<Class<? extends Identifiable>, String> intersection = index.intersection(otherNetwork.index);
        for (Map.Entry<Class<? extends Identifiable>, Collection<String>> entry : intersection.asMap().entrySet()) {
            Class<? extends Identifiable> clazz = entry.getKey();
            Collection<String> objs = entry.getValue();
            if (!objs.isEmpty()) {
                throw new PowsyblException("The following object(s) of type "
                        + clazz.getSimpleName() + " exist(s) in both networks: "
                        + objs);
            }
        }

        // Check if the intersection of VoltageAngleLimit ids is empty
        Set<String> intersectionVoltageAngleLimits = getVoltageAngleLimitsIndex().keySet().stream()
                .filter(otherNetwork.getVoltageAngleLimitsIndex()::containsKey)
                .collect(Collectors.toSet());
        if (!intersectionVoltageAngleLimits.isEmpty()) {
            throw new PowsyblException("The following voltage angle limit(s) exist(s) in both networks: "
                    + intersectionVoltageAngleLimits);
        }
    }

    private static void createSubnetwork(NetworkImpl parent, NetworkImpl original) {
        // The root network reference should point to parent and not original anymore.
        // All substations/voltage levels will this way refer to parent instead of original.
        // Note that "ref" should directly reference the parent network's ref and not reference directly
        // the parent network. This is needed to avoid inconsistencies if the whole network is latter flatten
        // then merged with another one (see "#flatten" for further details).
        original.ref.setRef(parent.ref);

        // Handles the case of creating a subnetwork for itself without duplicating the id
        String idSubNetwork = parent != original ? original.getId() : Identifiables.getUniqueId(original.getId(), parent.getIndex()::contains);

        SubnetworkImpl sn = new SubnetworkImpl(
                original.ref, original.subnetworkRef, idSubNetwork, original.name, original.sourceFormat, original.getCaseDate());
        transferExtensions(original, sn);
        transferProperties(original, sn);
        parent.subnetworks.put(idSubNetwork, sn);
        parent.index.checkAndAdd(sn);
    }

    private void pairDanglingLines(List<DanglingLinePair> danglingLinePairs, DanglingLine dl1, DanglingLine dl2, Map<String, List<DanglingLine>> dl1byPairingKey) {
        if (dl1 != null) {
            if (dl1.getPairingKey() != null) {
                dl1byPairingKey.get(dl1.getPairingKey()).remove(dl1);
            }
            DanglingLinePair l = new DanglingLinePair();
            l.id = buildMergedId(dl1.getId(), dl2.getId());
            l.name = buildMergedName(dl1.getId(), dl2.getId(), dl1.getOptionalName().orElse(null), dl2.getOptionalName().orElse(null));
            l.dl1Id = dl1.getId();
            l.dl2Id = dl2.getId();
            l.aliases = new HashMap<>();
            // No need to merge properties or aliases because we keep the original dangling lines after merge
            danglingLinePairs.add(l);

            if (dl1.getId().equals(dl2.getId())) { // if identical IDs, rename dangling lines
                ((DanglingLineImpl) dl1).replaceId(l.dl1Id + "_1");
                ((DanglingLineImpl) dl2).replaceId(l.dl2Id + "_2");
                l.dl1Id = dl1.getId();
                l.dl2Id = dl2.getId();
            } else if (l.dl1Id.compareTo(l.dl2Id) > 0) {
                // Invert the ids to always have them in lexicographical order (to ensure reproducibility)
                var tmp = l.dl1Id;
                l.dl1Id = l.dl2Id;
                l.dl2Id = tmp;
            }
        }
    }

    private void replaceDanglingLineByTieLine(List<DanglingLinePair> lines) {
        for (DanglingLinePair danglingLinePair : lines) {
            LOGGER.debug("Creating tie line '{}' between dangling line couple '{}' and '{}",
                    danglingLinePair.id, danglingLinePair.dl1Id, danglingLinePair.dl2Id);
            TieLineImpl l = newTieLine()
                    .setId(danglingLinePair.id)
                    .setEnsureIdUnicity(true)
                    .setName(danglingLinePair.name)
                    .setDanglingLine1(danglingLinePair.dl1Id)
                    .setDanglingLine2(danglingLinePair.dl2Id)
                    .add();
            danglingLinePair.properties.forEach((key, val) -> l.setProperty(key.toString(), val.toString()));
            danglingLinePair.aliases.forEach((alias, type) -> {
                if (type.isEmpty()) {
                    l.addAlias(alias);
                } else {
                    l.addAlias(alias, type);
                }
            });
        }
    }

    static class DanglingLinePair {
        String id;
        String name;
        String dl1Id;
        String dl2Id;
        Map<String, String> aliases;
        Properties properties = new Properties();
    }

    @Override
    public Network createSubnetwork(String subnetworkId, String name, String sourceFormat) {
        if (subnetworks.containsKey(subnetworkId)) {
            throw new IllegalArgumentException("The network already contains another subnetwork of id " + subnetworkId);
        }
        SubnetworkImpl subnetwork = new SubnetworkImpl(new RefChain<>(ref), subnetworkId, name, sourceFormat);
        subnetworks.put(subnetworkId, subnetwork);
        index.checkAndAdd(subnetwork);
        return subnetwork;
    }

    /**
     * {@inheritDoc}
     * <p>Since {@link NetworkImpl} instances are already independent networks, this method throws an {@link IllegalStateException}. </p>
     */
    @Override
    public Network detach() {
        throw new IllegalStateException("This network is already detached.");
    }

    /**
     * {@inheritDoc}
     * <p>Since {@link NetworkImpl} instances are independent networks and can't thus be detached, this method returns <code>false</code>.</p>
     * @return false
     */
    @Override
    public boolean isDetachable() {
        return false;
    }

    @Override
    public Set<Identifiable<?>> getBoundaryElements() {
        return getDanglingLineStream(DanglingLineFilter.UNPAIRED).collect(Collectors.toSet());
    }

    @Override
    public boolean isBoundaryElement(Identifiable<?> identifiable) {
        return identifiable.getType() == IdentifiableType.DANGLING_LINE && !((DanglingLine) identifiable).isPaired();
    }

    @Override
    public void flatten() {
        if (subnetworks.isEmpty()) {
            // Nothing to do
            return;
        }
        subnetworks.values().forEach(subnetwork -> {
            // The subnetwork ref chain should point to the current network's subnetworkRef
            // (thus, we obtain a "double ref chain": a refChain referencing another refChain).
            // This way, all its network elements (using this ref chain) will have a reference to the current network
            // if it is merged later.
            subnetwork.getRef().setRef(this.subnetworkRef);
            // Transfer the extensions and the properties from the subnetwork to the current network.
            // Those which are already present in the current network are not transferred.
            transferExtensions(subnetwork, this, true);
            transferProperties(subnetwork, this, true);
            index.remove(subnetwork);
        });
        subnetworks.clear();
    }

    @Override
    public void addListener(NetworkListener listener) {
        listeners.add(listener);
    }

    @Override
    public void removeListener(NetworkListener listener) {
        listeners.remove(listener);
    }

    @Override
    public ValidationLevel runValidationChecks() {
        return runValidationChecks(true);
    }

    @Override
    public ValidationLevel runValidationChecks(boolean throwsException) {
        return runValidationChecks(throwsException, ReportNode.NO_OP);
    }

    @Override
    public ValidationLevel runValidationChecks(boolean throwsException, ReportNode reportNode) {
        ReportNode readReportNode = NetworkReports.runIidmNetworkValidationCHecks(reportNode, id);
        validationLevel = ValidationUtil.validate(Collections.unmodifiableCollection(index.getAll()),
                true, throwsException ? ValidationUtil.ActionOnError.THROW_EXCEPTION : ValidationUtil.ActionOnError.LOG_ERROR, validationLevel != null ? validationLevel : minValidationLevel, readReportNode);
        return validationLevel;
    }

    @Override
    public ValidationLevel getValidationLevel() {
        if (validationLevel == null) {
            validationLevel = ValidationUtil.validate(Collections.unmodifiableCollection(index.getAll()), false, ValidationUtil.ActionOnError.IGNORE, minValidationLevel, ReportNode.NO_OP);
        }
        return validationLevel;
    }

    @Override
    public Network setMinimumAcceptableValidationLevel(ValidationLevel minLevel) {
        Objects.requireNonNull(minLevel);
        ValidationLevel currentLevel = getValidationLevel();
        if (currentLevel.compareTo(minLevel) < 0) {
            throw new ValidationException(this, "Network should be corrected in order to correspond to validation level " + minLevel);
        }
        this.minValidationLevel = minLevel;
        return this;
    }

    ValidationLevel getMinValidationLevel() {
        return minValidationLevel;
    }

    void setValidationLevelIfGreaterThan(ValidationLevel validationLevel) {
        if (this.validationLevel != null) {
            this.validationLevel = ValidationLevel.min(this.validationLevel, validationLevel);
        }
    }

    void invalidateValidationLevel() {
        if (minValidationLevel.compareTo(ValidationLevel.STEADY_STATE_HYPOTHESIS) < 0) {
            validationLevel = null;
        }
    }
}