ContainersMapping.java

/**
 * Copyright (c) 2020, 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.iidm.network.util;

import com.powsybl.commons.PowsyblException;
import org.jgrapht.Graph;
import org.jgrapht.alg.connectivity.ConnectivityInspector;
import org.jgrapht.graph.Pseudograph;

import java.util.*;
import java.util.function.*;
import java.util.stream.Collectors;

/**
 * A utility class that create IIDM containers, i.e voltage levels and substations from a bus branch model with respect
 * to IIDM container requirements.
 *
 * @author Geoffroy Jamgotchian {@literal <geoffroy.jamgotchian at rte-france.com>}
 */
public class ContainersMapping {

    private final Map<Integer, String> busNumToVoltageLevelId = new HashMap<>();

    private final Map<String, Set<Integer>> voltageLevelIdToBusNums = new HashMap<>();

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

    public Set<Integer> getBusesSet(String voltageLevelId) {
        return voltageLevelIdToBusNums.computeIfAbsent(voltageLevelId, k -> new HashSet<>());
    }

    public boolean isBusDefined(int num) {
        return busNumToVoltageLevelId.containsKey(num);
    }

    public String getVoltageLevelId(int num) {
        String voltageLevelId = busNumToVoltageLevelId.get(num);
        if (voltageLevelId == null) {
            throw new PowsyblException("Bus " + num + " not found");
        }
        return voltageLevelId;
    }

    public String getSubstationId(String voltageLevelId) {
        String substationId = voltageLevelIdToSubstationId.get(voltageLevelId);
        if (substationId == null) {
            throw new PowsyblException("Voltage level '" + voltageLevelId + "' not found");
        }
        return substationId;
    }

    /**
     * @deprecated Not used anymore. Use
     * {@link ContainersMapping#create(List, List, ToIntFunction, ToIntFunction, ToIntFunction,
     * Predicate, Predicate, ToDoubleFunction, Function, Function)} instead.
     */
    @Deprecated(since = "4.9.2")
    public static <N, B> ContainersMapping create(List<N> buses, List<B> branches, ToIntFunction<N> busToNum,
                                                  ToIntFunction<B> branchToNum1, ToIntFunction<B> branchToNum2, ToIntFunction<B> branchToNum3,
                                                  ToDoubleFunction<B> branchToResistance, ToDoubleFunction<B> branchToReactance,
                                                  Predicate<B> branchToIsTransformer, Function<Set<Integer>, String> busesToVoltageLevelId,
                                                  IntFunction<String> substationNumToId) {
        throw new PowsyblException("Deprecated. Not used anymore");
    }

    public static <N, B> ContainersMapping create(List<N> buses, List<B> branches,
        ToIntFunction<N> busToNum, ToIntFunction<B> branchToNum1, ToIntFunction<B> branchToNum2,
        Predicate<B> branchToIsZeroImpedance, Predicate<B> branchToIsTransformer,
        ToDoubleFunction<Integer> busToNominalVoltage, Function<Set<Integer>, String> busesToVoltageLevelId,
        Function<Set<Integer>, String> busesToSubstationId) {

        Objects.requireNonNull(buses);
        Objects.requireNonNull(branches);
        Objects.requireNonNull(busToNum);
        Objects.requireNonNull(branchToNum1);
        Objects.requireNonNull(branchToNum2);
        Objects.requireNonNull(branchToIsTransformer);
        Objects.requireNonNull(branchToIsZeroImpedance);

        Objects.requireNonNull(busesToVoltageLevelId);
        Objects.requireNonNull(busesToSubstationId);

        ContainersMapping containersMapping = new ContainersMapping();

        // graph for calculating substations
        // group buses connected by zero impedance lines and transformers to substations
        Graph<Integer, B> sGraph = new Pseudograph<>(null, null, false);
        for (N bus : buses) {
            sGraph.addVertex(busToNum.applyAsInt(bus));
        }

        for (B branch : branches) {
            if (branchToIsZeroImpedance.test(branch) || branchToIsTransformer.test(branch)) {
                sGraph.addEdge(branchToNum1.applyAsInt(branch), branchToNum2.applyAsInt(branch), branch);
            }
        }

        // analyze each substation set
        for (Set<Integer> busNums : new ConnectivityInspector<>(sGraph).connectedSets()) {

            createAndMapSubstationAndVoltageLevelsInside(branchToNum1, branchToNum2, branchToIsZeroImpedance,
                busToNominalVoltage, busesToVoltageLevelId, busesToSubstationId, busNums, sGraph, containersMapping);
        }

        return containersMapping;
    }

