Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove potential and use voltage everywhere #6

Merged
merged 3 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions roseau/load_flow_single/_constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import math
from typing import Final

SQRT3: Final = math.sqrt(3)
39 changes: 12 additions & 27 deletions roseau/load_flow_single/models/branches.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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,))
Expand All @@ -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)

#
Expand All @@ -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:
Expand Down
70 changes: 33 additions & 37 deletions roseau/load_flow_single/models/buses.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand All @@ -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.

Expand All @@ -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 <roseau.load_flow.units.Q_>` 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
Expand All @@ -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,))
Expand Down Expand Up @@ -354,26 +356,26 @@ 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

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:
Expand All @@ -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
11 changes: 6 additions & 5 deletions roseau/load_flow_single/models/flexible_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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,
)
Expand Down Expand Up @@ -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)
6 changes: 3 additions & 3 deletions roseau/load_flow_single/models/line_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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"``
Expand Down
38 changes: 17 additions & 21 deletions roseau/load_flow_single/models/lines.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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,))
Expand All @@ -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:
Expand All @@ -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,))
Expand Down Expand Up @@ -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)
Expand Down
Loading
Loading