From fff42fb30c09024c8a017f0b684c24cf55108629 Mon Sep 17 00:00:00 2001 From: Geoffroy Jamgotchian Date: Fri, 3 May 2024 11:15:48 +0200 Subject: [PATCH] CGMES GL profile support (#741) Signed-off-by: Geoffroy Jamgotchian --- cpp/src/bindings.cpp | 9 +- cpp/src/pypowsybl-api.h | 3 + cpp/src/pypowsybl.cpp | 6 + cpp/src/pypowsybl.h | 8 + ...TestConfiguration_T4_BE_BB_Complete_v2.zip | Bin 0 -> 59929 bytes .../images/nad_microgridbe_force_layout.svg | 605 ++++++++++++++++++ docs/_static/images/nad_microgridbe_geo.svg | 605 ++++++++++++++++++ docs/user_guide/network_visualization.rst | 60 +- java/pom.xml | 5 + .../LinePositionDataframeAdder.java | 87 +++ .../LinePositionDataframeProvider.java | 96 +++ .../SubstationPositionDataframeAdder.java | 72 +++ .../SubstationPositionDataframeProvider.java | 81 +++ .../python/commons/PyPowsyblApiHeader.java | 18 + .../python/network/NetworkCFunctions.java | 14 +- .../powsybl-cgmes-gl/resource-config.json | 6 + .../powsybl-iidm-api/resource-config.json | 3 +- pypowsybl/_pypowsybl.pyi | 18 + pypowsybl/network/__init__.py | 1 + pypowsybl/network/impl/nad_parameters.py | 46 +- tests/test_network.py | 32 +- tests/test_network_extensions.py | 27 +- 22 files changed, 1791 insertions(+), 11 deletions(-) create mode 100644 data/MicroGridTestConfiguration_T4_BE_BB_Complete_v2.zip create mode 100644 docs/_static/images/nad_microgridbe_force_layout.svg create mode 100644 docs/_static/images/nad_microgridbe_geo.svg create mode 100644 java/src/main/java/com/powsybl/dataframe/network/extensions/LinePositionDataframeAdder.java create mode 100644 java/src/main/java/com/powsybl/dataframe/network/extensions/LinePositionDataframeProvider.java create mode 100644 java/src/main/java/com/powsybl/dataframe/network/extensions/SubstationPositionDataframeAdder.java create mode 100644 java/src/main/java/com/powsybl/dataframe/network/extensions/SubstationPositionDataframeProvider.java create mode 100644 java/src/main/resources/META-INF/native-image/com.powsybl/powsybl-cgmes-gl/resource-config.json diff --git a/cpp/src/bindings.cpp b/cpp/src/bindings.cpp index b7e15d30c9..01ac372aac 100644 --- a/cpp/src/bindings.cpp +++ b/cpp/src/bindings.cpp @@ -486,6 +486,10 @@ PYBIND11_MODULE(_pypowsybl, m) { .def_readwrite("topological_coloring", &pypowsybl::SldParameters::topological_coloring) .def_readwrite("component_library", &pypowsybl::SldParameters::component_library); + py::enum_(m, "NadLayoutType") + .value("FORCE_LAYOUT", pypowsybl::NadLayoutType::FORCE_LAYOUT) + .value("GEOGRAPHICAL", pypowsybl::NadLayoutType::GEOGRAPHICAL); + py::class_(m, "NadParameters") .def(py::init(&pypowsybl::createNadParameters)) .def_readwrite("edge_name_displayed", &pypowsybl::NadParameters::edge_name_displayed) @@ -496,7 +500,10 @@ PYBIND11_MODULE(_pypowsybl, m) { .def_readwrite("voltage_value_precision", &pypowsybl::NadParameters::voltage_value_precision) .def_readwrite("id_displayed", &pypowsybl::NadParameters::id_displayed) .def_readwrite("bus_legend", &pypowsybl::NadParameters::bus_legend) - .def_readwrite("substation_description_displayed", &pypowsybl::NadParameters::substation_description_displayed); + .def_readwrite("substation_description_displayed", &pypowsybl::NadParameters::substation_description_displayed) + .def_readwrite("layout_type", &pypowsybl::NadParameters::layout_type) + .def_readwrite("scaling_factor", &pypowsybl::NadParameters::scaling_factor) + .def_readwrite("radius_factor", &pypowsybl::NadParameters::radius_factor); m.def("write_single_line_diagram_svg", &pypowsybl::writeSingleLineDiagramSvg, "Write single line diagram SVG", py::arg("network"), py::arg("container_id"), py::arg("svg_file"), py::arg("metadata_file"), py::arg("sld_parameters")); diff --git a/cpp/src/pypowsybl-api.h b/cpp/src/pypowsybl-api.h index c7c5abcb92..396499969d 100644 --- a/cpp/src/pypowsybl-api.h +++ b/cpp/src/pypowsybl-api.h @@ -350,6 +350,9 @@ typedef struct nad_parameters_struct { int voltage_value_precision; unsigned char substation_description_displayed; unsigned char bus_legend; + int layout_type; + int scaling_factor; + double radius_factor; } nad_parameters; typedef enum { diff --git a/cpp/src/pypowsybl.cpp b/cpp/src/pypowsybl.cpp index 6508c85059..98e14aed9b 100644 --- a/cpp/src/pypowsybl.cpp +++ b/cpp/src/pypowsybl.cpp @@ -1333,6 +1333,9 @@ NadParameters::NadParameters(nad_parameters* src) { voltage_value_precision = src->voltage_value_precision; substation_description_displayed = src->substation_description_displayed; bus_legend = src->bus_legend; + layout_type = static_cast(src->layout_type); + scaling_factor = src->scaling_factor; + radius_factor = src->radius_factor; } void SldParameters::sld_to_c_struct(sld_parameters& res) const { @@ -1355,6 +1358,9 @@ void NadParameters::nad_to_c_struct(nad_parameters& res) const { res.voltage_value_precision = voltage_value_precision; res.substation_description_displayed = substation_description_displayed; res.bus_legend = bus_legend; + res.layout_type = (int) layout_type; + res.scaling_factor = scaling_factor; + res.radius_factor = radius_factor; } std::shared_ptr SldParameters::to_c_struct() const { diff --git a/cpp/src/pypowsybl.h b/cpp/src/pypowsybl.h index b81b6b943d..becb3bf179 100644 --- a/cpp/src/pypowsybl.h +++ b/cpp/src/pypowsybl.h @@ -281,6 +281,11 @@ class SldParameters { std::string component_library; }; +enum class NadLayoutType { + FORCE_LAYOUT = 0, + GEOGRAPHICAL +}; + class NadParameters { public: NadParameters(nad_parameters* src); @@ -296,6 +301,9 @@ class NadParameters { int voltage_value_precision; bool bus_legend; bool substation_description_displayed; + NadLayoutType layout_type; + int scaling_factor; + double radius_factor; }; char* copyStringToCharPtr(const std::string& str); diff --git a/data/MicroGridTestConfiguration_T4_BE_BB_Complete_v2.zip b/data/MicroGridTestConfiguration_T4_BE_BB_Complete_v2.zip new file mode 100644 index 0000000000000000000000000000000000000000..0cac5e1f5464be2c159b309d4e87816b4768a88a GIT binary patch literal 59929 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�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 RUi_?=7valnXXDq={{v`|$!Guo literal 0 HcmV?d00001 diff --git a/docs/_static/images/nad_microgridbe_force_layout.svg b/docs/_static/images/nad_microgridbe_force_layout.svg new file mode 100644 index 0000000000..efff0b4e15 --- /dev/null +++ b/docs/_static/images/nad_microgridbe_force_layout.svg @@ -0,0 +1,605 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -3 + + + + + + + + + + + + + + + + + -17 + + + + + + + + + + + + + + + + + -1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -102 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -83 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
380.0
+ + + + + +
+
+
414.1 kV / -21.5°
+
+
+ +
+
10.5
+ + + + + +
+
+
10.8 kV / -19.6°
+
+
+ +
+
225.0
+ + + + + +
+
+
223.4 kV / -17.4°
+
+
+ +
+
110.0
+ + + + + +
+
+
115.5 kV / -22.0°
+
+
+ +
+
21.0
+ + + + + +
+
+
22.0 kV / -20.6°
+
+
+ +
+
225.0
+ + + + + +
+
+
224.2 kV / -21.8°
+
+
+ +
+
220.0
+ + + + + +
+
+
223.4 kV / -17.4°
+
+
+
+
diff --git a/docs/_static/images/nad_microgridbe_geo.svg b/docs/_static/images/nad_microgridbe_geo.svg new file mode 100644 index 0000000000..0a363fba77 --- /dev/null +++ b/docs/_static/images/nad_microgridbe_geo.svg @@ -0,0 +1,605 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -3 + + + + + + + + + + + + + + + + + -17 + + + + + + + + + + + + + + + + + -1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -102 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -83 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
380.0
+ + + + + +
+
+
414.1 kV / -21.5°
+
+
+ +
+
10.5
+ + + + + +
+
+
10.8 kV / -19.6°
+
+
+ +
+
225.0
+ + + + + +
+
+
223.4 kV / -17.4°
+
+
+ +
+
110.0
+ + + + + +
+
+
115.5 kV / -22.0°
+
+
+ +
+
21.0
+ + + + + +
+
+
22.0 kV / -20.6°
+
+
+ +
+
225.0
+ + + + + +
+
+
224.2 kV / -21.8°
+
+
+ +
+
220.0
+ + + + + +
+
+
223.4 kV / -17.4°
+
+
+
+
diff --git a/docs/user_guide/network_visualization.rst b/docs/user_guide/network_visualization.rst index 5a0f84fd7f..b4eda2a008 100644 --- a/docs/user_guide/network_visualization.rst +++ b/docs/user_guide/network_visualization.rst @@ -119,4 +119,62 @@ In order to get a list of the displayed voltage levels from an input voltage lev .. code-block:: python >>> network = pp.network.create_ieee300() - >>> list_vl = network.get_network_area_diagram_displayed_voltage_levels('VL1', 1) \ No newline at end of file + >>> list_vl = network.get_network_area_diagram_displayed_voltage_levels('VL1', 1) + +Network area diagram using geographical data +-------------------------------------------- + +We can load a network with geographical data (in WGS84 coordinates system) for substations and lines (in that case, +the geographical positions represent the line path). One way to do that is to load a CGMES file containing +a GL profile (Graphical Layout). By default this profile is not read. To activate GL profile loading and +creation of substations ans lines geographical positions in the PowSyBl network model we have to pass an +additional parameter to the load function. + +.. code-block:: python + + >>> network = pp.network.load('MicroGridTestConfiguration_T4_BE_BB_Complete_v2.zip', {'iidm.import.cgmes.post-processors': 'cgmesGLImport'}) + +We can now check loaded position by displaying `SubstationPosition` and `LinePosition` extensions. + +.. code-block:: python + + >>> n.get_extension('substationPosition') + latitude longitude + id + 87f7002b-056f-4a6a-a872-1744eea757e3 51.3251 4.25926 + 37e14a0f-5e34-4647-a062-8bfd9305fa9d 50.8038 4.30089 + +.. code-block:: python + + >>> n.get_extension('linePosition') + latitude longitude + id num + b58bf21a-096a-4dae-9a01-3f03b60c24c7 0 50.8035 4.30113 + 1 50.9169 4.34509 + 2 51.0448 4.29565 + 3 51.1570 4.38354 + ffbabc27-1ccd-4fdc-b037-e341706c8d29 0 50.8035 4.30113 + 1 50.9169 4.34509 + 2 51.0448 4.29565 + 3 51.1570 4.38354 + +When we generate a network area diagram, an automatic force layout is performed by default. +The diagram looks like this: + +.. code-block:: python + + >>> n.write_network_area_diagram('be.svg') + +.. image:: ../_static/images/nad_microgridbe_force_layout.svg + :class: forced-white-background + +Now that we have geographical positions in our data model, we can change the layout to render the diagram with +the geographical layout: + +.. code-block:: python + + >>> parameter = pp.network.NadParameters(layout_type=pp.network.NadLayoutType.GEOGRAPHICAL) + >>> n.write_network_area_diagram('be.svg', nad_parameters=parameter) + +.. image:: ../_static/images/nad_microgridbe_geo.svg + :class: forced-white-background diff --git a/java/pom.xml b/java/pom.xml index 16d66ec912..1866a5ef80 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -282,6 +282,11 @@ powsybl-cgmes-conversion runtime + + com.powsybl + powsybl-cgmes-gl + runtime + com.powsybl powsybl-config-classic diff --git a/java/src/main/java/com/powsybl/dataframe/network/extensions/LinePositionDataframeAdder.java b/java/src/main/java/com/powsybl/dataframe/network/extensions/LinePositionDataframeAdder.java new file mode 100644 index 0000000000..404ce96a62 --- /dev/null +++ b/java/src/main/java/com/powsybl/dataframe/network/extensions/LinePositionDataframeAdder.java @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2024, 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/. + */ +package com.powsybl.dataframe.network.extensions; + +import com.powsybl.commons.PowsyblException; +import com.powsybl.dataframe.SeriesMetadata; +import com.powsybl.dataframe.network.adders.AbstractSimpleAdder; +import com.powsybl.dataframe.update.DoubleSeries; +import com.powsybl.dataframe.update.IntSeries; +import com.powsybl.dataframe.update.StringSeries; +import com.powsybl.dataframe.update.UpdatingDataframe; +import com.powsybl.iidm.network.Line; +import com.powsybl.iidm.network.Network; +import com.powsybl.iidm.network.extensions.Coordinate; +import com.powsybl.iidm.network.extensions.LinePositionAdder; + +import java.util.*; + +/** + * @author Geoffroy Jamgotchian + */ +public class LinePositionDataframeAdder extends AbstractSimpleAdder { + + private static final List METADATA = List.of( + SeriesMetadata.stringIndex("id"), + SeriesMetadata.intIndex("num"), + SeriesMetadata.doubles("latitude"), + SeriesMetadata.doubles("longitude") + ); + + @Override + public List> getMetadata() { + return Collections.singletonList(METADATA); + } + + private static Map> readCoordinates(UpdatingDataframe dataframe) { + StringSeries idCol = dataframe.getStrings("id"); + IntSeries numCol = dataframe.getInts("num"); + DoubleSeries latitudeCol = dataframe.getDoubles("latitude"); + DoubleSeries longitudeCol = dataframe.getDoubles("longitude"); + Map> coordinatesByLineId = new HashMap<>(); + if (numCol != null && latitudeCol != null && longitudeCol != null) { + for (int row = 0; row < dataframe.getRowCount(); row++) { + String id = idCol.get(row); + int num = numCol.get(row); + double latitude = latitudeCol.get(row); + double longitude = longitudeCol.get(row); + List coordinates = coordinatesByLineId.computeIfAbsent(id, k -> new ArrayList<>()); + // ensure list can store coordinate at num index + if (num > coordinates.size()) { + for (int i = 0; i < num - coordinates.size(); i++) { + coordinates.add(null); + } + } + coordinates.set(num, new Coordinate(latitude, longitude)); + } + } + return coordinatesByLineId; + } + + @Override + public void addElements(Network network, UpdatingDataframe dataframe) { + Map> coordinatesByLineId = readCoordinates(dataframe); + for (var e : coordinatesByLineId.entrySet()) { + String id = e.getKey(); + List coordinates = e.getValue(); + Line l = network.getLine(id); + if (l == null) { + throw new PowsyblException("Line '" + id + "' not found"); + } + // check there is no hole in the coordinate list + for (int num = 0; num < coordinates.size(); num++) { + Coordinate coordinate = coordinates.get(num); + if (coordinate == null) { + throw new PowsyblException("Missing coordinate at " + num + " for line '" + id + "'"); + } + } + l.newExtension(LinePositionAdder.class) + .withCoordinates(coordinates) + .add(); + } + } +} diff --git a/java/src/main/java/com/powsybl/dataframe/network/extensions/LinePositionDataframeProvider.java b/java/src/main/java/com/powsybl/dataframe/network/extensions/LinePositionDataframeProvider.java new file mode 100644 index 0000000000..6e0f65c03f --- /dev/null +++ b/java/src/main/java/com/powsybl/dataframe/network/extensions/LinePositionDataframeProvider.java @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2024, 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/. + */ +package com.powsybl.dataframe.network.extensions; + +import com.google.auto.service.AutoService; +import com.powsybl.commons.PowsyblException; +import com.powsybl.dataframe.BaseDataframeMapperBuilder; +import com.powsybl.dataframe.network.ExtensionInformation; +import com.powsybl.dataframe.network.NetworkDataframeMapper; +import com.powsybl.dataframe.network.NetworkDataframeMapperBuilder; +import com.powsybl.dataframe.network.adders.NetworkElementAdder; +import com.powsybl.dataframe.update.UpdatingDataframe; +import com.powsybl.iidm.network.Identifiable; +import com.powsybl.iidm.network.Line; +import com.powsybl.iidm.network.Network; +import com.powsybl.iidm.network.extensions.Coordinate; +import com.powsybl.iidm.network.extensions.LinePosition; + +import java.util.List; +import java.util.Objects; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +/** + * @author Geoffroy Jamgotchian + */ +@AutoService(NetworkExtensionDataframeProvider.class) +public class LinePositionDataframeProvider extends AbstractSingleDataframeNetworkExtension { + + @Override + public String getExtensionName() { + return LinePosition.NAME; + } + + @Override + public ExtensionInformation getExtensionInformation() { + return new ExtensionInformation(LinePosition.NAME, + "Provides information about the line geographical coordinates", + "index : id (str), num (int), latitude (float), longitude (float)"); + } + + record LineCoordinate(LinePosition linePosition, Integer num) { + } + + private Stream itemsStream(Network network) { + return network.getLineStream() + .map(g -> (LinePosition) g.getExtension(LinePosition.class)) + .filter(Objects::nonNull) + .flatMap(lp -> IntStream.range(0, lp.getCoordinates().size() - 1).mapToObj(num -> new LineCoordinate(lp, num))); + } + + private static class LineCoordinateGetter implements BaseDataframeMapperBuilder.ItemGetter { + + @Override + public LineCoordinate getItem(Network network, UpdatingDataframe updatingDataframe, int row) { + String id = updatingDataframe.getStringValue("id", row).orElseThrow(); + Line l = network.getLine(id); + if (l == null) { + throw new PowsyblException("Line '" + id + "' not found"); + } + LinePosition lp = l.getExtension(LinePosition.class); + if (lp == null) { + throw new PowsyblException("Line '" + id + "' has no LinePosition extension"); + } + int num = updatingDataframe.getIntValue("id", row).orElseThrow(); + return new LineCoordinate(lp, num); + } + } + + @Override + public NetworkDataframeMapper createMapper() { + return NetworkDataframeMapperBuilder.ofStream(this::itemsStream, new LineCoordinateGetter()) + .stringsIndex("id", lc -> ((Identifiable) lc.linePosition().getExtendable()).getId()) + .intsIndex("num", lc -> lc.num) + .doubles("latitude", lc -> ((Coordinate) lc.linePosition.getCoordinates().get(lc.num)).getLatitude()) + .doubles("longitude", lc -> ((Coordinate) lc.linePosition.getCoordinates().get(lc.num)).getLongitude()) + .build(); + } + + @Override + public void removeExtensions(Network network, List ids) { + ids.stream().filter(Objects::nonNull) + .map(network::getIdentifiable) + .filter(Objects::nonNull) + .forEach(g -> g.removeExtension(LinePosition.class)); + } + + @Override + public NetworkElementAdder createAdder() { + return new LinePositionDataframeAdder(); + } +} diff --git a/java/src/main/java/com/powsybl/dataframe/network/extensions/SubstationPositionDataframeAdder.java b/java/src/main/java/com/powsybl/dataframe/network/extensions/SubstationPositionDataframeAdder.java new file mode 100644 index 0000000000..77ae083f26 --- /dev/null +++ b/java/src/main/java/com/powsybl/dataframe/network/extensions/SubstationPositionDataframeAdder.java @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2024, 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/. + */ +package com.powsybl.dataframe.network.extensions; + +import com.powsybl.commons.PowsyblException; +import com.powsybl.dataframe.SeriesMetadata; +import com.powsybl.dataframe.network.adders.AbstractSimpleAdder; +import com.powsybl.dataframe.update.DoubleSeries; +import com.powsybl.dataframe.update.StringSeries; +import com.powsybl.dataframe.update.UpdatingDataframe; +import com.powsybl.iidm.network.Network; +import com.powsybl.iidm.network.Substation; +import com.powsybl.iidm.network.extensions.Coordinate; +import com.powsybl.iidm.network.extensions.SubstationPositionAdder; + +import java.util.Collections; +import java.util.List; + +/** + * @author Geoffroy Jamgotchian + */ +public class SubstationPositionDataframeAdder extends AbstractSimpleAdder { + + private static final List METADATA = List.of( + SeriesMetadata.stringIndex("id"), + SeriesMetadata.doubles("latitude"), + SeriesMetadata.doubles("longitude") + ); + + @Override + public List> getMetadata() { + return Collections.singletonList(METADATA); + } + + private static class SubstationPositionSeries { + + private final StringSeries id; + private final DoubleSeries latitude; + private final DoubleSeries longitude; + + SubstationPositionSeries(UpdatingDataframe dataframe) { + this.id = dataframe.getStrings("id"); + this.latitude = dataframe.getDoubles("latitude"); + this.longitude = dataframe.getDoubles("longitude"); + } + + void create(Network network, int row) { + String id = this.id.get(row); + Substation s = network.getSubstation(id); + if (s == null) { + throw new PowsyblException("Substation '" + id + "' not found"); + } + var adder = s.newExtension(SubstationPositionAdder.class); + if (latitude != null && longitude != null) { + adder.withCoordinate(new Coordinate(latitude.get(row), longitude.get(row))); + } + adder.add(); + } + } + + @Override + public void addElements(Network network, UpdatingDataframe dataframe) { + SubstationPositionSeries series = new SubstationPositionSeries(dataframe); + for (int row = 0; row < dataframe.getRowCount(); row++) { + series.create(network, row); + } + } +} diff --git a/java/src/main/java/com/powsybl/dataframe/network/extensions/SubstationPositionDataframeProvider.java b/java/src/main/java/com/powsybl/dataframe/network/extensions/SubstationPositionDataframeProvider.java new file mode 100644 index 0000000000..6acaac8ced --- /dev/null +++ b/java/src/main/java/com/powsybl/dataframe/network/extensions/SubstationPositionDataframeProvider.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2024, 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/. + */ +package com.powsybl.dataframe.network.extensions; + +import com.google.auto.service.AutoService; +import com.powsybl.commons.PowsyblException; +import com.powsybl.dataframe.network.ExtensionInformation; +import com.powsybl.dataframe.network.NetworkDataframeMapper; +import com.powsybl.dataframe.network.NetworkDataframeMapperBuilder; +import com.powsybl.dataframe.network.adders.NetworkElementAdder; +import com.powsybl.iidm.network.Network; +import com.powsybl.iidm.network.Substation; +import com.powsybl.iidm.network.extensions.SubstationPosition; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +/** + * @author Geoffroy Jamgotchian + */ +@AutoService(NetworkExtensionDataframeProvider.class) +public class SubstationPositionDataframeProvider extends AbstractSingleDataframeNetworkExtension { + + @Override + public String getExtensionName() { + return SubstationPosition.NAME; + } + + @Override + public ExtensionInformation getExtensionInformation() { + return new ExtensionInformation(SubstationPosition.NAME, + "Provides information about the substation geographical coordinate", + "index : id (str), latitude (float), longitude (float)"); + } + + private Stream itemsStream(Network network) { + return network.getSubstationStream() + .map(g -> (SubstationPosition) g.getExtension(SubstationPosition.class)) + .filter(Objects::nonNull); + } + + private SubstationPosition getOrThrow(Network network, String id) { + Substation s = network.getSubstation(id); + if (s == null) { + throw new PowsyblException("Substation '" + id + "' not found"); + } + SubstationPosition sp = s.getExtension(SubstationPosition.class); + if (sp == null) { + throw new PowsyblException("Substation '" + id + "' has no SubstationPosition extension"); + } + return sp; + } + + @Override + public NetworkDataframeMapper createMapper() { + return NetworkDataframeMapperBuilder.ofStream(this::itemsStream, this::getOrThrow) + .stringsIndex("id", ext -> ext.getExtendable().getId()) + .doubles("latitude", s -> s.getCoordinate().getLatitude()) + .doubles("longitude", s -> s.getCoordinate().getLongitude()) + .build(); + } + + @Override + public void removeExtensions(Network network, List ids) { + ids.stream().filter(Objects::nonNull) + .map(network::getSubstation) + .filter(Objects::nonNull) + .forEach(g -> g.removeExtension(SubstationPosition.class)); + } + + @Override + public NetworkElementAdder createAdder() { + return new SubstationPositionDataframeAdder(); + } + +} diff --git a/java/src/main/java/com/powsybl/python/commons/PyPowsyblApiHeader.java b/java/src/main/java/com/powsybl/python/commons/PyPowsyblApiHeader.java index 4b534e44c8..66933b968d 100644 --- a/java/src/main/java/com/powsybl/python/commons/PyPowsyblApiHeader.java +++ b/java/src/main/java/com/powsybl/python/commons/PyPowsyblApiHeader.java @@ -1117,6 +1117,24 @@ public interface NadParametersPointer extends PointerBase { @CField("substation_description_displayed") boolean isSubstationDescriptionDisplayed(); + + @CField("layout_type") + void setLayoutType(int layoutType); + + @CField("layout_type") + int getLayoutType(); + + @CField("scaling_factor") + int getScalingFactor(); + + @CField("scaling_factor") + void setScalingFactor(int scalingFactor); + + @CField("radius_factor") + double getRadiusFactor(); + + @CField("radius_factor") + void setRadiusFactor(double radiusFactor); } @CEnum("DynamicMappingType") diff --git a/java/src/main/java/com/powsybl/python/network/NetworkCFunctions.java b/java/src/main/java/com/powsybl/python/network/NetworkCFunctions.java index 73de78da96..d328961325 100644 --- a/java/src/main/java/com/powsybl/python/network/NetworkCFunctions.java +++ b/java/src/main/java/com/powsybl/python/network/NetworkCFunctions.java @@ -30,6 +30,9 @@ import com.powsybl.iidm.network.*; import com.powsybl.iidm.reducer.*; import com.powsybl.nad.NadParameters; +import com.powsybl.nad.layout.BasicForceLayoutFactory; +import com.powsybl.nad.layout.GeographicalLayoutFactory; +import com.powsybl.nad.layout.LayoutFactory; import com.powsybl.python.commons.CTypeUtil; import com.powsybl.python.commons.Directives; import com.powsybl.python.commons.PyPowsyblApiHeader; @@ -943,8 +946,13 @@ public static SldParameters convertSldParameters(SldParametersPointer sldParamet return sldParameters; } - public static NadParameters convertNadParameters(NadParametersPointer nadParametersPointer) { + public static NadParameters convertNadParameters(NadParametersPointer nadParametersPointer, Network network) { NadParameters nadParameters = NetworkAreaDiagramUtil.createNadParameters(); + LayoutFactory layoutFactory = switch (nadParametersPointer.getLayoutType()) { + case 1: yield new GeographicalLayoutFactory(network, nadParametersPointer.getScalingFactor(), nadParametersPointer.getRadiusFactor(), new BasicForceLayoutFactory()); + default: yield new BasicForceLayoutFactory(); + }; + nadParameters.setLayoutFactory(layoutFactory); nadParameters.getSvgParameters() .setEdgeNameDisplayed(nadParametersPointer.isEdgeNameDisplayed()) .setEdgeInfoAlongEdge(nadParametersPointer.isEdgeInfoAlongEdge()) @@ -1025,7 +1033,7 @@ public static void writeNetworkAreaDiagramSvg(IsolateThread thread, ObjectHandle Network network = ObjectHandles.getGlobal().get(networkHandle); String svgFileStr = CTypeUtil.toString(svgFile); List voltageLevelIds = toStringList(voltageLevelIdsPointer, voltageLevelIdCount); - NadParameters nadParameters = convertNadParameters(nadParametersPointer); + NadParameters nadParameters = convertNadParameters(nadParametersPointer, network); NetworkAreaDiagramUtil.writeSvg(network, voltageLevelIds, depth, svgFileStr, highNominalVoltageBound, lowNominalVoltageBound, nadParameters); }); } @@ -1037,7 +1045,7 @@ public static CCharPointer getNetworkAreaDiagramSvg(IsolateThread thread, Object return doCatch(exceptionHandlerPtr, () -> { Network network = ObjectHandles.getGlobal().get(networkHandle); List voltageLevelIds = toStringList(voltageLevelIdsPointer, voltageLevelIdCount); - NadParameters nadParameters = convertNadParameters(nadParametersPointer); + NadParameters nadParameters = convertNadParameters(nadParametersPointer, network); String svg = NetworkAreaDiagramUtil.getSvg(network, voltageLevelIds, depth, highNominalVoltageBound, lowNominalVoltageBound, nadParameters); return CTypeUtil.toCharPtr(svg); }); diff --git a/java/src/main/resources/META-INF/native-image/com.powsybl/powsybl-cgmes-gl/resource-config.json b/java/src/main/resources/META-INF/native-image/com.powsybl/powsybl-cgmes-gl/resource-config.json new file mode 100644 index 0000000000..60d6bda345 --- /dev/null +++ b/java/src/main/resources/META-INF/native-image/com.powsybl/powsybl-cgmes-gl/resource-config.json @@ -0,0 +1,6 @@ +{ + "resources":{ + "includes":[ + {"pattern":"\\QCGMES-GL.sparql\\E"} + ]} +} diff --git a/java/src/main/resources/META-INF/native-image/com.powsybl/powsybl-iidm-api/resource-config.json b/java/src/main/resources/META-INF/native-image/com.powsybl/powsybl-iidm-api/resource-config.json index ae9916b917..2fb37a1b41 100644 --- a/java/src/main/resources/META-INF/native-image/com.powsybl/powsybl-iidm-api/resource-config.json +++ b/java/src/main/resources/META-INF/native-image/com.powsybl/powsybl-iidm-api/resource-config.json @@ -1,6 +1,7 @@ { "resources":{ "includes":[ - {"pattern":"\\QMETA-INF/services/com.powsybl.iidm.network.NetworkFactoryService\\E"} + {"pattern":"\\QMETA-INF/services/com.powsybl.iidm.network.NetworkFactoryService\\E"}, + {"pattern":"\\QMETA-INF/services/com.powsybl.iidm.network.Importer\\E"} ]} } diff --git a/pypowsybl/_pypowsybl.pyi b/pypowsybl/_pypowsybl.pyi index 34033cb612..adbac56c20 100644 --- a/pypowsybl/_pypowsybl.pyi +++ b/pypowsybl/_pypowsybl.pyi @@ -250,6 +250,21 @@ class SldParameters: component_library: str def __init__(self) -> None: ... +class NadLayoutType: + __members__: ClassVar[Dict[str, NadLayoutType]] = ... # read-only + FORCE_LAYOUT: ClassVar[NadLayoutType] = ... + GEOGRAPHICAL: ClassVar[NadLayoutType] = ... + def __init__(self, arg0: int) -> None: ... + def __eq__(self, arg0: object) -> bool: ... + def __getstate__(self) -> int: ... + def __hash__(self) -> int: ... + def __index__(self) -> int: ... + def __int__(self) -> int: ... + def __ne__(self, arg0: object) -> bool: ... + def __setstate__(self, arg0: int) -> None: ... + @property + def name(self) -> str: ... + class NadParameters: edge_name_displayed: bool edge_info_along_edge: bool @@ -260,6 +275,9 @@ class NadParameters: voltage_value_precision: int substation_description_displayed: bool bus_legend: bool + layout_type: NadLayoutType + scaling_factor: int + radius_factor: float def __init__(self) -> None: ... class LimitType: diff --git a/pypowsybl/network/__init__.py b/pypowsybl/network/__init__.py index e0853d7078..df1043d90a 100644 --- a/pypowsybl/network/__init__.py +++ b/pypowsybl/network/__init__.py @@ -14,6 +14,7 @@ from .impl.bus_breaker_topology import BusBreakerTopology from .impl.node_breaker_topology import NodeBreakerTopology from .impl.sld_parameters import SldParameters +from .impl.nad_parameters import NadLayoutType from .impl.nad_parameters import NadParameters from .impl.layout_parameters import LayoutParameters from .impl.network_creation_util import ( diff --git a/pypowsybl/network/impl/nad_parameters.py b/pypowsybl/network/impl/nad_parameters.py index aa04c7d976..d56671e9db 100644 --- a/pypowsybl/network/impl/nad_parameters.py +++ b/pypowsybl/network/impl/nad_parameters.py @@ -4,7 +4,9 @@ # SPDX-License-Identifier: MPL-2.0 # import pypowsybl._pypowsybl as _pp - +from pypowsybl._pypowsybl import ( + NadLayoutType +) class NadParameters: """ @@ -13,7 +15,8 @@ class NadParameters: def __init__(self, edge_name_displayed: bool = False, id_displayed: bool = False, edge_info_along_edge: bool = True, power_value_precision: int = 0, angle_value_precision: int = 1, current_value_precision: int = 0, voltage_value_precision: int = 1, bus_legend: bool = True, - substation_description_displayed: bool = False): + substation_description_displayed: bool = False, layout_type: NadLayoutType = NadLayoutType.FORCE_LAYOUT, + scaling_factor: int = 150000, radius_factor: float = 150.0): self._edge_name_displayed = edge_name_displayed self._edge_info_along_edge = edge_info_along_edge self._id_displayed = id_displayed @@ -23,6 +26,9 @@ def __init__(self, edge_name_displayed: bool = False, id_displayed: bool = False self._voltage_value_precision = voltage_value_precision self._bus_legend = bus_legend self._substation_description_displayed = substation_description_displayed + self._layout_type = layout_type + self._scaling_factor = scaling_factor + self._radius_factor = radius_factor @property def edge_name_displayed(self) -> bool: @@ -32,7 +38,7 @@ def edge_name_displayed(self) -> bool: @property def edge_info_along_edge(self) -> bool: """edge_info_along_edge""" - return self.edge_info_along_edge + return self._edge_info_along_edge @property def id_displayed(self) -> bool: @@ -69,6 +75,21 @@ def substation_description_displayed(self) -> int: """substation_description_displayed""" return self._substation_description_displayed + @property + def layout_type(self) -> NadLayoutType: + """layout_type""" + return self._layout_type + + @property + def scaling_factor(self) -> int: + """scaling_factor""" + return self._scaling_factor + + @property + def radius_factor(self) -> float: + """radius_factor""" + return self._radius_factor + def _to_c_parameters(self) -> _pp.NadParameters: c_parameters = _pp.NadParameters() c_parameters.edge_name_displayed = self._edge_name_displayed @@ -80,4 +101,23 @@ def _to_c_parameters(self) -> _pp.NadParameters: c_parameters.voltage_value_precision = self._voltage_value_precision c_parameters.bus_legend = self._bus_legend c_parameters.substation_description_displayed = self._substation_description_displayed + c_parameters.layout_type = self._layout_type + c_parameters.scaling_factor = self._scaling_factor + c_parameters.radius_factor = self._radius_factor return c_parameters + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(" \ + f"edge_name_displayed={self._edge_name_displayed}" \ + f", edge_info_along_edge={self._edge_info_along_edge}" \ + f", id_displayed={self._id_displayed}" \ + f", power_value_precision={self._power_value_precision}" \ + f", angle_value_precision={self._angle_value_precision}" \ + f", current_value_precision={self._current_value_precision}" \ + f", voltage_value_precision={self._voltage_value_precision}" \ + f", bus_legend={self._bus_legend}" \ + f", substation_description_displayed={self._substation_description_displayed}" \ + f", layout_type={self._layout_type}" \ + f", scaling_factor={self._scaling_factor}" \ + f", radius_factor={self._radius_factor}" \ + f")" diff --git a/tests/test_network.py b/tests/test_network.py index e5db820231..6e9e2b7dfa 100644 --- a/tests/test_network.py +++ b/tests/test_network.py @@ -26,7 +26,7 @@ import pypowsybl.report as rp import util from pypowsybl import PyPowsyblError -from pypowsybl.network import ValidationLevel, SldParameters, NadParameters, LayoutParameters +from pypowsybl.network import ValidationLevel, SldParameters, NadLayoutType, NadParameters, LayoutParameters TEST_DIR = pathlib.Path(__file__).parent DATA_DIR = TEST_DIR.parent / 'data' @@ -1939,5 +1939,35 @@ def test_terminals(): assert "No enum constant" in str(e) +def test_nad_parameters(): + nad_parameters = NadParameters() + assert not nad_parameters.edge_name_displayed + assert nad_parameters.edge_info_along_edge + assert not nad_parameters.id_displayed + assert nad_parameters.power_value_precision == 0 + assert nad_parameters.angle_value_precision == 1 + assert nad_parameters.current_value_precision == 0 + assert nad_parameters.voltage_value_precision == 1 + assert nad_parameters.bus_legend + assert not nad_parameters.substation_description_displayed + assert nad_parameters.layout_type == NadLayoutType.FORCE_LAYOUT + assert nad_parameters.scaling_factor == 150000 + assert nad_parameters.radius_factor == 150.0 + + nad_parameters = NadParameters(True, True, False, 1, 2, 1, 2, False, True, NadLayoutType.GEOGRAPHICAL, 100000, 120.0) + assert nad_parameters.edge_name_displayed + assert not nad_parameters.edge_info_along_edge + assert nad_parameters.id_displayed + assert nad_parameters.power_value_precision == 1 + assert nad_parameters.angle_value_precision == 2 + assert nad_parameters.current_value_precision == 1 + assert nad_parameters.voltage_value_precision == 2 + assert not nad_parameters.bus_legend + assert nad_parameters.substation_description_displayed + assert nad_parameters.layout_type == NadLayoutType.GEOGRAPHICAL + assert nad_parameters.scaling_factor == 100000 + assert nad_parameters.radius_factor == 120.0 + + if __name__ == '__main__': unittest.main() diff --git a/tests/test_network_extensions.py b/tests/test_network_extensions.py index 08238fd880..2860bbb5e3 100644 --- a/tests/test_network_extensions.py +++ b/tests/test_network_extensions.py @@ -429,6 +429,29 @@ def test_secondary_voltage_control(): e2 = n.get_extensions(extension_name, "units").loc['GEN'] assert e2.participate == False + +def test_geo_data(): + n = pn.load(str(DATA_DIR.joinpath('MicroGridTestConfiguration_T4_BE_BB_Complete_v2.zip')), {'iidm.import.cgmes.post-processors': 'cgmesGLImport'}) + substation_expected = pd.DataFrame.from_records(index='id', + data=[{'id': '87f7002b-056f-4a6a-a872-1744eea757e3', 'latitude': 51.3251, 'longitude': 4.25926}, + {'id': '37e14a0f-5e34-4647-a062-8bfd9305fa9d', 'latitude': 50.8038, 'longitude': 4.30089}]) + pd.testing.assert_frame_equal(n.get_extensions('substationPosition'), substation_expected) + line_expected = pd.DataFrame.from_records(data=[{'id': 'b58bf21a-096a-4dae-9a01-3f03b60c24c7', 'num': 0, 'latitude': 50.8035, 'longitude': 4.30113}, + {'id': 'b58bf21a-096a-4dae-9a01-3f03b60c24c7', 'num': 1, 'latitude': 50.9169, 'longitude': 4.34509}, + {'id': 'b58bf21a-096a-4dae-9a01-3f03b60c24c7', 'num': 2, 'latitude': 51.0448, 'longitude': 4.29565}, + {'id': 'b58bf21a-096a-4dae-9a01-3f03b60c24c7', 'num': 3, 'latitude': 51.1570, 'longitude': 4.38354}, + {'id': 'ffbabc27-1ccd-4fdc-b037-e341706c8d29', 'num': 0, 'latitude': 50.8035, 'longitude': 4.30113}, + {'id': 'ffbabc27-1ccd-4fdc-b037-e341706c8d29', 'num': 1, 'latitude': 50.9169, 'longitude': 4.34509}, + {'id': 'ffbabc27-1ccd-4fdc-b037-e341706c8d29', 'num': 2, 'latitude': 51.0448, 'longitude': 4.29565}, + {'id': 'ffbabc27-1ccd-4fdc-b037-e341706c8d29', 'num': 3, 'latitude': 51.1570, 'longitude': 4.38354}]) + # force num column dtype to be int32 like the one generated by pypowsybl and not int64 + # as a consequence index has to be created after + line_expected = line_expected.astype({'id': str, 'num': np.int32, 'latitude': np.float64, 'longitude': np.float64}) + line_expected.set_index(['id', 'num'], inplace=True) + + pd.testing.assert_frame_equal(n.get_extensions('linePosition'), line_expected) + + def test_get_extensions_information(): extensions_information = pypowsybl.network.get_extensions_information() assert extensions_information.loc['measurements']['detail'] == 'Provides measurement about a specific equipment' @@ -467,4 +490,6 @@ def test_get_extensions_information(): assert extensions_information.loc['busbarSectionPosition']['detail'] == 'Position information about the BusbarSection' assert extensions_information.loc['busbarSectionPosition']['attributes'] == 'index : id (str), busbar_index (int), section_index (int)' assert extensions_information.loc['secondaryVoltageControl']['detail'] == 'Provides information about the secondary voltage control zones and units, in two distinct dataframes.' - assert extensions_information.loc['secondaryVoltageControl']['attributes'] == '[dataframe "zones"] index : name (str), target_v (float), bus_ids (str) / [dataframe "units"] index : unit_id (str), participate (bool), zone_name (str)' \ No newline at end of file + assert extensions_information.loc['secondaryVoltageControl']['attributes'] == '[dataframe "zones"] index : name (str), target_v (float), bus_ids (str) / [dataframe "units"] index : unit_id (str), participate (bool), zone_name (str)' + assert extensions_information.loc['substationPosition']['attributes'] == 'index : id (str), latitude (float), longitude (float)' + assert extensions_information.loc['linePosition']['attributes'] == 'index : id (str), num (int), latitude (float), longitude (float)' \ No newline at end of file