diff --git a/doc/Changelog.md b/doc/Changelog.md index 20db0f9f..5e921f97 100644 --- a/doc/Changelog.md +++ b/doc/Changelog.md @@ -4,6 +4,7 @@ **In development** +* [PR112](https://github.com/RoseauTechnologies/Roseau_Load_Flow/pull/112) Make the geometry serialization optional. * [PR106](https://github.com/RoseauTechnologies/Roseau_Load_Flow/pull/106) Improvements for non-euclidean projections. * [PR104](https://github.com/RoseauTechnologies/Roseau_Load_Flow/pull/104) Remove `roseau.load_flow.utils.BranchType` * [GH99](https://github.com/RoseauTechnologies/Roseau_Load_Flow/issues/99) Add `Line.res_series_currents` diff --git a/roseau/load_flow/io/dict.py b/roseau/load_flow/io/dict.py index f713f0ef..e1b481d3 100644 --- a/roseau/load_flow/io/dict.py +++ b/roseau/load_flow/io/dict.py @@ -132,13 +132,16 @@ def network_from_dict( return buses, branches_dict, loads, sources, grounds, potential_refs -def network_to_dict(en: "ElectricalNetwork") -> JsonDict: +def network_to_dict(en: "ElectricalNetwork", include_geometry: bool) -> JsonDict: """Return a dictionary of the current network data. Args: en: The electrical network. + include_geometry: + If False, the geometry will not be added to the network dictionary. + Returns: The created dictionary. """ @@ -152,7 +155,7 @@ def network_to_dict(en: "ElectricalNetwork") -> JsonDict: sources: list[JsonDict] = [] short_circuits: list[JsonDict] = [] for bus in en.buses.values(): - buses.append(bus.to_dict()) + buses.append(bus.to_dict(include_geometry=include_geometry)) for element in bus._connected_elements: if isinstance(element, AbstractLoad): assert element.bus is bus @@ -168,7 +171,7 @@ def network_to_dict(en: "ElectricalNetwork") -> JsonDict: lines_params_dict: dict[Id, LineParameters] = {} transformers_params_dict: dict[Id, TransformerParameters] = {} for branch in en.branches.values(): - branches.append(branch.to_dict()) + branches.append(branch.to_dict(include_geometry=include_geometry)) if isinstance(branch, Line): params_id = branch.parameters.id if params_id in lines_params_dict and branch.parameters != lines_params_dict[params_id]: diff --git a/roseau/load_flow/io/tests/test_dict.py b/roseau/load_flow/io/tests/test_dict.py index d4a1725c..cc9a1057 100644 --- a/roseau/load_flow/io/tests/test_dict.py +++ b/roseau/load_flow/io/tests/test_dict.py @@ -1,6 +1,6 @@ import numpy as np import pytest -from shapely import Point +from shapely import LineString, Point from roseau.load_flow import Line from roseau.load_flow.exceptions import RoseauLoadFlowException, RoseauLoadFlowExceptionCode @@ -23,8 +23,8 @@ def test_to_dict(): ground = Ground("ground") vn = 400 / np.sqrt(3) voltages = [vn, vn * np.exp(-2 / 3 * np.pi * 1j), vn * np.exp(2 / 3 * np.pi * 1j)] - source_bus = Bus(id="source", phases="abcn") - load_bus = Bus(id="load bus", phases="abcn") + source_bus = Bus(id="source", phases="abcn", geometry=Point(0.0, 0.0)) + load_bus = Bus(id="load bus", phases="abcn", geometry=Point(0.0, 1.0)) ground.connect(load_bus) p_ref = PotentialRef("pref", element=ground) vs = VoltageSource("vs", source_bus, phases="abcn", voltages=voltages) @@ -33,8 +33,9 @@ def test_to_dict(): lp1 = LineParameters("test", z_line=np.eye(4, dtype=complex), y_shunt=np.eye(4, dtype=complex)) lp2 = LineParameters("test", z_line=np.eye(4, dtype=complex), y_shunt=np.eye(4, dtype=complex) * 1.1) - line1 = Line("line1", source_bus, load_bus, phases="abcn", ground=ground, parameters=lp1, length=10) - line2 = Line("line2", source_bus, load_bus, phases="abcn", ground=ground, parameters=lp2, length=10) + geom = LineString([(0.0, 0.0), (0.0, 1.0)]) + line1 = Line("line1", source_bus, load_bus, phases="abcn", ground=ground, parameters=lp1, length=10, geometry=geom) + line2 = Line("line2", source_bus, load_bus, phases="abcn", ground=ground, parameters=lp2, length=10, geometry=geom) en = ElectricalNetwork( buses=[source_bus, load_bus], branches=[line1, line2], @@ -51,14 +52,25 @@ def test_to_dict(): # Same id, same line parameters -> ok lp2 = LineParameters("test", z_line=np.eye(4, dtype=complex), y_shunt=np.eye(4, dtype=complex)) line2.parameters = lp2 - en.to_dict() + res = en.to_dict() + assert "geometry" in res["buses"][0] + assert "geometry" in res["buses"][1] + assert "geometry" in res["branches"][0] + assert "geometry" in res["branches"][1] + + res = en.to_dict(include_geometry=False) + assert "geometry" not in res["buses"][0] + assert "geometry" not in res["buses"][1] + assert "geometry" not in res["branches"][0] + assert "geometry" not in res["branches"][1] # Same id, different transformer parameters -> fail ground = Ground("ground") vn = 400 / np.sqrt(3) voltages = [vn, vn * np.exp(-2 / 3 * np.pi * 1j), vn * np.exp(2 / 3 * np.pi * 1j)] - source_bus = Bus(id="source", phases="abcn") - load_bus = Bus(id="load bus", phases="abcn") + geom = Point(0.0, 0.0) + source_bus = Bus(id="source", phases="abcn", geometry=geom) + load_bus = Bus(id="load bus", phases="abcn", geometry=geom) ground.connect(load_bus) ground.connect(source_bus) p_ref = PotentialRef("pref", element=ground) @@ -71,8 +83,8 @@ def test_to_dict(): tp2 = TransformerParameters( "t", type="Dyn11", uhv=20000, ulv=400, sn=200 * 1e3, p0=460, i0=2.3 / 100, psc=2350, vsc=4 / 100 ) - transformer1 = Transformer(id="Transformer1", bus1=source_bus, bus2=load_bus, parameters=tp1) - transformer2 = Transformer(id="Transformer2", bus1=source_bus, bus2=load_bus, parameters=tp2) + transformer1 = Transformer(id="Transformer1", bus1=source_bus, bus2=load_bus, parameters=tp1, geometry=geom) + transformer2 = Transformer(id="Transformer2", bus1=source_bus, bus2=load_bus, parameters=tp2, geometry=geom) en = ElectricalNetwork( buses=[source_bus, load_bus], branches=[transformer1, transformer2], @@ -91,7 +103,17 @@ def test_to_dict(): "t", type="Dyn11", uhv=20000, ulv=400, sn=160 * 1e3, p0=460, i0=2.3 / 100, psc=2350, vsc=4 / 100 ) transformer2.parameters = tp2 - en.to_dict() + res = en.to_dict() + assert "geometry" in res["buses"][0] + assert "geometry" in res["buses"][1] + assert "geometry" in res["branches"][0] + assert "geometry" in res["branches"][1] + + res = en.to_dict(include_geometry=False) + assert "geometry" not in res["buses"][0] + assert "geometry" not in res["buses"][1] + assert "geometry" not in res["branches"][0] + assert "geometry" not in res["branches"][1] def test_v0_to_v1_converter(monkeypatch): diff --git a/roseau/load_flow/models/branches.py b/roseau/load_flow/models/branches.py index b83185bb..c9206f6e 100644 --- a/roseau/load_flow/models/branches.py +++ b/roseau/load_flow/models/branches.py @@ -126,7 +126,7 @@ def res_voltages(self) -> tuple[Q_[np.ndarray], Q_[np.ndarray]]: def from_dict(cls, data: JsonDict) -> Self: return cls(**data) # not used anymore - def to_dict(self) -> JsonDict: + def to_dict(self, include_geometry: bool = True) -> JsonDict: res = { "id": self.id, "type": self.branch_type, @@ -135,7 +135,7 @@ def to_dict(self) -> JsonDict: "bus1": self.bus1.id, "bus2": self.bus2.id, } - if self.geometry is not None: + if self.geometry is not None and include_geometry: res["geometry"] = self.geometry.__geo_interface__ return res diff --git a/roseau/load_flow/models/buses.py b/roseau/load_flow/models/buses.py index f022799d..5547584d 100644 --- a/roseau/load_flow/models/buses.py +++ b/roseau/load_flow/models/buses.py @@ -138,11 +138,11 @@ def from_dict(cls, data: JsonDict) -> Self: potentials = [complex(v[0], v[1]) for v in potentials] return cls(id=data["id"], phases=data["phases"], geometry=geometry, potentials=potentials) - def to_dict(self) -> JsonDict: + def to_dict(self, include_geometry: bool = True) -> JsonDict: res = {"id": self.id, "phases": self.phases} if not np.allclose(self.potentials, 0): res["potentials"] = [[v.real, v.imag] for v in self._potentials] - if self.geometry is not None: + if self.geometry is not None and include_geometry: res["geometry"] = self.geometry.__geo_interface__ return res diff --git a/roseau/load_flow/models/grounds.py b/roseau/load_flow/models/grounds.py index 627116a0..bbaf0cb1 100644 --- a/roseau/load_flow/models/grounds.py +++ b/roseau/load_flow/models/grounds.py @@ -97,7 +97,7 @@ def from_dict(cls, data: JsonDict) -> Self: self._connected_buses = data["buses"] return self - def to_dict(self) -> JsonDict: + def to_dict(self, include_geometry: bool = True) -> JsonDict: # Shunt lines and potential references will have the ground in their dict not here. return { "id": self.id, diff --git a/roseau/load_flow/models/lines/lines.py b/roseau/load_flow/models/lines/lines.py index 7adf47b3..f6862de8 100644 --- a/roseau/load_flow/models/lines/lines.py +++ b/roseau/load_flow/models/lines/lines.py @@ -358,8 +358,12 @@ def res_power_losses(self) -> Q_[np.ndarray]: # # Json Mixin interface # - def to_dict(self) -> JsonDict: - res = {**super().to_dict(), "length": self._length, "params_id": self.parameters.id} + def to_dict(self, include_geometry: bool = True) -> JsonDict: + res = { + **super().to_dict(include_geometry=include_geometry), + "length": self._length, + "params_id": self.parameters.id, + } if self.ground is not None: res["ground"] = self.ground.id return res diff --git a/roseau/load_flow/models/lines/parameters.py b/roseau/load_flow/models/lines/parameters.py index 8ded124b..b665d562 100644 --- a/roseau/load_flow/models/lines/parameters.py +++ b/roseau/load_flow/models/lines/parameters.py @@ -656,7 +656,7 @@ def from_dict(cls, data: JsonDict) -> Self: logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_LINE_MODEL) - def to_dict(self) -> JsonDict: + def to_dict(self, include_geometry: bool = True) -> JsonDict: """Return the line parameters information as a dictionary format.""" res = { "id": self.id, diff --git a/roseau/load_flow/models/loads/flexible_parameters.py b/roseau/load_flow/models/loads/flexible_parameters.py index ec429021..327f027b 100644 --- a/roseau/load_flow/models/loads/flexible_parameters.py +++ b/roseau/load_flow/models/loads/flexible_parameters.py @@ -296,7 +296,7 @@ def from_dict(cls, data: JsonDict) -> Self: logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_CONTROL_TYPE) - def to_dict(self) -> JsonDict: + def to_dict(self, include_geometry: bool = True) -> JsonDict: if self.type == "constant": return {"type": "constant"} elif self.type == "p_max_u_production": @@ -410,7 +410,7 @@ def from_dict(cls, data: JsonDict) -> Self: epsilon = data["epsilon"] if "epsilon" in data else cls.DEFAULT_EPSILON return cls(type=data["type"], alpha=alpha, epsilon=epsilon) - def to_dict(self) -> JsonDict: + def to_dict(self, include_geometry: bool = True) -> JsonDict: return {"type": self.type, "alpha": self._alpha, "epsilon": self._epsilon} def _results_to_dict(self, warning: bool) -> NoReturn: @@ -806,7 +806,7 @@ def from_dict(cls, data: JsonDict) -> Self: projection = cls._projection_class.from_dict(data["projection"]) return cls(control_p=control_p, control_q=control_q, projection=projection, s_max=data["s_max"]) - def to_dict(self) -> JsonDict: + def to_dict(self, include_geometry: bool = True) -> JsonDict: return { "control_p": self.control_p.to_dict(), "control_q": self.control_q.to_dict(), diff --git a/roseau/load_flow/models/loads/loads.py b/roseau/load_flow/models/loads/loads.py index b5b14a21..4b958496 100644 --- a/roseau/load_flow/models/loads/loads.py +++ b/roseau/load_flow/models/loads/loads.py @@ -308,7 +308,7 @@ def res_flexible_powers(self) -> Q_[np.ndarray]: # # Json Mixin interface # - def to_dict(self) -> JsonDict: + def to_dict(self, include_geometry: bool = True) -> JsonDict: if self.bus is None: msg = f"The load {self.id!r} is disconnected and cannot be used anymore." logger.error(msg) @@ -383,7 +383,7 @@ def currents(self, value: Sequence[complex]) -> None: self._currents = self._validate_value(value) self._invalidate_network_results() - def to_dict(self) -> JsonDict: + def to_dict(self, include_geometry: bool = True) -> JsonDict: if self.bus is None: msg = f"The load {self.id!r} is disconnected and cannot be used anymore." logger.error(msg) @@ -441,7 +441,7 @@ def impedances(self, impedances: Sequence[complex]) -> None: self._impedances = self._validate_value(impedances) self._invalidate_network_results() - def to_dict(self) -> JsonDict: + def to_dict(self, include_geometry: bool = True) -> JsonDict: if self.bus is None: msg = f"The load {self.id!r} is disconnected and cannot be used anymore." logger.error(msg) diff --git a/roseau/load_flow/models/potential_refs.py b/roseau/load_flow/models/potential_refs.py index ce28bd6b..deb25c3e 100644 --- a/roseau/load_flow/models/potential_refs.py +++ b/roseau/load_flow/models/potential_refs.py @@ -86,7 +86,7 @@ def res_current(self) -> Q_[complex]: def from_dict(cls, data: JsonDict) -> Self: return cls(data["id"], data["element"], phase=data.get("phases")) - def to_dict(self) -> JsonDict: + def to_dict(self, include_geometry: bool = True) -> JsonDict: res = {"id": self.id} e = self.element if isinstance(e, Bus): diff --git a/roseau/load_flow/models/sources.py b/roseau/load_flow/models/sources.py index b3ef3e90..4ee826d2 100644 --- a/roseau/load_flow/models/sources.py +++ b/roseau/load_flow/models/sources.py @@ -153,7 +153,7 @@ def from_dict(cls, data: JsonDict) -> Self: voltages = [complex(v[0], v[1]) for v in data["voltages"]] return cls(data["id"], data["bus"], voltages=voltages, phases=data["phases"]) - def to_dict(self) -> JsonDict: + def to_dict(self, include_geometry: bool = True) -> JsonDict: if self.bus is None: msg = f"The voltage source {self.id!r} is disconnected and cannot be used anymore." logger.error(msg) diff --git a/roseau/load_flow/models/transformers/parameters.py b/roseau/load_flow/models/transformers/parameters.py index 94c64c1c..fdc04d11 100644 --- a/roseau/load_flow/models/transformers/parameters.py +++ b/roseau/load_flow/models/transformers/parameters.py @@ -297,7 +297,7 @@ def from_dict(cls, data: JsonDict) -> Self: vsc=data["vsc"], ) - def to_dict(self) -> JsonDict: + def to_dict(self, include_geometry: bool = True) -> JsonDict: return { "id": self.id, "sn": self._sn, diff --git a/roseau/load_flow/models/transformers/transformers.py b/roseau/load_flow/models/transformers/transformers.py index c3adcbd7..300d8432 100644 --- a/roseau/load_flow/models/transformers/transformers.py +++ b/roseau/load_flow/models/transformers/transformers.py @@ -130,8 +130,8 @@ def parameters(self, value: TransformerParameters) -> None: self._parameters = value self._invalidate_network_results() - def to_dict(self) -> JsonDict: - return {**super().to_dict(), "params_id": self.parameters.id, "tap": self.tap} + def to_dict(self, include_geometry: bool = True) -> JsonDict: + return {**super().to_dict(include_geometry=include_geometry), "params_id": self.parameters.id, "tap": self.tap} def _compute_phases_three( self, diff --git a/roseau/load_flow/network.py b/roseau/load_flow/network.py index f7457a10..a5c03424 100644 --- a/roseau/load_flow/network.py +++ b/roseau/load_flow/network.py @@ -422,7 +422,7 @@ def solve_load_flow( # Get the data data = { - "network": self.to_dict(), + "network": self.to_dict(include_geometry=False), "solver": { "name": solver, "params": solver_params, @@ -1103,9 +1103,14 @@ def from_dict(cls, data: JsonDict) -> Self: potential_refs=p_refs, ) - def to_dict(self) -> JsonDict: - """Convert the electrical network to a dictionary.""" - return network_to_dict(self) + def to_dict(self, include_geometry: bool = True) -> JsonDict: + """Convert the electrical network to a dictionary. + + Args: + include_geometry: + If False, the geometry will not be added to the network dictionary. + """ + return network_to_dict(self, include_geometry=include_geometry) # # Results saving/loading diff --git a/roseau/load_flow/utils/mixins.py b/roseau/load_flow/utils/mixins.py index 7b0119d7..e85aed7c 100644 --- a/roseau/load_flow/utils/mixins.py +++ b/roseau/load_flow/utils/mixins.py @@ -50,8 +50,13 @@ def from_json(cls, path: StrPath) -> Self: return cls.from_dict(data=data) @abstractmethod - def to_dict(self) -> JsonDict: - """Return the element information as a dictionary format.""" + def to_dict(self, include_geometry: bool = True) -> JsonDict: + """Return the element information as a dictionary format. + + Args: + include_geometry: + If False, the geometry will not be added to the result dictionary. + """ raise NotImplementedError def to_json(self, path: StrPath) -> Path: