|
| 1 | +/* |
| 2 | + * Copyright (c) 2025, RTE (http://www.rte-france.com) |
| 3 | + * This Source Code Form is subject to the terms of the Mozilla Public |
| 4 | + * License, v. 2.0. If a copy of the MPL was not distributed with this |
| 5 | + * file, You can obtain one at http://mozilla.org/MPL/2.0/. |
| 6 | + * SPDX-License-Identifier: MPL-2.0 |
| 7 | + */ |
| 8 | +package com.powsybl.nad.build.iidm; |
| 9 | + |
| 10 | +import com.powsybl.commons.PowsyblException; |
| 11 | +import com.powsybl.iidm.network.*; |
| 12 | +import com.powsybl.nad.build.GraphBuilder; |
| 13 | +import com.powsybl.nad.model.BranchEdge; |
| 14 | +import com.powsybl.nad.model.BusNode; |
| 15 | +import com.powsybl.nad.model.Graph; |
| 16 | +import com.powsybl.nad.model.VoltageLevelNode; |
| 17 | +import com.powsybl.nad.svg.EdgeInfo; |
| 18 | + |
| 19 | +import java.util.*; |
| 20 | +import java.util.stream.Collectors; |
| 21 | + |
| 22 | +/** |
| 23 | + * Graph builder that creates a graph based on substation countries. |
| 24 | + * Creates one VoltageLevelNode with one BusNode for each country found in the network substations, |
| 25 | + * and BranchEdges between countries representing the existing lines between countries. |
| 26 | + * |
| 27 | + * @author Florian Dupuy {@literal <florian.dupuy at rte-france.com>} |
| 28 | + */ |
| 29 | +public class CountryGraphBuilder implements GraphBuilder { |
| 30 | + |
| 31 | + private final Network network; |
| 32 | + private final IdProvider idProvider; |
| 33 | + private final CountryLabelProvider labelProvider; |
| 34 | + |
| 35 | + /** |
| 36 | + * Creates a new CountryGraphBuilder. |
| 37 | + * |
| 38 | + * @param network the network |
| 39 | + * @param idProvider the ID provider |
| 40 | + * @param labelProvider the label provider |
| 41 | + */ |
| 42 | + public CountryGraphBuilder(Network network, IdProvider idProvider, CountryLabelProvider labelProvider) { |
| 43 | + this.network = Objects.requireNonNull(network); |
| 44 | + this.idProvider = Objects.requireNonNull(idProvider); |
| 45 | + this.labelProvider = Objects.requireNonNull(labelProvider); |
| 46 | + } |
| 47 | + |
| 48 | + @Override |
| 49 | + public Graph buildGraph() { |
| 50 | + Graph graph = new Graph(); |
| 51 | + |
| 52 | + // Get all countries from substations |
| 53 | + Set<Country> countries = getCountries(); |
| 54 | + |
| 55 | + // Create a VoltageLevelNode with one BusNode for each country |
| 56 | + Map<Country, VoltageLevelNode> countryToVlNode = new EnumMap<>(Country.class); |
| 57 | + |
| 58 | + for (Country country : countries) { |
| 59 | + CountryLabelProvider.CountryLegend legend = labelProvider.getCountryLegend(country); |
| 60 | + VoltageLevelNode vlNode = new VoltageLevelNode( |
| 61 | + idProvider, |
| 62 | + country.name(), |
| 63 | + country.name(), |
| 64 | + false, |
| 65 | + true, |
| 66 | + legend.header(), |
| 67 | + legend.footer() |
| 68 | + ); |
| 69 | + |
| 70 | + BusNode busNode = new BusNode(idProvider, country.name(), Collections.emptyList(), null); |
| 71 | + |
| 72 | + vlNode.addBusNode(busNode); |
| 73 | + graph.addNode(vlNode); |
| 74 | + graph.addTextNode(vlNode); |
| 75 | + |
| 76 | + countryToVlNode.put(country, vlNode); |
| 77 | + } |
| 78 | + |
| 79 | + // Create edges between countries based on lines |
| 80 | + createCountryConnections(graph, countryToVlNode); |
| 81 | + |
| 82 | + return graph; |
| 83 | + } |
| 84 | + |
| 85 | + /** |
| 86 | + * Gets all countries from substations in the network. |
| 87 | + * |
| 88 | + * @return set of countries |
| 89 | + */ |
| 90 | + private Set<Country> getCountries() { |
| 91 | + return network.getSubstationStream() |
| 92 | + .map(Substation::getNullableCountry) |
| 93 | + .filter(Objects::nonNull) |
| 94 | + .collect(Collectors.toSet()); |
| 95 | + } |
| 96 | + |
| 97 | + /** |
| 98 | + * Creates connections (BranchEdges) between countries based on lines in the network. |
| 99 | + * |
| 100 | + * @param graph the graph to add edges to |
| 101 | + * @param countryToVlNode mapping from country to voltage level node |
| 102 | + */ |
| 103 | + private void createCountryConnections(Graph graph, |
| 104 | + Map<Country, VoltageLevelNode> countryToVlNode) { |
| 105 | + |
| 106 | + // Map to store aggregated active powers between countries |
| 107 | + Map<Border, BorderEdges> borderEdgesMap = new HashMap<>(); |
| 108 | + |
| 109 | + // Process all lines |
| 110 | + network.getLineStream().forEach(line -> fillBorderEdgesMap(line, borderEdgesMap)); |
| 111 | + network.getTieLineStream().forEach(tieLine -> fillBorderEdgesMap(tieLine, borderEdgesMap)); |
| 112 | + |
| 113 | + // Process HVDC lines |
| 114 | + network.getHvdcLineStream().forEach(hvdcLine -> fillBorderEdgesMap(hvdcLine, borderEdgesMap)); |
| 115 | + |
| 116 | + // Create BranchEdges for each country pair with connections |
| 117 | + borderEdgesMap.forEach((border, borderEdges) -> { |
| 118 | + Country country1 = border.country1(); |
| 119 | + Country country2 = border.country2(); |
| 120 | + |
| 121 | + VoltageLevelNode vlNode1 = countryToVlNode.get(country1); |
| 122 | + VoltageLevelNode vlNode2 = countryToVlNode.get(country2); |
| 123 | + |
| 124 | + if (vlNode1 != null && vlNode2 != null) { |
| 125 | + createCountryEdge(graph, country1, country2, borderEdges, vlNode1, vlNode2); |
| 126 | + } |
| 127 | + }); |
| 128 | + } |
| 129 | + |
| 130 | + /** |
| 131 | + * Processes a branch to aggregate active power between countries. |
| 132 | + */ |
| 133 | + private void fillBorderEdgesMap(Branch<?> branch, Map<Border, BorderEdges> allBorderLines) { |
| 134 | + Country country1 = getCountryFromTerminal(branch.getTerminal1()); |
| 135 | + Country country2 = getCountryFromTerminal(branch.getTerminal2()); |
| 136 | + |
| 137 | + if (country1 != null && country2 != null && country1 != country2) { |
| 138 | + Border pair = new Border(country1, country2); |
| 139 | + allBorderLines.computeIfAbsent(pair, k -> new BorderEdges()) |
| 140 | + .addBranch(branch); |
| 141 | + } |
| 142 | + } |
| 143 | + |
| 144 | + /** |
| 145 | + * Processes a tie line to aggregate active power between countries. |
| 146 | + */ |
| 147 | + private void fillBorderEdgesMap(HvdcLine hvdcLine, Map<Border, BorderEdges> allBorderLines) { |
| 148 | + Country country1 = getCountryFromTerminal(hvdcLine.getConverterStation1().getTerminal()); |
| 149 | + Country country2 = getCountryFromTerminal(hvdcLine.getConverterStation2().getTerminal()); |
| 150 | + |
| 151 | + if (country1 != null && country2 != null && country1 != country2) { |
| 152 | + Border pair = new Border(country1, country2); |
| 153 | + allBorderLines.computeIfAbsent(pair, k -> new BorderEdges()) |
| 154 | + .hvdcLines().add(hvdcLine); |
| 155 | + } |
| 156 | + } |
| 157 | + |
| 158 | + /** |
| 159 | + * Gets the country from a terminal's substation. |
| 160 | + */ |
| 161 | + private Country getCountryFromTerminal(Terminal terminal) { |
| 162 | + return terminal.getVoltageLevel().getSubstation().map(Substation::getNullableCountry).orElse(null); |
| 163 | + } |
| 164 | + |
| 165 | + /** |
| 166 | + * Creates a BranchEdge between two countries. |
| 167 | + */ |
| 168 | + private void createCountryEdge(Graph graph, Country country1, Country country2, BorderEdges borderEdges, |
| 169 | + VoltageLevelNode vlNode1, VoltageLevelNode vlNode2) { |
| 170 | + |
| 171 | + String edgeId = country1.name() + "-" + country2.name(); |
| 172 | + Optional<EdgeInfo> edgeInfo1 = labelProvider.getCountryEdgeInfo(country1, country2, borderEdges.lines, borderEdges.tieLines, borderEdges.hvdcLines, BranchEdge.Side.ONE); |
| 173 | + Optional<EdgeInfo> edgeInfo2 = labelProvider.getCountryEdgeInfo(country1, country2, borderEdges.lines, borderEdges.tieLines, borderEdges.hvdcLines, BranchEdge.Side.TWO); |
| 174 | + String label = labelProvider.getBranchLabel(country1, country2, borderEdges.lines, borderEdges.tieLines, borderEdges.hvdcLines); |
| 175 | + |
| 176 | + BranchEdge edge = new BranchEdge( |
| 177 | + idProvider, |
| 178 | + edgeId, |
| 179 | + edgeId, |
| 180 | + BranchEdge.LINE_EDGE, |
| 181 | + edgeInfo1.orElse(null), |
| 182 | + edgeInfo2.orElse(null), |
| 183 | + label |
| 184 | + ); |
| 185 | + |
| 186 | + graph.addEdge(vlNode1, vlNode1.getBusNodes().getFirst(), vlNode2, vlNode2.getBusNodes().getFirst(), edge); |
| 187 | + } |
| 188 | + |
| 189 | + /** |
| 190 | + * Record to represent a pair of countries, ensuring consistent ordering. |
| 191 | + */ |
| 192 | + private record Border(Country country1, Country country2) { |
| 193 | + Border { |
| 194 | + // Ensure consistent ordering to avoid duplicate pairs |
| 195 | + if (country1.compareTo(country2) > 0) { |
| 196 | + Country temp = country1; |
| 197 | + country1 = country2; |
| 198 | + country2 = temp; |
| 199 | + } |
| 200 | + } |
| 201 | + } |
| 202 | + |
| 203 | + private record BorderEdges(List<Line> lines, List<TieLine> tieLines, List<HvdcLine> hvdcLines) { |
| 204 | + private BorderEdges() { |
| 205 | + this(new ArrayList<>(), new ArrayList<>(), new ArrayList<>()); |
| 206 | + } |
| 207 | + |
| 208 | + public void addBranch(Branch<?> branch) { |
| 209 | + if (branch instanceof Line line) { |
| 210 | + lines.add(line); |
| 211 | + } else if (branch instanceof TieLine tieLine) { |
| 212 | + tieLines.add(tieLine); |
| 213 | + } else { |
| 214 | + throw new PowsyblException("Unexcepted branch class: " + branch.getClass()); |
| 215 | + } |
| 216 | + } |
| 217 | + } |
| 218 | +} |
0 commit comments