    private static <B> void createAndMapSubstationAndVoltageLevelsInside(ToIntFunction<B> branchToNum1,
        ToIntFunction<B> branchToNum2, Predicate<B> branchToIsZeroImpedance,
        ToDoubleFunction<Integer> busToNominalVoltage, Function<Set<Integer>, String> busesToVoltageLevelId,
        Function<Set<Integer>, String> busesToSubstationId, Set<Integer> substationBusNums,
        Graph<Integer, B> sGraph, ContainersMapping containersMapping) {

        String substationId = busesToSubstationId.apply(substationBusNums);

        // build the graph for splitting substation buses into voltageLevels sets
        // avoid including edges between buses with the same nominal voltage, there are a lot

        Set<B> zeroImpedanceBranchesInsideSubstation = new HashSet<>();
        substationBusNums.forEach(bus -> zeroImpedanceBranchesInsideSubstation
            .addAll(sGraph.edgesOf(bus).stream().filter(branchToIsZeroImpedance::test).collect(Collectors.toSet())));

        Graph<Integer, Object> vlGraph = new Pseudograph<>(Object.class);
        Iterator<Integer> iter = substationBusNums.iterator();
        while (iter.hasNext()) {
            vlGraph.addVertex(iter.next());
        }
        zeroImpedanceBranchesInsideSubstation.forEach(branch -> vlGraph.addEdge(branchToNum1.applyAsInt(branch), branchToNum2.applyAsInt(branch)));

        if (busToNominalVoltage == null) {
            new ConnectivityInspector<>(vlGraph).connectedSets()
                .forEach(voltageLevelIds -> mapSubstationAndVoltageLevel(busesToVoltageLevelId, substationId, voltageLevelIds, containersMapping));
        } else {
            Map<String, Set<Integer>> vls = new HashMap<>();

            new ConnectivityInspector<>(vlGraph).connectedSets()
                .forEach(voltageLevelIds -> vls.merge(getNominalVoltage(voltageLevelIds, busToNominalVoltage), voltageLevelIds, ContainersMapping::unionSet));

            vls.values().forEach(voltageLevelIds -> mapSubstationAndVoltageLevel(busesToVoltageLevelId, substationId, voltageLevelIds, containersMapping));
        }
    }

    private static String getNominalVoltage(Set<Integer> voltageLevelIds, ToDoubleFunction<Integer> busToVoltageLevelNominal) {
        Objects.requireNonNull(busToVoltageLevelNominal);
        if (voltageLevelIds.isEmpty()) { // should never happen
            throw new PowsyblException("Unexpected empty connected set");
        }
        return String.valueOf(busToVoltageLevelNominal.applyAsDouble(voltageLevelIds.iterator().next()));
    }

    private static Set<Integer> unionSet(Set<Integer> set1, Set<Integer> set2) {
        Set<Integer> unionSet = new HashSet<>(set1);
        unionSet.addAll(set2);
        return unionSet;
    }

    private static void mapSubstationAndVoltageLevel(Function<Set<Integer>, String> busesToVoltageLevelId,
        String substationId, Set<Integer> busNums, ContainersMapping containersMapping) {

        String voltageLevelId = busesToVoltageLevelId.apply(busNums);
        containersMapping.voltageLevelIdToBusNums.put(voltageLevelId, busNums);
        for (int busNum : busNums) {
            containersMapping.busNumToVoltageLevelId.put(busNum, voltageLevelId);
        }
        containersMapping.voltageLevelIdToSubstationId.put(voltageLevelId, substationId);
    }
}