diff --git a/roseau/load_flow_single/_constants.py b/roseau/load_flow_single/_constants.py new file mode 100644 index 0000000..f06d9c7 --- /dev/null +++ b/roseau/load_flow_single/_constants.py @@ -0,0 +1,4 @@ +import math +from typing import Final + +SQRT3: Final = math.sqrt(3) diff --git a/roseau/load_flow_single/models/branches.py b/roseau/load_flow_single/models/branches.py index 1ea3633..3e17cb3 100644 --- a/roseau/load_flow_single/models/branches.py +++ b/roseau/load_flow_single/models/branches.py @@ -1,11 +1,11 @@ import logging -import numpy as np from shapely.geometry.base import BaseGeometry from typing_extensions import Self from roseau.load_flow.typing import Complex, Id, JsonDict from roseau.load_flow.units import Q_, ureg_wraps +from roseau.load_flow_single._constants import SQRT3 from roseau.load_flow_single.models.buses import Bus from roseau.load_flow_single.models.core import Element @@ -75,17 +75,17 @@ def res_currents(self) -> tuple[Q_[Complex], Q_[Complex]]: def _res_powers_getter( self, warning: bool, - potential1: Complex | None = None, - potential2: Complex | None = None, + voltage1: Complex | None = None, + voltage2: Complex | None = None, current1: Complex | None = None, current2: Complex | None = None, ) -> tuple[Complex, Complex]: if current1 is None or current2 is None: current1, current2 = self._res_currents_getter(warning) - if potential1 is None or potential2 is None: - potential1, potential2 = self._res_potentials_getter(warning=False) # we warn on the previous line - power1 = potential1 * current1.conjugate() * 3.0 - power2 = potential2 * current2.conjugate() * 3.0 + if voltage1 is None or voltage2 is None: + voltage1, voltage2 = self._res_voltages_getter(warning=False) # we warn on the previous line + power1 = voltage1 * current1.conjugate() * SQRT3 + power2 = voltage2 * current2.conjugate() * SQRT3 return power1, power2 @property @@ -94,17 +94,10 @@ def res_powers(self) -> tuple[Q_[Complex], Q_[Complex]]: """The load flow result of the branch powers (VA).""" return self._res_powers_getter(warning=True) - def _res_potentials_getter(self, warning: bool) -> tuple[Complex, Complex]: - pot1 = self.bus1._res_potential_getter(warning=warning) - pot2 = self.bus2._res_potential_getter(warning=False) # we warn on the previous line - return pot1, pot2 - - def _res_voltages_getter( - self, warning: bool, potential1: Complex | None = None, potential2: Complex | None = None - ) -> tuple[Complex, Complex]: - if potential1 is None or potential2 is None: - potential1, potential2 = self._res_potentials_getter(warning) - return potential1 * np.sqrt(3.0), potential2 * np.sqrt(3.0) + def _res_voltages_getter(self, warning: bool) -> tuple[Complex, Complex]: + voltage1 = self.bus1._res_voltage_getter(warning=warning) + voltage2 = self.bus2._res_voltage_getter(warning=False) # we warn on the previous line + return voltage1, voltage2 @property @ureg_wraps(("V", "V"), (None,)) @@ -114,12 +107,8 @@ def res_voltages(self) -> tuple[Q_[Complex], Q_[Complex]]: def _cy_connect(self) -> None: """Connect the Cython elements of the buses and the branch""" - assert isinstance(self.bus1, Bus) for i in range(self._n): self._cy_element.connect(self.bus1._cy_element, [(i, i)], True) - - assert isinstance(self.bus2, Bus) - for i in range(self._n): self._cy_element.connect(self.bus2._cy_element, [(i, i)], False) # @@ -130,11 +119,7 @@ def from_dict(cls, data: JsonDict, *, include_results: bool = True) -> Self: return cls(**data) # not used anymore def _to_dict(self, include_results: bool) -> JsonDict: - res = { - "id": self.id, - "bus1": self.bus1.id, - "bus2": self.bus2.id, - } + res = {"id": self.id, "bus1": self.bus1.id, "bus2": self.bus2.id} if self.geometry is not None: res["geometry"] = self.geometry.__geo_interface__ if include_results: diff --git a/roseau/load_flow_single/models/buses.py b/roseau/load_flow_single/models/buses.py index 538efc0..c3b0b23 100644 --- a/roseau/load_flow_single/models/buses.py +++ b/roseau/load_flow_single/models/buses.py @@ -13,6 +13,7 @@ from roseau.load_flow.units import Q_, ureg_wraps from roseau.load_flow.utils._exceptions import find_stack_level from roseau.load_flow_engine.cy_engine import CyBus +from roseau.load_flow_single._constants import SQRT3 from roseau.load_flow_single.models.core import Element logger = logging.getLogger(__name__) @@ -26,10 +27,10 @@ def __init__( id: Id, *, geometry: BaseGeometry | None = None, - potential: float | None = None, nominal_voltage: float | None = None, min_voltage_level: float | None = None, max_voltage_level: float | None = None, + initial_voltage: complex | None = None, ) -> None: """Bus constructor. @@ -54,12 +55,16 @@ def __init__( An optional maximum voltage of the bus (%). It is not used in the load flow. It must be a percentage of the `nominal_voltage`. If provided, the nominal voltage becomes mandatory. Either a float (unitless) or a :class:`Quantity ` of float. + + initial_voltage: + An optional initial voltage of the bus (V). It can be used to improve the convergence + of the load flow algorithm. """ super().__init__(id) - initialized = potential is not None - if potential is None: - potential = 0.0 - self.potential = potential + initialized = initial_voltage is not None + if initial_voltage is None: + initial_voltage = 0.0 + self.initial_voltage = initial_voltage self.geometry = geometry self._nominal_voltage: float | None = None self._min_voltage_level: float | None = None @@ -71,42 +76,39 @@ def __init__( if max_voltage_level is not None: self.max_voltage_level = max_voltage_level - self._res_potential: Complex | None = None + self._res_voltage: Complex | None = None self._short_circuits: list[dict[str, Any]] = [] self._n = 2 self._initialized = initialized self._initialized_by_the_user = initialized # only used for serialization - self._cy_element = CyBus(n=self._n, potentials=np.array([self._potential, 0], dtype=np.complex128)) + self._cy_element = CyBus( + n=self._n, potentials=np.array([self._initial_voltage / SQRT3, 0], dtype=np.complex128) + ) def __repr__(self) -> str: return f"{type(self).__name__}(id={self.id!r})" @property @ureg_wraps("V", (None,)) - def potential(self) -> Q_[Complex]: - """An array of initial potentials of the bus (V).""" - return self._potential + def initial_voltage(self) -> Q_[Complex]: + """Initial voltage of the bus (V).""" + return self._initial_voltage - @potential.setter + @initial_voltage.setter @ureg_wraps(None, (None, "V")) - def potential(self, value: float) -> None: - self._potential = value / np.sqrt(3.0) + def initial_voltage(self, value: Complex | Q_[Complex]) -> None: + self._initial_voltage: complex = value self._invalidate_network_results() self._initialized = True self._initialized_by_the_user = True if self._cy_element is not None: - self._cy_element.initialize_potentials(np.array([self._potential, 0], dtype=np.complex128)) + self._cy_element.initialize_potentials(np.array([self._initial_voltage / SQRT3, 0], dtype=np.complex128)) - def _res_potential_getter(self, warning: bool) -> Complex: + def _res_voltage_getter(self, warning: bool) -> Complex: if self._fetch_results: - self._res_potential = self._cy_element.get_potentials(self._n)[0] - return self._res_getter(value=self._res_potential, warning=warning) - - def _res_voltage_getter(self, warning: bool, potential: Complex | None = None) -> Complex: - if potential is None: - potential = self._res_potential_getter(warning=warning) - return potential * np.sqrt(3.0) + self._res_voltage = self._cy_element.get_potentials(self._n)[0] * SQRT3 + return self._res_getter(value=self._res_voltage, warning=warning) @property @ureg_wraps("V", (None,)) @@ -354,18 +356,18 @@ def get_connected_buses(self) -> Iterator[Id]: @classmethod def from_dict(cls, data: JsonDict, *, include_results: bool = True) -> Self: geometry = cls._parse_geometry(data.get("geometry")) - if (potential := data.get("potential")) is not None: - potential = complex(potential[0], potential[1]) + if (initial_voltage := data.get("initial_voltage")) is not None: + initial_voltage = complex(initial_voltage[0], initial_voltage[1]) self = cls( id=data["id"], geometry=geometry, - potential=potential, + initial_voltage=initial_voltage, nominal_voltage=data.get("nominal_voltage"), min_voltage_level=data.get("min_voltage_level"), max_voltage_level=data.get("max_voltage_level"), ) if include_results and "results" in data: - self._res_potential = complex(data["results"]["potential"][0], data["results"]["potential"][1]) + self._res_voltage = complex(data["results"]["voltage"][0], data["results"]["voltage"][1]) self._fetch_results = False self._no_results = False return self @@ -373,7 +375,7 @@ def from_dict(cls, data: JsonDict, *, include_results: bool = True) -> Self: def _to_dict(self, include_results: bool) -> JsonDict: res = {"id": self.id} if self._initialized_by_the_user: - res["potential"] = [self._potential.real, self._potential.imag] + res["initial_voltage"] = [self._initial_voltage.real, self._initial_voltage.imag] if self.geometry is not None: res["geometry"] = self.geometry.__geo_interface__ if self.nominal_voltage is not None: @@ -383,17 +385,11 @@ def _to_dict(self, include_results: bool) -> JsonDict: if self.max_voltage_level is not None: res["max_voltage_level"] = self.max_voltage_level.magnitude if include_results: - potential = self._res_potential_getter(warning=True) - res["results"] = {"potential": [potential.real, potential.imag]} + voltage = self._res_voltage_getter(warning=True) + res["results"] = {"voltage": [voltage.real, voltage.imag]} return res def _results_to_dict(self, warning: bool, full: bool) -> JsonDict: - potential = self._res_potential_getter(warning) - res = { - "id": self.id, - "potential": [potential.real, potential.imag], - } - if full: - v = self._res_voltage_getter(warning=False, potential=potential) - res["voltage"] = [v.real, v.imag] + voltage = self._res_voltage_getter(warning) + res = {"id": self.id, "voltage": [voltage.real, voltage.imag]} return res diff --git a/roseau/load_flow_single/models/flexible_parameters.py b/roseau/load_flow_single/models/flexible_parameters.py index 98128a9..fb4a5d8 100644 --- a/roseau/load_flow_single/models/flexible_parameters.py +++ b/roseau/load_flow_single/models/flexible_parameters.py @@ -10,6 +10,7 @@ from roseau.load_flow.typing import ComplexArray, ControlType, FloatArrayLike1D, ProjectionType from roseau.load_flow.units import Q_, ureg_wraps from roseau.load_flow_engine.cy_engine import CyControl, CyFlexibleParameter +from roseau.load_flow_single._constants import SQRT3 logger = logging.getLogger(__name__) @@ -77,10 +78,10 @@ def __init__( super().__init__(type=type, u_min=u_min, u_down=u_down, u_up=u_up, u_max=u_max, alpha=alpha, epsilon=epsilon) self._cy_control = CyControl( t=self._type, - u_min=self._u_min / np.sqrt(3.0), - u_down=self._u_down / np.sqrt(3.0), - u_up=self._u_up / np.sqrt(3.0), - u_max=self._u_max / np.sqrt(3.0), + u_min=self._u_min / SQRT3, + u_down=self._u_down / SQRT3, + u_up=self._u_up / SQRT3, + u_max=self._u_max / SQRT3, alpha=self._alpha, epsilon=self._epsilon, ) @@ -626,5 +627,5 @@ def pq_u_consumption( def _compute_powers(self, voltages: FloatArrayLike1D, power: complex) -> ComplexArray: # Iterate over the provided voltages to get the associated flexible powers - res_flexible_powers = [self._cy_fp.compute_power(v / np.sqrt(3.0), power / 3.0) for v in voltages] + res_flexible_powers = [self._cy_fp.compute_power(v / SQRT3, power / 3.0) for v in voltages] return np.array(res_flexible_powers, dtype=complex) diff --git a/roseau/load_flow_single/models/line_parameters.py b/roseau/load_flow_single/models/line_parameters.py index 6d4d9aa..986031e 100644 --- a/roseau/load_flow_single/models/line_parameters.py +++ b/roseau/load_flow_single/models/line_parameters.py @@ -195,10 +195,10 @@ def insulator(self) -> Insulator | None: @property def ampacity(self) -> Q_[Float] | None: - """The ampacities of the line (A) if it is set. Informative only, it has no impact on the load flow. + """The ampacity of the line (A) if it is set. Informative only, it has no impact on the load flow. Returns: - The ampacities of the line to model. + The ampacity of the line to model. """ return None if self._ampacity is None else Q_(self._ampacity, "A") @@ -529,7 +529,7 @@ def from_open_dss( OpenDSS parameter: `NormAmps`. Normal ampere limit on line (A). This is the so-called Planning Limit. It may also be the value above which load will have to be dropped in a contingency. Usually about 75% - 80% of the emergency (one-hour) rating. - This value is passed to `ampacities` and used for violation checks. + This value is passed to `ampacity` and used for violation checks. linetype: OpenDSS parameter: `LineType`. Code designating the type of line. Only ``"OH"`` diff --git a/roseau/load_flow_single/models/lines.py b/roseau/load_flow_single/models/lines.py index dc04f46..5e261a4 100644 --- a/roseau/load_flow_single/models/lines.py +++ b/roseau/load_flow_single/models/lines.py @@ -7,6 +7,7 @@ from roseau.load_flow.typing import Complex, Id, JsonDict from roseau.load_flow.units import Q_, ureg_wraps from roseau.load_flow_engine.cy_engine import CyShuntLine, CySimplifiedLine +from roseau.load_flow_single._constants import SQRT3 from roseau.load_flow_single.models.branches import AbstractBranch from roseau.load_flow_single.models.buses import Bus from roseau.load_flow_single.models.line_parameters import LineParameters @@ -50,7 +51,7 @@ def __init__( max_loading: The maximum loading of the line (unitless). It is not used in the load flow. It is used with the - `ampacities` of the :class:`LineParameters` to compute the + `ampacity` of the :class:`LineParameters` to compute the :meth:`~roseau.load_flow_single.Line.max_current` of the line. geometry: @@ -188,9 +189,9 @@ def with_shunt(self) -> bool: return self._with_shunt def _res_series_values_getter(self, warning: bool) -> tuple[Complex, Complex]: - pot1, pot2 = self._res_potentials_getter(warning) # V - du_line = pot1 - pot2 - i_line = self._z_line_inv * du_line # Zₗ x Iₗ = ΔU -> I = Zₗ⁻¹ x ΔU + volt1, volt2 = self._res_voltages_getter(warning) # V + du_line = volt1 - volt2 + i_line = self._z_line_inv * du_line / SQRT3 # Zₗ x Iₗ = ΔU -> I = Zₗ⁻¹ x ΔU return du_line, i_line def _res_series_currents_getter(self, warning: bool) -> Complex: @@ -205,7 +206,7 @@ def res_series_currents(self) -> Q_[Complex]: def _res_series_power_losses_getter(self, warning: bool) -> Complex: du_line, i_line = self._res_series_values_getter(warning) - return du_line * i_line.conjugate() * 3.0 # Sₗ = ΔU.Iₗ* + return du_line * i_line.conjugate() * SQRT3 # Sₗ = √3.ΔU.Iₗ* @property @ureg_wraps("VA", (None,)) @@ -215,12 +216,10 @@ def res_series_power_losses(self) -> Q_[Complex]: def _res_shunt_values_getter(self, warning: bool) -> tuple[Complex, Complex, Complex, Complex]: assert self.with_shunt, "This method only works when there is a shunt" - pot1, pot2 = self._res_potentials_getter(warning) - vg = 0j - ig = self._yg * vg - i1_shunt = (self._y_shunt * pot1 - ig) / 2 - i2_shunt = (self._y_shunt * pot2 - ig) / 2 - return pot1, pot2, i1_shunt, i2_shunt + volt1, volt2 = self._res_voltages_getter(warning) + i1_shunt = self._y_shunt * volt1 / SQRT3 / 2 + i2_shunt = self._y_shunt * volt2 / SQRT3 / 2 + return volt1, volt2, i1_shunt, i2_shunt def _res_shunt_currents_getter(self, warning: bool) -> tuple[Complex, Complex]: if not self.with_shunt: @@ -238,8 +237,8 @@ def res_shunt_currents(self) -> tuple[Q_[Complex], Q_[Complex]]: def _res_shunt_power_losses_getter(self, warning: bool) -> Complex: if not self.with_shunt: return 0j - pot1, pot2, cur1, cur2 = self._res_shunt_values_getter(warning) - return (pot1 * cur1.conjugate() + pot2 * cur2.conjugate()) * 3.0 + volt1, volt2, cur1, cur2 = self._res_shunt_values_getter(warning) + return (volt1 * cur1.conjugate() + volt2 * cur2.conjugate()) * SQRT3 @property @ureg_wraps("VA", (None,)) @@ -306,21 +305,18 @@ def _results_to_dict(self, warning: bool, full: bool) -> JsonDict: "current2": [current2.real, current2.imag], } if full: - potential1, potential2 = self._res_potentials_getter(warning=False) - results["potential1"] = [potential1.real, potential1.imag] - results["potential2"] = [potential2.real, potential2.imag] + voltage1, voltage2 = self._res_voltages_getter(warning=False) + results["voltage1"] = [voltage1.real, voltage1.imag] + results["voltage2"] = [voltage2.real, voltage2.imag] power1, power2 = self._res_powers_getter( warning=False, - potential1=potential1, - potential2=potential2, + voltage1=voltage1, + voltage2=voltage2, current1=current1, current2=current2, ) results["power1"] = [power1.real, power1.imag] results["power2"] = [power2.real, power2.imag] - voltage1, voltage2 = self._res_voltages_getter(warning=False, potential1=potential1, potential2=potential2) - results["voltage1"] = [voltage1.real, voltage1.imag] - results["voltage2"] = [voltage2.real, voltage2.imag] s = self._res_power_losses_getter(warning=False) results["power_losses"] = [s.real, s.imag] i = self._res_series_currents_getter(warning=False) diff --git a/roseau/load_flow_single/models/loads.py b/roseau/load_flow_single/models/loads.py index 95a3388..c535d73 100644 --- a/roseau/load_flow_single/models/loads.py +++ b/roseau/load_flow_single/models/loads.py @@ -8,6 +8,7 @@ from roseau.load_flow.typing import Complex, Id, JsonDict from roseau.load_flow.units import Q_, ureg_wraps from roseau.load_flow_engine.cy_engine import CyAdmittanceLoad, CyCurrentLoad, CyFlexibleLoad, CyPowerLoad +from roseau.load_flow_single._constants import SQRT3 from roseau.load_flow_single.models.buses import Bus from roseau.load_flow_single.models.core import Element from roseau.load_flow_single.models.flexible_parameters import FlexibleParameter @@ -41,7 +42,7 @@ def __init__(self, id: Id, bus: Bus) -> None: # Results self._res_current: Complex | None = None - self._res_potential: Complex | None = None + self._res_voltage: Complex | None = None def __repr__(self) -> str: bus_id = self.bus.id if self.bus is not None else None @@ -59,7 +60,7 @@ def is_flexible(self) -> bool: def _refresh_results(self) -> None: self._res_current = self._cy_element.get_currents(self._n)[0] - self._res_potential = self._cy_element.get_potentials(self._n)[0] + self._res_voltage = self._cy_element.get_potentials(self._n)[0] * SQRT3 def _res_current_getter(self, warning: bool) -> Complex: if self._fetch_results: @@ -80,13 +81,10 @@ def _validate_value(self, value: Complex) -> Complex: raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_Z_VALUE) return value - def _res_potential_getter(self, warning: bool) -> Complex: + def _res_voltage_getter(self, warning: bool) -> Complex: if self._fetch_results: self._refresh_results() - return self._res_getter(value=self._res_potential, warning=warning) - - def _res_voltage_getter(self, warning: bool) -> Complex: - return self._res_potential_getter(warning) * np.sqrt(3.0) + return self._res_getter(value=self._res_voltage, warning=warning) @property @ureg_wraps("V", (None,)) @@ -95,14 +93,14 @@ def res_voltage(self) -> Q_[Complex]: return self._res_voltage_getter(warning=True) def _res_power_getter( - self, warning: bool, current: Complex | None = None, potential: Complex | None = None + self, warning: bool, current: Complex | None = None, voltage: Complex | None = None ) -> Complex: if current is None: current = self._res_current_getter(warning=warning) warning = False # we warn only one - if potential is None: - potential = self._res_potential_getter(warning=warning) - return potential * current.conjugate() * 3.0 + if voltage is None: + voltage = self._res_voltage_getter(warning=warning) + return voltage * current.conjugate() * SQRT3 @property @ureg_wraps("VA", (None,)) @@ -165,7 +163,7 @@ def from_dict(cls, data: JsonDict, *, include_results: bool = True) -> "Abstract raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_LOAD_TYPE) if include_results and "results" in data: self._res_current = complex(data["results"]["current"][0], data["results"]["current"][1]) - self._res_potential = complex(data["results"]["potential"][0], data["results"]["potential"][1]) + self._res_voltage = complex(data["results"]["voltage"][0], data["results"]["voltage"][1]) if "flexible_power" in data["results"]: self._res_flexible_power = complex( data["results"]["flexible_power"][0], data["results"]["flexible_power"][1] @@ -187,21 +185,17 @@ def _to_dict(self, include_results: bool) -> JsonDict: if include_results: current = self._res_current_getter(warning=True) res["results"] = {"current": [current.real, current.imag]} - potential = self._res_potential_getter(warning=True) - res["results"]["potential"] = [potential.real, potential.imag] + voltage = self._res_voltage_getter(warning=True) + res["results"]["voltage"] = [voltage.real, voltage.imag] return res def _results_to_dict(self, warning: bool, full: bool) -> JsonDict: current = self._res_current_getter(warning) - results = { - "id": self.id, - "type": self.type, - "current": [current.real, current.imag], - } - potential = self._res_potential_getter(warning=False) - results["potential"] = [potential.real, potential.imag] + results = {"id": self.id, "type": self.type, "current": [current.real, current.imag]} + voltage = self._res_voltage_getter(warning=False) + results["voltage"] = [voltage.real, voltage.imag] if full: - power = self._res_power_getter(warning=False, current=current, potential=potential) + power = self._res_power_getter(warning=False, current=current, voltage=voltage) results["power"] = [power.real, power.imag] return results diff --git a/roseau/load_flow_single/models/sources.py b/roseau/load_flow_single/models/sources.py index 14f4f1f..74ce85e 100644 --- a/roseau/load_flow_single/models/sources.py +++ b/roseau/load_flow_single/models/sources.py @@ -8,6 +8,7 @@ from roseau.load_flow.typing import Complex, Id, JsonDict from roseau.load_flow.units import Q_, ureg_wraps from roseau.load_flow_engine.cy_engine import CyVoltageSource +from roseau.load_flow_single._constants import SQRT3 from roseau.load_flow_single.models.core import Element logger = logging.getLogger(__name__) @@ -47,14 +48,12 @@ def __init__( self._bus = bus self._n = 2 self.voltage = voltage - self._cy_element = CyVoltageSource( - n=self._n, voltages=np.array([self._voltage / np.sqrt(3.0)], dtype=np.complex128) - ) + self._cy_element = CyVoltageSource(n=self._n, voltages=np.array([self._voltage / SQRT3], dtype=np.complex128)) self._cy_connect() # Results self._res_current: Complex | None = None - self._res_potential: Complex | None = None + self._res_voltage: Complex | None = None def __repr__(self) -> str: bus_id = self.bus.id if self.bus is not None else None @@ -81,11 +80,11 @@ def voltage(self, value: Complex) -> None: self._voltage = value self._invalidate_network_results() if self._cy_element is not None: - self._cy_element.update_voltages(np.array([self._voltage / np.sqrt(3.0)], dtype=np.complex128)) + self._cy_element.update_voltages(np.array([self._voltage / SQRT3], dtype=np.complex128)) def _refresh_results(self) -> None: self._res_current = self._cy_element.get_currents(self._n)[0] - self._res_potential = self._cy_element.get_potentials(self._n)[0] + self._res_voltage = self._cy_element.get_potentials(self._n)[0] * SQRT3 def _res_current_getter(self, warning: bool) -> Complex: if self._fetch_results: @@ -98,13 +97,10 @@ def res_current(self) -> Q_[Complex]: """The load flow result of the source currents (A).""" return self._res_current_getter(warning=True) - def _res_potential_getter(self, warning: bool) -> Complex: + def _res_voltage_getter(self, warning: bool) -> Complex: if self._fetch_results: self._refresh_results() - return self._res_getter(value=self._res_potential, warning=warning) - - def _res_voltage_getter(self, warning: bool) -> Complex: - return self._res_potential_getter(warning) * np.sqrt(3.0) + return self._res_getter(value=self._res_voltage, warning=warning) @property @ureg_wraps("V", (None,)) @@ -113,14 +109,14 @@ def res_voltage(self) -> Q_[Complex]: return self._res_voltage_getter(warning=True) def _res_power_getter( - self, warning: bool, current: Complex | None = None, potential: Complex | None = None + self, warning: bool, current: Complex | None = None, voltage: Complex | None = None ) -> Complex: if current is None: current = self._res_current_getter(warning=warning) warning = False # we warn only once - if potential is None: - potential = self._res_potential_getter(warning=warning) - return potential * current.conjugate() * 3.0 + if voltage is None: + voltage = self._res_voltage_getter(warning=warning) + return voltage * current.conjugate() * SQRT3 @property @ureg_wraps("VA", (None,)) @@ -158,7 +154,7 @@ def from_dict(cls, data: JsonDict, *, include_results: bool = True) -> Self: self = cls(id=data["id"], bus=data["bus"], voltage=voltage) if include_results and "results" in data: self._res_current = complex(data["results"]["current"][0], data["results"]["current"][1]) - self._res_potential = complex(data["results"]["potential"][0], data["results"]["potential"][1]) + self._res_voltage = complex(data["results"]["voltage"][0], data["results"]["voltage"][1]) self._fetch_results = False self._no_results = False return self @@ -173,8 +169,8 @@ def _to_dict(self, include_results: bool) -> JsonDict: if include_results: current = self._res_current_getter(warning=True) res["results"] = {"current": [current.real, current.imag]} - potential = self._res_potential_getter(warning=False) - res["results"]["potential"] = [potential.real, potential.imag] + voltage = self._res_voltage_getter(warning=False) + res["results"]["voltage"] = [voltage.real, voltage.imag] return res def _results_to_dict(self, warning: bool, full: bool) -> JsonDict: @@ -183,9 +179,9 @@ def _results_to_dict(self, warning: bool, full: bool) -> JsonDict: "id": self.id, "current": [current.real, current.imag], } - potential = self._res_potential_getter(warning=False) - results["potential"] = [potential.real, potential.imag] + voltage = self._res_voltage_getter(warning=False) + results["voltage"] = [voltage.real, voltage.imag] if full: - power = self._res_power_getter(warning=False, current=current, potential=potential) + power = self._res_power_getter(warning=False, current=current, voltage=voltage) results["power"] = [power.real, power.imag] return results diff --git a/roseau/load_flow_single/models/switches.py b/roseau/load_flow_single/models/switches.py index 58554d5..d244203 100644 --- a/roseau/load_flow_single/models/switches.py +++ b/roseau/load_flow_single/models/switches.py @@ -100,19 +100,16 @@ def _results_to_dict(self, warning: bool, full: bool) -> JsonDict: "current2": [current2.real, current2.imag], } if full: - potential1, potential2 = self._res_potentials_getter(warning=False) - results["potential1"] = [potential1.real, potential1.imag] - results["potential2"] = [potential2.real, potential2.imag] + voltage1, voltage2 = self._res_voltages_getter(warning=False) + results["voltage1"] = [voltage1.real, voltage1.imag] + results["voltage2"] = [voltage2.real, voltage2.imag] power1, power2 = self._res_powers_getter( warning=False, - potential1=potential1, - potential2=potential2, + voltage1=voltage1, + voltage2=voltage2, current1=current1, current2=current2, ) results["power1"] = [power1.real, power1.imag] results["power2"] = [power2.real, power2.imag] - voltage1, voltage2 = self._res_voltages_getter(warning=False, potential1=potential1, potential2=potential2) - results["voltage1"] = [voltage1.real, voltage1.imag] - results["voltage2"] = [voltage2.real, voltage2.imag] return results diff --git a/roseau/load_flow_single/models/tests/data/small_network.json b/roseau/load_flow_single/models/tests/data/small_network.json index 73554ae..da24536 100644 --- a/roseau/load_flow_single/models/tests/data/small_network.json +++ b/roseau/load_flow_single/models/tests/data/small_network.json @@ -9,7 +9,7 @@ "coordinates": [-1.318375372111463, 48.64794139348595] }, "results": { - "potential": [11547.005383792515, 0.0] + "voltage": [20000.0, 0.0] } }, { @@ -19,7 +19,7 @@ "coordinates": [-1.320149235966572, 48.64971306653889] }, "results": { - "potential": [11546.918780602613, 0.0] + "voltage": [19999.84999887499, 0.0] } } ], @@ -54,7 +54,7 @@ "power": [300, 0], "results": { "current": [0.008660318990723963, -0.0], - "potential": [11546.918780602613, 0.0] + "voltage": [19999.84999887499, 0.0] } } ], @@ -65,7 +65,7 @@ "voltage": [20000.0, 0.0], "results": { "current": [-0.008660318990236813, 0.0], - "potential": [11547.005383792515, 0.0] + "voltage": [20000.0, 0.0] } } ], diff --git a/roseau/load_flow_single/models/tests/data/small_shunt_network.json b/roseau/load_flow_single/models/tests/data/small_shunt_network.json index ac0c22f..a3f6aa8 100644 --- a/roseau/load_flow_single/models/tests/data/small_shunt_network.json +++ b/roseau/load_flow_single/models/tests/data/small_shunt_network.json @@ -9,7 +9,7 @@ "coordinates": [-1.318375372111463, 48.64794139348595] }, "results": { - "potential": [11547.005383792515, 0.0] + "voltage": [20000.0, 0.0] } }, { @@ -19,7 +19,7 @@ "coordinates": [-1.320149235966572, 48.64971306653889] }, "results": { - "potential": [10997.061381341915, 0.0] + "voltage": [19047.469046437775, 0.0] } } ], @@ -54,7 +54,7 @@ "power": [300, 0], "results": { "current": [0.009093338350340053, -0.0], - "potential": [10997.061381341915, 0.0] + "voltage": [19047.469046437775, 0.0] } } ], @@ -65,7 +65,7 @@ "voltage": [20000.0, 0.0], "results": { "current": [-112.72942716402237, 0.0], - "potential": [11547.005383792515, 0.0] + "voltage": [20000.0, 0.0] } } ], diff --git a/roseau/load_flow_single/models/tests/test_branches.py b/roseau/load_flow_single/models/tests/test_branches.py index c6faf68..706ea6b 100644 --- a/roseau/load_flow_single/models/tests/test_branches.py +++ b/roseau/load_flow_single/models/tests/test_branches.py @@ -9,18 +9,8 @@ def test_res_branches_voltages(): bus2 = Bus("bus2") lp = LineParameters("lp", z_line=1) line = Line("line", bus1, bus2, length=1, parameters=lp) - bus1._res_potential = 230.0 + 0.0j - bus2._res_potential = 225.47405027 + 0.0j + bus1._res_voltage = 400.0 + 0.0j + bus2._res_voltage = 380.47405027 + 0.0j line_v1, line_v2 = line.res_voltages assert np.isclose(line_v1, bus1.res_voltage) assert np.isclose(line_v2, bus2.res_voltage) - - -def test_powers_equal(network_with_results): - line: Line = network_with_results.lines["line"] - vs = network_with_results.sources["vs"] - pl = network_with_results.loads["load"] - power1, power2 = line.res_powers - assert np.allclose(power1, -vs.res_power) - assert np.allclose(power2, -pl.res_power) - assert np.allclose(power1 + power2, line.res_power_losses) diff --git a/roseau/load_flow_single/models/tests/test_buses.py b/roseau/load_flow_single/models/tests/test_buses.py index 9a27722..7399f69 100644 --- a/roseau/load_flow_single/models/tests/test_buses.py +++ b/roseau/load_flow_single/models/tests/test_buses.py @@ -75,8 +75,10 @@ def test_voltage_limits(): bus.nominal_voltage = None with pytest.warns( UserWarning, - match=r"The min voltage level of the bus 'bus' is useless without a nominal voltage. Please define a nominal " - "voltage for this bus.", + match=( + r"The min voltage level of the bus 'bus' is useless without a nominal voltage. Please " + r"define a nominal voltage for this bus." + ), ): bus.min_voltage_level = 0.95 @@ -84,8 +86,10 @@ def test_voltage_limits(): assert bus.min_voltage is None with pytest.warns( UserWarning, - match=r"The max voltage level of the bus 'bus' is useless without a nominal voltage. Please define a nominal " - "voltage for this bus.", + match=( + r"The max voltage level of the bus 'bus' is useless without a nominal voltage. Please " + r"define a nominal voltage for this bus." + ), ): bus.max_voltage_level = 1.05 assert bus.max_voltage_level == Q_(1.05, "") @@ -131,17 +135,17 @@ def test_voltage_limits(): def test_res_voltages(): bus = Bus(id="bus") - bus._res_potential = 230 + 0j + bus._res_voltage = 400 + 0j - assert np.allclose(bus.res_voltage.m, 230 * np.sqrt(3.0) + 0j) + assert np.allclose(bus.res_voltage.m, 400 + 0j) assert bus.res_voltage_level is None bus.nominal_voltage = 400 # V - assert np.allclose(bus.res_voltage_level.m, 230 / 400 * np.sqrt(3)) + assert np.allclose(bus.res_voltage_level.m, 400 / 400) def test_res_violated(): bus = Bus(id="bus") - bus._res_potential = 230 + 0 + bus._res_voltage = 400 + 0j # No limits assert bus.res_violated is None diff --git a/roseau/load_flow_single/models/tests/test_lines.py b/roseau/load_flow_single/models/tests/test_lines.py index 0eed7fe..d940e56 100644 --- a/roseau/load_flow_single/models/tests/test_lines.py +++ b/roseau/load_flow_single/models/tests/test_lines.py @@ -120,8 +120,8 @@ def test_res_violated(): lp = LineParameters(id="lp", z_line=1.0) line = Line(id="line", bus1=bus1, bus2=bus2, parameters=lp, length=Q_(50, "m")) - bus1._res_potential = 230 - bus2._res_potential = 225 + bus1._res_voltage = 400 + bus2._res_voltage = 380 line._res_currents = 10, -10 # No limits @@ -192,38 +192,56 @@ def test_lines_results(): z_line = (0.1 + 0.1j) / 2 y_shunt = None len_line = 10 - bus_pot = 20000.0 + 0.0j, 19961.964706645947 - 62.5j - line_cur = (100.53529335405256 + 24.464706645947444j, -100.53529335405256 - 24.464706645947444j) + bus1_voltage = 20000.0 + 0.0j + bus2_voltage = 19883.965550324414 - 84.999999999981j + line_currents = (116.06729363657514 - 17.9177478743607j), (-116.06729363657514 + 17.9177478743607j) bus1 = Bus(id="bus1") bus2 = Bus(id="bus2") y_shunt = np.array(y_shunt, dtype=np.complex128) if y_shunt is not None else None lp = LineParameters(id="lp", z_line=z_line, y_shunt=y_shunt) - line = Line( - id="line", - bus1=bus1, - bus2=bus2, - length=len_line, - parameters=lp, - ) - bus1._res_potential = bus_pot[0] - bus2._res_potential = bus_pot[1] - line._res_currents = line_cur[0], line_cur[1] - res_powers1, res_powers2 = line.res_powers - series_losses = line.res_series_power_losses - shunt_losses = line.res_shunt_power_losses - line_losses = line.res_power_losses + line = Line(id="line", bus1=bus1, bus2=bus2, length=len_line, parameters=lp) + bus1._res_voltage = bus1_voltage + bus2._res_voltage = bus2_voltage + line._res_currents = line_currents + res_powers1, res_powers2 = (x.m for x in line.res_powers) + series_losses = line.res_series_power_losses.m + shunt_losses = line.res_shunt_power_losses.m + line_losses = line.res_power_losses.m if y_shunt is None: - assert np.allclose(shunt_losses, 0) + assert np.isclose(shunt_losses, 0) else: - assert not np.allclose(shunt_losses, 0) - assert np.allclose(line_losses, series_losses + shunt_losses) + assert not np.isclose(shunt_losses, 0) + assert np.isclose(line_losses, series_losses + shunt_losses) # Sanity check: the total power lost is equal to the sum of the powers flowing through - assert np.allclose(res_powers1 + res_powers2, line_losses) + assert np.isclose(res_powers1 + res_powers2, line_losses) # Check currents (Kirchhoff's law at each end of the line) - i1_line, i2_line = line.res_currents - i_series = line.res_series_currents - i1_shunt, i2_shunt = line.res_shunt_currents - assert np.allclose(i1_line, i_series + i1_shunt) - assert np.allclose(i2_line + i_series, i2_shunt, atol=1.0e-4) + i1_line, i2_line = (x.m for x in line.res_currents) + i_series = line.res_series_currents.m + i1_shunt, i2_shunt = (x.m for x in line.res_shunt_currents) + assert np.isclose(i1_line, i_series + i1_shunt) + assert np.isclose(i2_line + i_series, i2_shunt) + + +def test_currents_equal(network_with_results): + line = network_with_results.lines["line"] + current1, current2 = (x.m for x in line.res_currents) + series_current = line.res_series_currents.m + shunt_current1, shunt_current2 = (x.m for x in line.res_shunt_currents) + assert np.isclose(current1, series_current + shunt_current1) + assert np.isclose(current2 + series_current, shunt_current2) + + +def test_powers_equal(network_with_results): + line = network_with_results.lines["line"] + vs = network_with_results.sources["vs"] + pl = network_with_results.loads["load"] + power1, power2 = (x.m for x in line.res_powers) + power_loss = power1 + power2 + expected_power1 = -vs.res_power.m + expected_power2 = -pl.res_power.m + expected_power_loss = line.res_power_losses.m + assert np.isclose(power1, expected_power1) + assert np.isclose(power2, expected_power2) + assert np.isclose(power_loss, expected_power_loss) diff --git a/roseau/load_flow_single/models/tests/test_loads.py b/roseau/load_flow_single/models/tests/test_loads.py index 40c099a..c1a2a98 100644 --- a/roseau/load_flow_single/models/tests/test_loads.py +++ b/roseau/load_flow_single/models/tests/test_loads.py @@ -189,10 +189,10 @@ def test_loads_units(): def test_non_flexible_load_res_flexible_powers(): bus = Bus(id="bus") - load = PowerLoad(id="load", bus=bus, power=2300) - bus._res_potential = 230 - load._res_currents = 10, -10 - load._res_potential = 230 + load = PowerLoad(id="load", bus=bus, power=4000 * np.sqrt(3)) + bus._res_voltage = 400 + load._res_current = 10 + load._res_voltage = 400 with pytest.raises(RoseauLoadFlowException) as e: _ = load.res_flexible_power assert e.value.msg == "The load 'load' is not flexible and does not have flexible powers" diff --git a/roseau/load_flow_single/models/tests/test_switches.py b/roseau/load_flow_single/models/tests/test_switches.py index 8438a06..84bf642 100644 --- a/roseau/load_flow_single/models/tests/test_switches.py +++ b/roseau/load_flow_single/models/tests/test_switches.py @@ -36,8 +36,8 @@ def test_switch_loop(): def test_switch_connection(): bus1 = Bus(id="bus1") bus2 = Bus(id="bus2") - VoltageSource(id="vs1", bus=bus1, voltage=230 + 0j) - VoltageSource(id="vs2", bus=bus2, voltage=230 + 0j) + VoltageSource(id="vs1", bus=bus1, voltage=400 + 0j) + VoltageSource(id="vs2", bus=bus2, voltage=400 + 0j) with pytest.raises(RoseauLoadFlowException) as e: Switch(id="switch", bus1=bus1, bus2=bus2) assert e.value.msg == ( diff --git a/roseau/load_flow_single/models/tests/test_transformers.py b/roseau/load_flow_single/models/tests/test_transformers.py index b379e42..71b5464 100644 --- a/roseau/load_flow_single/models/tests/test_transformers.py +++ b/roseau/load_flow_single/models/tests/test_transformers.py @@ -48,9 +48,9 @@ def test_res_violated(): ) transformer = Transformer(id="transformer", bus1=bus1, bus2=bus2, parameters=tp) - bus1._res_potential = 20_000 - bus2._res_potential = 230 - transformer._res_currents = 0.8, -65 + bus1._res_voltage = 20_000 + bus2._res_voltage = 400 + transformer._res_currents = 1.0, -29.0 # 69% loading primary, 40% loading secondary # Default value assert transformer.max_loading == Q_(1, "") @@ -59,23 +59,23 @@ def test_res_violated(): # No constraint violated transformer.max_loading = 1 assert transformer.res_violated is False - assert np.allclose(transformer.res_loading, 0.8 * 20 * 3 / 50) + assert np.allclose(transformer.res_loading, 1.0 * 20 * np.sqrt(3) / 50) # Two violations - transformer.max_loading = 4 / 5 + transformer.max_loading = 0.3 assert transformer.res_violated is True - assert np.allclose(transformer.res_loading, 0.8 * 20 * 3 / 50) + assert np.allclose(transformer.res_loading, 1.0 * 20 * np.sqrt(3) / 50) # Primary side violation - transformer.max_loading = Q_(45, "%") + transformer.max_loading = Q_(50, "%") assert transformer.res_violated is True - assert np.allclose(transformer.res_loading, 0.8 * 20 * 3 / 50) + assert np.allclose(transformer.res_loading, 1.0 * 20 * np.sqrt(3) / 50) # Secondary side violation transformer.max_loading = 1 - transformer._res_currents = 0.8, -80 + transformer._res_currents = 1.0, -87.0 # 69% loading primary, 120% loading secondary assert transformer.res_violated is True - assert np.allclose(transformer.res_loading, 80 * 230 * 3 / 50_000) + assert np.allclose(transformer.res_loading, 87 * 400 * np.sqrt(3) / 50_000) def test_transformer_results(): @@ -86,17 +86,17 @@ def test_transformer_results(): ) transformer = Transformer(id="transformer", bus1=bus1, bus2=bus2, parameters=tp) - bus1._res_potential = 20_000 - bus2._res_potential = 230 + bus1._res_voltage = 20_000 + bus2._res_voltage = 400 transformer._res_currents = np.complex128(0.8 + 0j), np.complex128(-65 + 0j) res_p1, res_p2 = (p.m for p in transformer.res_powers) np.testing.assert_allclose( - res_p1, transformer.res_voltages[0].m / np.sqrt(3.0) * transformer.res_currents[0].m.conj() * 3.0 + res_p1, transformer.res_voltages[0].m * transformer.res_currents[0].m.conj() * np.sqrt(3.0) ) np.testing.assert_allclose( - res_p2, transformer.res_voltages[1].m / np.sqrt(3.0) * transformer.res_currents[1].m.conj() * 3.0 + res_p2, transformer.res_voltages[1].m * transformer.res_currents[1].m.conj() * np.sqrt(3.0) ) expected_total_losses = res_p1 + res_p2 diff --git a/roseau/load_flow_single/models/transformers.py b/roseau/load_flow_single/models/transformers.py index 3d38510..3bdefd6 100644 --- a/roseau/load_flow_single/models/transformers.py +++ b/roseau/load_flow_single/models/transformers.py @@ -202,21 +202,18 @@ def _results_to_dict(self, warning: bool, full: bool) -> JsonDict: "current2": [current2.real, current2.imag], } if full: - potential1, potential2 = self._res_potentials_getter(warning=False) - results["potential1"] = [potential1.real, potential1.imag] - results["potential2"] = [potential2.real, potential2.imag] + voltage1, voltage2 = self._res_voltages_getter(warning=False) + results["voltage1"] = [voltage1.real, voltage1.imag] + results["voltage2"] = [voltage2.real, voltage2.imag] power1, power2 = self._res_powers_getter( warning=False, - potential1=potential1, - potential2=potential2, + voltage1=voltage1, + voltage2=voltage2, current1=current1, current2=current2, ) results["power1"] = [power1.real, power1.imag] results["power2"] = [power2.real, power2.imag] - voltage1, voltage2 = self._res_voltages_getter(warning=False, potential1=potential1, potential2=potential2) - results["voltage1"] = [voltage1.real, voltage1.imag] - results["voltage2"] = [voltage2.real, voltage2.imag] power_losses = power1 + power2 results["power_losses"] = [power_losses.real, power_losses.imag] diff --git a/roseau/load_flow_single/network.py b/roseau/load_flow_single/network.py index 3e42ba5..a277736 100644 --- a/roseau/load_flow_single/network.py +++ b/roseau/load_flow_single/network.py @@ -23,6 +23,7 @@ from roseau.load_flow.utils import JsonMixin, _optional_deps from roseau.load_flow.utils.types import _DTYPES, LoadTypeDtype from roseau.load_flow_engine.cy_engine import CyElectricalNetwork, CyGround, CyPotentialRef +from roseau.load_flow_single._constants import SQRT3 from roseau.load_flow_single.io import network_from_dict, network_to_dict from roseau.load_flow_single.models.branches import AbstractBranch from roseau.load_flow_single.models.buses import Bus @@ -445,8 +446,8 @@ def solve_load_flow( Tolerance needed for the convergence. warm_start: - If true (the default), the solver is initialized with the potentials of the last - successful load flow result (if any). Otherwise, the potentials are reset to their + If true (the default), the solver is initialized with the voltages of the last + successful load flow result (if any). Otherwise, the voltages are reset to their initial values. solver: @@ -464,7 +465,7 @@ def solve_load_flow( """ if not self._valid: self._check_validity(constructed=False) - self._create_network() # <-- calls _propagate_potentials, no warm start + self._create_network() # <-- calls _propagate_voltages, no warm start self._solver.update_network(self) # Update solver @@ -670,11 +671,11 @@ def res_lines(self) -> pd.DataFrame: dtypes = {c: _DTYPES[c] for c in res_dict} for line in self.lines.values(): current1, current2 = line._res_currents_getter(warning=False) - potential1, potential2 = line._res_potentials_getter(warning=False) + voltage1, voltage2 = line._res_voltages_getter(warning=False) du_line, series_current = line._res_series_values_getter(warning=False) - power1 = potential1 * current1.conjugate() * 3.0 - power2 = potential2 * current2.conjugate() * 3.0 - series_loss = du_line * series_current.conjugate() * 3.0 + power1 = voltage1 * current1.conjugate() * SQRT3 + power2 = voltage2 * current2.conjugate() * SQRT3 + series_loss = du_line * series_current.conjugate() * SQRT3 max_loading = line._max_loading ampacity = line.parameters._ampacity if ampacity is None: @@ -688,8 +689,8 @@ def res_lines(self) -> pd.DataFrame: res_dict["current2"].append(current2) res_dict["power1"].append(power1) res_dict["power2"].append(power2) - res_dict["voltage1"].append(potential1 * np.sqrt(3.0)) - res_dict["voltage2"].append(potential2 * np.sqrt(3.0)) + res_dict["voltage1"].append(voltage1) + res_dict["voltage2"].append(voltage2) res_dict["series_losses"].append(series_loss) res_dict["series_current"].append(series_current) res_dict["loading"].append(loading) @@ -733,9 +734,9 @@ def res_transformers(self) -> pd.DataFrame: dtypes = {c: _DTYPES[c] for c in res_dict} for transformer in self.transformers.values(): current1, current2 = transformer._res_currents_getter(warning=False) - potential1, potential2 = transformer._res_potentials_getter(warning=False) - power1 = potential1 * current1.conjugate() * 3.0 - power2 = potential2 * current2.conjugate() * 3.0 + voltage1, voltage2 = transformer._res_voltages_getter(warning=False) + power1 = voltage1 * current1.conjugate() * SQRT3 + power2 = voltage2 * current2.conjugate() * SQRT3 sn = transformer.parameters._sn max_loading = transformer._max_loading loading = max(abs(power1), abs(power2)) / sn @@ -745,8 +746,8 @@ def res_transformers(self) -> pd.DataFrame: res_dict["current2"].append(current2) res_dict["power1"].append(power1) res_dict["power2"].append(power2) - res_dict["voltage1"].append(potential1 * np.sqrt(3.0)) - res_dict["voltage2"].append(potential2 * np.sqrt(3.0)) + res_dict["voltage1"].append(voltage1) + res_dict["voltage2"].append(voltage2) res_dict["violated"].append(violated) res_dict["loading"].append(loading) # Non results @@ -784,16 +785,16 @@ def res_switches(self) -> pd.DataFrame: if not isinstance(switch, Switch): continue current1, current2 = switch._res_currents_getter(warning=False) - potential1, potential2 = switch._res_potentials_getter(warning=False) - power1 = potential1 * current1.conjugate() * 3.0 - power2 = potential2 * current2.conjugate() * 3.0 + voltage1, voltage2 = switch._res_voltages_getter(warning=False) + power1 = voltage1 * current1.conjugate() * SQRT3 + power2 = voltage2 * current2.conjugate() * SQRT3 res_dict["switch_id"].append(switch.id) res_dict["current1"].append(current1) res_dict["current2"].append(current2) res_dict["power1"].append(power1) res_dict["power2"].append(power2) - res_dict["voltage1"].append(potential1 * np.sqrt(3.0)) - res_dict["voltage2"].append(potential2 * np.sqrt(3.0)) + res_dict["voltage1"].append(voltage1) + res_dict["voltage2"].append(voltage2) return pd.DataFrame(res_dict).astype(dtypes).set_index("switch_id") @property @@ -814,13 +815,13 @@ def res_loads(self) -> pd.DataFrame: dtypes = {c: _DTYPES[c] for c in res_dict} | {"type": LoadTypeDtype} for load_id, load in self.loads.items(): current = load._res_current_getter(warning=False) - potential = load._res_potential_getter(warning=False) - power = potential * current.conjugate() * 3.0 + voltage = load._res_voltage_getter(warning=False) + power = voltage * current.conjugate() * SQRT3 res_dict["load_id"].append(load_id) res_dict["type"].append(load.type) res_dict["current"].append(current) res_dict["power"].append(power) - res_dict["voltage"].append(potential * np.sqrt(3.0)) + res_dict["voltage"].append(voltage) return pd.DataFrame(res_dict).astype(dtypes).set_index("load_id") @property @@ -865,12 +866,12 @@ def res_sources(self) -> pd.DataFrame: dtypes = {c: _DTYPES[c] for c in res_dict} for source_id, source in self.sources.items(): current = source._res_current_getter(warning=False) - potential = source._res_potential_getter(warning=False) - power = potential * current.conjugate() * 3.0 + voltage = source._res_voltage_getter(warning=False) + power = voltage * current.conjugate() * SQRT3 res_dict["source_id"].append(source_id) res_dict["current"].append(current) res_dict["power"].append(power) - res_dict["voltage"].append(potential * np.sqrt(3.0)) + res_dict["voltage"].append(voltage) return pd.DataFrame(res_dict).astype(dtypes).set_index("source_id") # @@ -955,7 +956,7 @@ def _disconnect_element(self, element: Element) -> None: def _create_network(self) -> None: """Create the Cython and C++ electrical network of all the passed elements.""" self._valid = True - self._propagate_potentials() + self._propagate_voltages() cy_elements = [] for element in self._elements: cy_elements.append(element._cy_element) @@ -1021,27 +1022,27 @@ def _reset_inputs(self) -> None: if self._solver is not None: self._solver.reset_inputs() - def _propagate_potentials(self) -> None: - """Set the bus potentials that have not been initialized yet and compute self._elements order.""" - starting_potential, starting_source = self._get_potential() - elements = [(starting_source, starting_potential, None)] + def _propagate_voltages(self) -> None: + """Set the voltage on buses that have not been initialized yet and compute self._elements order.""" + starting_voltage, starting_source = self._get_starting_voltage() + elements = [(starting_source, starting_voltage, None)] self._elements = [] self._has_loop = False visited = {starting_source} while elements: - element, potential, parent = elements.pop(-1) + element, initial_voltage, parent = elements.pop(-1) self._elements.append(element) if isinstance(element, Bus) and not element._initialized: - element.potential = potential + element.initial_voltage = initial_voltage element._initialized_by_the_user = False # only used for serialization for e in element._connected_elements: if e not in visited: if isinstance(element, Transformer): k = element.parameters._ulv / element.parameters._uhv - elements.append((e, potential * k, element)) # TODO dephasage + elements.append((e, initial_voltage * k, element)) # TODO dephasage visited.add(e) else: - elements.append((e, potential, element)) + elements.append((e, initial_voltage, element)) visited.add(e) elif parent != e: self._has_loop = True @@ -1068,17 +1069,17 @@ def _propagate_potentials(self) -> None: logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.POORLY_CONNECTED_ELEMENT) - def _get_potential(self) -> tuple[Complex, VoltageSource]: - """Compute initial potentials from the voltages sources of the network, get also the starting source""" + def _get_starting_voltage(self) -> tuple[Complex, VoltageSource]: + """Compute initial voltages from the voltage sources of the network, get also the starting source.""" starting_source = None - potential = None + initial_voltage = None # if there are multiple voltage sources, start from the higher one (the last one in the sorted below) for source in sorted(self.sources.values(), key=lambda x: np.average(np.abs(x._voltage))): source_voltage = source._voltage starting_source = source - potential = source_voltage + initial_voltage = source_voltage - return potential, starting_source + return initial_voltage, starting_source # # Network saving/loading diff --git a/roseau/load_flow_single/tests/data/networks/all_elements_network.json b/roseau/load_flow_single/tests/data/networks/all_elements_network.json index 4dd1c51..9079b99 100644 --- a/roseau/load_flow_single/tests/data/networks/all_elements_network.json +++ b/roseau/load_flow_single/tests/data/networks/all_elements_network.json @@ -5,31 +5,31 @@ { "id": "bus0", "results": { - "potential": [11547.005383792515, 9.648748247733486e-24] + "voltage": [20000.0, 1.671212219451557e-23] } }, { "id": "bus1", "results": { - "potential": [11545.367868180967, -0.7576708754205583] + "voltage": [19997.16373976261, -1.3123244516435961] } }, { "id": "bus2", "results": { - "potential": [201.08198474993915, 116.11720526341806] + "voltage": [348.2842140736848, 201.12089914914432] } }, { "id": "bus3", "results": { - "potential": [201.08198474993915, 116.11720526341806] + "voltage": [348.2842140736848, 201.12089914914432] } }, { "id": "bus4", "results": { - "potential": [194.1081015925913, 109.4174889092076] + "voltage": [336.20509411910945, 189.5166500273517] } } ], @@ -92,7 +92,7 @@ "power": [100.0, 5.0], "results": { "current": [0.13399020161652975, 0.06694313439799478], - "potential": [194.1081015925913, 109.4174889092076] + "voltage": [336.20509411910945, 189.5166500273517] } }, { @@ -102,7 +102,7 @@ "current": [1.0, 0.1], "results": { "current": [0.8220258033394239, 0.5781639721084105], - "potential": [194.1081015925913, 109.4174889092076] + "voltage": [336.20509411910945, 189.5166500273517] } }, { @@ -112,7 +112,7 @@ "impedance": [1, 0], "results": { "current": [194.1081015925913, 109.4174889092076], - "potential": [194.1081015925913, 109.4174889092076] + "voltage": [336.20509411910945, 189.5166500273517] } }, { @@ -122,7 +122,7 @@ "power": [100.0, 0.0], "results": { "current": [0.13031725176721196, 0.07345899698635538], - "potential": [194.1081015925913, 109.4174889092076], + "voltage": [336.20509411910945, 189.5166500273517], "flexible_power": [100.0, 0.0] }, "flexible_param": { @@ -147,7 +147,7 @@ "power": [100.0, 0.0], "results": { "current": [0.13030171340261612, 0.07345023811013185], - "potential": [194.1081015925913, 109.4174889092076], + "voltage": [336.20509411910945, 189.5166500273517], "flexible_power": [99.98807650991321, 0.0] }, "flexible_param": { @@ -176,7 +176,7 @@ "power": [100.0, 0.0], "results": { "current": [0.04002930587311224, 0.220793971726398], - "potential": [194.1081015925913, 109.4174889092076], + "voltage": [336.20509411910945, 189.5166500273517], "flexible_power": [95.7862035710761, -115.43397769045458] }, "flexible_param": { @@ -210,7 +210,7 @@ "power": [-100.0, 0.0], "results": { "current": [-0.13031725176721196, -0.07345899698635538], - "potential": [194.1081015925913, 109.4174889092076], + "voltage": [336.20509411910945, 189.5166500273517], "flexible_power": [-100.0, 0.0] }, "flexible_param": { @@ -239,7 +239,7 @@ "power": [-100.0, 0.0], "results": { "current": [-0.20961193505212175, 0.08009469372627451], - "potential": [194.1081015925913, 109.4174889092076], + "voltage": [336.20509411910945, 189.5166500273517], "flexible_power": [-95.77084356490765, -115.44672157695076] }, "flexible_param": { @@ -273,7 +273,7 @@ "power": [-100.0, 0.0], "results": { "current": [-0.18935122673446944, 0.031268071095670584], - "potential": [194.1081015925913, 109.4174889092076], + "voltage": [336.20509411910945, 189.5166500273517], "flexible_power": [-100.0, -80.36316501601938] }, "flexible_param": { @@ -306,7 +306,7 @@ "voltage": [20000.0, 0.0], "results": { "current": [-4.594797875915128, -0.5537548820066578], - "potential": [11547.005383792515, 9.648748247733486e-24] + "voltage": [20000.0, 1.671212219451557e-23] } } ], diff --git a/roseau/load_flow_single/tests/data/networks/small_network.json b/roseau/load_flow_single/tests/data/networks/small_network.json index 73554ae..da24536 100644 --- a/roseau/load_flow_single/tests/data/networks/small_network.json +++ b/roseau/load_flow_single/tests/data/networks/small_network.json @@ -9,7 +9,7 @@ "coordinates": [-1.318375372111463, 48.64794139348595] }, "results": { - "potential": [11547.005383792515, 0.0] + "voltage": [20000.0, 0.0] } }, { @@ -19,7 +19,7 @@ "coordinates": [-1.320149235966572, 48.64971306653889] }, "results": { - "potential": [11546.918780602613, 0.0] + "voltage": [19999.84999887499, 0.0] } } ], @@ -54,7 +54,7 @@ "power": [300, 0], "results": { "current": [0.008660318990723963, -0.0], - "potential": [11546.918780602613, 0.0] + "voltage": [19999.84999887499, 0.0] } } ], @@ -65,7 +65,7 @@ "voltage": [20000.0, 0.0], "results": { "current": [-0.008660318990236813, 0.0], - "potential": [11547.005383792515, 0.0] + "voltage": [20000.0, 0.0] } } ], diff --git a/roseau/load_flow_single/tests/test_electrical_network.py b/roseau/load_flow_single/tests/test_electrical_network.py index 7f1a0a6..44caad9 100644 --- a/roseau/load_flow_single/tests/test_electrical_network.py +++ b/roseau/load_flow_single/tests/test_electrical_network.py @@ -125,7 +125,7 @@ def test_connect_and_disconnect(): def test_recursive_connect_disconnect(): - vn = 400 / np.sqrt(3) + vn = 400 source_bus = Bus(id="source") load_bus = Bus(id="load bus") VoltageSource(id="vs", bus=source_bus, voltage=vn) @@ -269,7 +269,7 @@ def test_bad_networks(): load_bus = Bus(id="lb") lp = LineParameters(id="test", z_line=1.0) line = Line(id="ln", bus1=src_bus, bus2=load_bus, parameters=lp, length=10) - vs = VoltageSource(id="vs", bus=src_bus, voltage=230) + vs = VoltageSource(id="vs", bus=src_bus, voltage=400) load = PowerLoad(id="pl", bus=load_bus, power=1000) with pytest.raises(RoseauLoadFlowException) as e: ElectricalNetwork( @@ -314,7 +314,7 @@ def test_invalid_element_overrides(): bus2 = Bus(id="bus2") lp = LineParameters(id="lp", z_line=1.0) Line(id="line", bus1=bus1, bus2=bus2, parameters=lp, length=1) - VoltageSource(id="source", bus=bus1, voltage=230) + VoltageSource(id="source", bus=bus1, voltage=400) old_load = PowerLoad(id="load", bus=bus2, power=1000) ElectricalNetwork.from_element(bus1) @@ -333,7 +333,7 @@ def test_invalid_element_overrides(): # Case of a source (also suggests disconnecting first) with pytest.raises(RoseauLoadFlowException) as e: - VoltageSource(id="source", bus=bus2, voltage=230) + VoltageSource(id="source", bus=bus2, voltage=400) assert e.value.code == RoseauLoadFlowExceptionCode.BAD_ELEMENT_OBJECT assert e.value.msg == ( "A source of ID 'source' is already connected to the network. Disconnect the old source first " @@ -809,10 +809,10 @@ def test_load_flow_results_frames(small_network_with_results): expected_res_lines_records = [ { "line_id": "line", - "current1": 0.008660318990723968, - "current2": -0.008660318990723968, - "power1": (20000 + 2.89120e-18j) / np.sqrt(3.0) * (0.008660318990723968 + 7.22799e-25j).conjugate() * 3.0, - "power2": 19999.94999 / np.sqrt(3.0) * (-0.008660318990723968 - 7.22799e-25j).conjugate() * 3.0, + "current1": 0.008660318990723968 + 7.22799e-25j, + "current2": -0.008660318990723968 - 7.22799e-25j, + "power1": (20000 + 2.89120e-18j) * (0.008660318990723968 + 7.22799e-25j).conjugate() * np.sqrt(3.0), + "power2": 19999.94999 * (-0.008660318990723968 - 7.22799e-25j).conjugate() * np.sqrt(3.0), "voltage1": 20000 + 2.89120e-18j, "voltage2": 19999.94999 + 2.89119e-18j, "series_losses": (0.002250033750656307 + 0j), @@ -963,58 +963,58 @@ def test_solver_warm_start(small_network: ElectricalNetwork, monkeypatch): load: PowerLoad = small_network.loads["load"] load_bus = small_network.buses["bus1"] - original_propagate_potentials = small_network._propagate_potentials + original_propagate_voltages = small_network._propagate_voltages original_reset_inputs = small_network._reset_inputs - def _propagate_potentials(): - nonlocal propagate_potentials_called - propagate_potentials_called = True - return original_propagate_potentials() + def _propagate_voltages(): + nonlocal propagate_voltages_called + propagate_voltages_called = True + return original_propagate_voltages() def _reset_inputs(): nonlocal reset_inputs_called reset_inputs_called = True return original_reset_inputs() - monkeypatch.setattr(small_network, "_propagate_potentials", _propagate_potentials) + monkeypatch.setattr(small_network, "_propagate_voltages", _propagate_voltages) monkeypatch.setattr(small_network, "_reset_inputs", _reset_inputs) monkeypatch.setattr(small_network._solver, "solve_load_flow", lambda *_, **__: (1, 1e-20)) # First case: network is valid, no results yet -> no warm start - propagate_potentials_called = False + propagate_voltages_called = False reset_inputs_called = False assert small_network._valid assert not small_network._results_valid # Results are not valid by default with warnings.catch_warnings(): warnings.simplefilter("error") # Make sure there is no warning small_network.solve_load_flow(warm_start=True) - assert not propagate_potentials_called # Is not called because it was already called in the constructor + assert not propagate_voltages_called # Is not called because it was already called in the constructor assert not reset_inputs_called # Second case: the user requested no warm start (even though the network and results are valid) - propagate_potentials_called = False + propagate_voltages_called = False reset_inputs_called = False assert small_network._valid assert small_network._results_valid with warnings.catch_warnings(): warnings.simplefilter("error") # Make sure there is no warning small_network.solve_load_flow(warm_start=False) - assert not propagate_potentials_called + assert not propagate_voltages_called assert reset_inputs_called # Third case: network is valid, results are valid -> warm start - propagate_potentials_called = False + propagate_voltages_called = False reset_inputs_called = False assert small_network._valid assert small_network._results_valid with warnings.catch_warnings(): warnings.simplefilter("error") # Make sure there is no warning small_network.solve_load_flow(warm_start=True) - assert not propagate_potentials_called + assert not propagate_voltages_called assert not reset_inputs_called # Fourth case (load powers changes): network is valid, results are not valid -> warm start - propagate_potentials_called = False + propagate_voltages_called = False reset_inputs_called = False load.power = load.power + Q_(1 + 1j, "VA") assert small_network._valid @@ -1022,11 +1022,11 @@ def _reset_inputs(): with warnings.catch_warnings(): warnings.simplefilter("error") # Make sure there is no warning small_network.solve_load_flow(warm_start=True) - assert not propagate_potentials_called + assert not propagate_voltages_called assert not reset_inputs_called # Fifth case: network is not valid -> no warm start - propagate_potentials_called = False + propagate_voltages_called = False reset_inputs_called = False new_load = PowerLoad("new_load", load_bus, power=100) assert new_load.network is small_network @@ -1037,11 +1037,11 @@ def _reset_inputs(): # but this will be disruptive for the user especially that warm start is the default warnings.simplefilter("error") # Make sure there is no warning small_network.solve_load_flow(warm_start=True) - assert propagate_potentials_called + assert propagate_voltages_called assert not reset_inputs_called -def test_propagate_potentials(): +def test_propagate_voltages(): # Delta source source_bus = Bus(id="source_bus") _ = VoltageSource(id="source", bus=source_bus, voltage=20e3) @@ -1053,9 +1053,9 @@ def test_propagate_potentials(): _ = ElectricalNetwork.from_element(source_bus) assert load_bus._initialized assert source_bus._initialized - expected_potentials = 20e3 / np.sqrt(3.0) - assert np.allclose(load_bus.potential.m, expected_potentials) - assert np.allclose(source_bus.potential.m, expected_potentials) + expected_voltages = 20e3 + assert np.allclose(load_bus.initial_voltage.m, expected_voltages) + assert np.allclose(source_bus.initial_voltage.m, expected_voltages) def test_to_graph(small_network: ElectricalNetwork): @@ -1121,9 +1121,9 @@ def assert_results(en_dict: dict, included: bool): en_dict_without_results = en.to_dict(include_results=False) assert_results(en_dict_with_results, included=False) assert_results(en_dict_without_results, included=False) - assert en_dict_with_results == en_dict_without_results + assert_json_close(en_dict_with_results, en_dict_without_results) new_en = ElectricalNetwork.from_dict(en_dict_without_results) - assert new_en.to_dict() == en_dict_without_results + assert_json_close(new_en.to_dict(), en_dict_without_results) # Has results: include_results is respected en = all_elements_network_with_results @@ -1133,10 +1133,10 @@ def assert_results(en_dict: dict, included: bool): assert_results(en_dict_without_results, included=False) assert en_dict_with_results != en_dict_without_results # round tripping - assert ElectricalNetwork.from_dict(en_dict_with_results).to_dict() == en_dict_with_results - assert ElectricalNetwork.from_dict(en_dict_without_results).to_dict() == en_dict_without_results + assert_json_close(ElectricalNetwork.from_dict(en_dict_with_results).to_dict(), en_dict_with_results) + assert_json_close(ElectricalNetwork.from_dict(en_dict_without_results).to_dict(), en_dict_without_results) # default is to include the results - assert en.to_dict() == en_dict_with_results + assert_json_close(en.to_dict(), en_dict_with_results) # Has invalid results: cannot include them en.loads["load0"].power += Q_(1, "VA") # <- invalidate the results @@ -1169,8 +1169,8 @@ def test_results_to_dict(all_elements_network_with_results): assert isinstance(v, list) for res_bus in res_network["buses"]: bus = en.buses[res_bus["id"]] - complex_potentials = res_bus["potential"][0] + 1j * res_bus["potential"][1] - np.testing.assert_allclose(complex_potentials, bus._res_potential) + complex_voltages = res_bus["voltage"][0] + 1j * res_bus["voltage"][1] + np.testing.assert_allclose(complex_voltages, bus._res_voltage) for res_line in res_network["lines"]: line = en.lines[res_line["id"]] complex_currents1 = res_line["current1"][0] + 1j * res_line["current1"][1] @@ -1216,8 +1216,8 @@ def test_results_to_dict_full(all_elements_network_with_results): assert isinstance(v, list) for res_bus in res_network["buses"]: bus = en.buses[res_bus["id"]] - complex_potentials = res_bus["potential"][0] + 1j * res_bus["potential"][1] - np.testing.assert_allclose(complex_potentials, bus._res_potential) + complex_voltages = res_bus["voltage"][0] + 1j * res_bus["voltage"][1] + np.testing.assert_allclose(complex_voltages, bus._res_voltage) complex_voltages = res_bus["voltage"][0] + 1j * res_bus["voltage"][1] np.testing.assert_allclose(complex_voltages, bus.res_voltage.m) for res_line in res_network["lines"]: diff --git a/roseau/load_flow_single/tests/test_import.py b/roseau/load_flow_single/tests/test_import.py index d02f99d..4260102 100644 --- a/roseau/load_flow_single/tests/test_import.py +++ b/roseau/load_flow_single/tests/test_import.py @@ -5,7 +5,7 @@ def test_import(): # Ensure that RLF and RLFS have nearly the same interface rlf_dir = set(dir(rlf)) - rlfs_dir = set(dir(rlfs)) + rlfs_dir = set(dir(rlfs)) - {"_constants"} assert rlf_dir - rlfs_dir == { # Multi-phase elements