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

384 bug voltage drop should be considered when selecting cable to connect new components #411

Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f9972bc
Update select_cable to consider voltage drop constraints
joda9 Jul 22, 2024
3dc762b
adding test for cable voltage drop
joda9 Jul 22, 2024
9d0411a
Adding default values for adding new components
joda9 Jul 22, 2024
b8a8baa
Update default voltage deviation values for new components
joda9 Jul 22, 2024
bab29bc
Update default voltage deviation values for new components
joda9 Jul 24, 2024
f31886a
Refactor calculate_voltage_drop function for clarity and readability
joda9 Jul 24, 2024
8adc372
update tests select cables
joda9 Jul 24, 2024
a37c4d5
use "difference" instead of "drop" in the variable names
joda9 Jul 24, 2024
1b2301a
source of formula added
joda9 Jul 25, 2024
a0f01fb
specified source for values
joda9 Jul 30, 2024
3a23e5e
update tools to consistency variable names and minor fixes
joda9 Jul 30, 2024
6e3cd06
adding tests for cable_selection
joda9 Jul 30, 2024
8404748
adding component type to select cable function
joda9 Aug 2, 2024
f1a02e2
adding component type to tests
joda9 Aug 2, 2024
9c9f456
defining return values
joda9 Aug 5, 2024
bd9c87e
change name voltage_diff_pu
joda9 Aug 5, 2024
803d796
adding tests for coverage
joda9 Aug 5, 2024
2115254
Refactor voltage difference calculation to use per unit (pu) instead …
joda9 Aug 5, 2024
e68b601
Refactor voltage difference calculation to use per unit (pu) instead …
joda9 Aug 5, 2024
93e3592
Rename config parameters to make wording more consistent
birgits Aug 7, 2024
59f12c8
Minor docstring fix
birgits Aug 7, 2024
ace46a9
Simplify getting config values
birgits Aug 7, 2024
aa49064
Adapt calculate_voltage_diff_pu_per_line to have q_sign and power_fac…
birgits Aug 8, 2024
0d1683d
Change function calculate_voltage_diff_pu_per_line_from_type to have …
birgits Aug 8, 2024
095e3d0
Change function select_cable to retrieve reactive power behavior from…
birgits Aug 8, 2024
2875aa7
Adapt tests to changes
birgits Aug 8, 2024
ec36c0b
Consider voltage drop when selecting suitable cable in connect functions
birgits Aug 8, 2024
a49d907
Adapt test that is now failing because voltage drop is too high to fi…
birgits Aug 8, 2024
4552729
Add changes to whatsnew
birgits Aug 8, 2024
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
5 changes: 5 additions & 0 deletions edisgo/config/config_grid_default.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ upper_limit_voltage_level_6 = 0.2
upper_limit_voltage_level_5 = 5.5
upper_limit_voltage_level_4 = 20.0

lv_max_voltage_deviation = 0.03
# from VDE-AR-N 4100 (VDE-AR-N 4100) Anwendungsregel: 2019-04
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please add the comment above the value it applies to?

mv_max_voltage_deviation = 0.02
# from VDE-AR-N 4110 (VDE-AR-N 4110) Anwendungsregel: 2023-09

[disconnecting_point]

# Positioning of disconnecting points: Can be position at location of most
Expand Down
168 changes: 159 additions & 9 deletions edisgo/tools/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,109 @@ def drop_duplicated_columns(df, keep="last"):
return df.loc[:, ~df.columns.duplicated(keep=keep)]


def select_cable(edisgo_obj, level, apparent_power):
def calculate_voltage_diff_per_line(
s_max: float | np.ndarray,
r_total: float | np.ndarray,
x_total: float | np.ndarray,
v_nom: float | np.ndarray,
sign: int = -1,
cos_phi: float = 0.95,
) -> float | np.ndarray:
"""
Calculate the voltage difference across a line in kV.

Parameters
----------
s_max : float or array-like
Apparent power the cable must carry in MVA.
r_total : float or array-like
Total resistance in Ohms.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add "of the line" here, as well as for x_total and v_nom.

x_total : float or array-like
Total reactance in Ohms.
v_nom : float or array-like
Nominal voltage in kV.
sign : int, optional
Sign of the reactance. -1 for inductive and +1 for capacitive. Default is -1.
cos_phi : float, optional
Power factor (cosine of the phase angle) of the load or generator.
Default is 0.95.

Returns
-------
float or array-like
Voltage difference in kV.
"""
sin_phi = np.sqrt(1 - cos_phi**2)
# Calculate the voltage difference using the formula from VDE-AR-N 4105
voltage_diff = np.abs(
(s_max * 1e6 / (v_nom * 1e3)) * (r_total * cos_phi + sign * x_total * sin_phi)
Copy link
Collaborator

@birgits birgits Jul 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please avoid multiplying and dividing unnecessarily due to rounding errors. 1e6/1e3/1e3 all cancels out, so it is not necessary here.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, please don't return the absolute value here because it would be better to test, whether the voltage rises or drops.

) # in V
return voltage_diff / 1e3 # Convert to kV


def voltage_diff_pu(
R_per_km: float | np.ndarray,
L_per_km: float | np.ndarray,
length: float,
num_parallel: int,
v_nom: float | np.ndarray,
s_max: float | np.ndarray,
cos_phi: float = 0.95,
sign: int = -1,
) -> float | np.ndarray:
"""
Calculate the voltage difference per unit of nominal voltage.

Parameters
----------
R_per_km : float or array-like
Resistance per kilometer of the cable in ohm/km.
L_per_km : float or array-like
Inductance per kilometer of the cable in mH/km.
length : float
Length of the cable in km.
num_parallel : int
Number of parallel cables.
v_nom : int
Nominal voltage in kV.
s_max : float
Apparent power the cable must carry in MVA.
cos_phi : float, optional
Cosine phi of the load or generator. Default: 0.95.
sign : int, optional
Sign of the reactance. -1 for inductive and +1 for capacitive. Default is -1.

Returns
-------
float
Voltage difference in per unit of nominal voltage.
"""
# Calculate total resistance and reactance for the given length and
# number of parallel cables
r_total = calculate_line_resistance(R_per_km, length, num_parallel)
x_total = calculate_line_reactance(L_per_km, length, num_parallel)

# Calculate the voltage drop or increase
delta_v = calculate_voltage_diff_per_line(
s_max, r_total, x_total, v_nom, sign=sign, cos_phi=cos_phi
)

# Convert voltage difference to per unit of nominal voltage
voltage_difference_pu = delta_v / v_nom

return voltage_difference_pu


def select_cable(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please adapt the docstring of this function. The cable is not only selected based on the apparent power anymore but also considering the voltage deviation over the cable. The docstring should always start with a short one line description and then in the next paragraph it can be elaborated if needed.

edisgo_obj: EDisGo,
level: str,
apparent_power: float,
length: float = 0,
max_voltage_diff: float | None = None,
max_cables: int = 7,
cos_phi: float | None = 0.95,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please rename this parameter to power_factor to have it consistent with the rest of the code base. Also, I would prefer that there is no default value but that the default value is obtained from the configs. For this, the type of the component to be connected needs to be specified as input parameter.

inductive_reactance: bool = True,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use "reactive_power_mode" instead of "inductive_reactance" to have it consistent with the rest of the code base, e.g. here). Also, whether the reactive power is positive or negative in case of inductive behavior depends on the sign convention used. In eDisGo we use the generator sign convention for generators and storage units and the load sign convention for loads. This also needs to be fixed in the other functions.

) -> tuple[pd.Series, int]:
"""
Selects suitable cable type and quantity using given apparent power.

Expand All @@ -209,6 +311,17 @@ def select_cable(edisgo_obj, level, apparent_power):
'lv'.
apparent_power : float
Apparent power the cable must carry in MVA.
length : float
Length of the cable in km. Default: 0.
max_voltage_diff : float
Maximum voltage difference in pu. Default: None.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please elaborate this a bit more. It is the maximum allowed voltage difference over the line that connects the new component to the grid. If it is not specified, it is taken from the config files (section grid_connection, parameters lv_max_voltage_deviation and mv_max_voltage_deviation).

max_cables : int
Maximum number of parallel cables to consider. Default is 7.
cos_phi : float
Cosine phi of the load or generator. Default: 0.95.
inductive_reactance : bool
If True, inductive reactance is considered. Default
is True. If False, capacitive reactance is considered.

Returns
-------
Expand All @@ -219,39 +332,76 @@ def select_cable(edisgo_obj, level, apparent_power):
Number of necessary parallel cables.

"""

cable_count = 1

if not cos_phi:
cos_phi = 0.95
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not necessary because 0.95 is already the default value in the function head.

if inductive_reactance:
sign = -1
else:
sign = 1
if level == "mv":
cable_data = edisgo_obj.topology.equipment_data["mv_cables"]
available_cables = cable_data[
cable_data["U_n"] == edisgo_obj.topology.mv_grid.nominal_voltage
]
if not max_voltage_diff:
max_voltage_diff = edisgo_obj.config["grid_connection"][
"mv_max_voltage_deviation"
]
elif level == "lv":
available_cables = edisgo_obj.topology.equipment_data["lv_cables"]
if not max_voltage_diff:
max_voltage_diff = edisgo_obj.config["grid_connection"][
"lv_max_voltage_deviation"
]
else:
raise ValueError(
"Specified voltage level is not valid. Must either be 'mv' or 'lv'."
)

cable_count = 1
suitable_cables = available_cables[
calculate_apparent_power(
available_cables["U_n"], available_cables["I_max_th"], cable_count
)
> apparent_power
]
if length != 0:
suitable_cables = suitable_cables[
voltage_diff_pu(
R_per_km=available_cables["R_per_km"],
L_per_km=available_cables["L_per_km"],
length=length,
num_parallel=cable_count,
v_nom=available_cables["U_n"],
s_max=apparent_power,
cos_phi=cos_phi,
sign=sign,
)
< max_voltage_diff
]

# increase cable count until appropriate cable type is found
while suitable_cables.empty and cable_count < 7:
while suitable_cables.empty and cable_count < max_cables: # parameter
cable_count += 1
suitable_cables = available_cables[
calculate_apparent_power(
available_cables["U_n"],
available_cables["I_max_th"],
cable_count,
available_cables["U_n"], available_cables["I_max_th"], cable_count
)
> apparent_power
]
if length != 0:
suitable_cables = suitable_cables[
voltage_diff_pu(
R_per_km=available_cables["R_per_km"],
L_per_km=available_cables["L_per_km"],
length=length,
num_parallel=cable_count,
v_nom=available_cables["U_n"],
s_max=apparent_power,
cos_phi=cos_phi,
sign=sign,
)
< max_voltage_diff
]
if suitable_cables.empty:
raise exceptions.MaximumIterationError(
"Could not find a suitable cable for apparent power of "
Expand Down
83 changes: 83 additions & 0 deletions tests/tools/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,56 @@ def test_calculate_line_reactance(self):
data = tools.calculate_line_reactance(np.array([2, 3]), 3, 2)
assert_allclose(data, np.array([1.88496 / 2, 2.82743 / 2]), rtol=1e-5)

def test_voltage_diff(self):
data = tools.calculate_voltage_diff_per_line(50, 0.125, 0.36, 20, -1, 0.9)
correct_value = 0.11105090491866049
assert np.isclose(data, correct_value)
data = tools.calculate_voltage_diff_per_line(
np.array([50, 50]),
np.array([0.125, 0.125]),
np.array([0.36, 0.36]),
20,
-1,
0.9,
)
assert_allclose(data, np.array([correct_value, correct_value]), rtol=1e-5)
data = tools.calculate_voltage_diff_per_line(50, 0.125, 0.36, 40, -1, 0.9)
assert np.isclose(data, correct_value / 2)
data = tools.calculate_voltage_diff_per_line(100, 0.125, 0.36, 20, -1, 0.9)
assert np.isclose(data, correct_value * 2)
data = tools.calculate_voltage_diff_per_line(
np.array([100, 100]),
np.array([0.125, 0.125]),
np.array([0.36, 0.36]),
np.array([20, 20]),
-1,
0.9,
)
assert_allclose(
data, np.array([correct_value * 2, correct_value * 2]), rtol=1e-5
)

def test_voltage_diff_pu(self):
data = tools.voltage_diff_pu(0.1, 0.350, 1, 1, 20, 50, 0.9, -1)
correct_value = 0.52589253567891375 * 1e-2
assert np.isclose(data, correct_value)
data = tools.voltage_diff_pu(
np.array([0.1, 0.1]), np.array([0.35, 0.35]), 1, 1, 20, 50, 0.9, -1
)
assert_allclose(data, np.array([correct_value, correct_value]), rtol=1e-5)
data = tools.voltage_diff_pu(0.1, 0.35, 2, 1, 20, 50, 0.9, -1)
assert np.isclose(data, 2 * correct_value)
data = tools.voltage_diff_pu(
np.array([0.1, 0.1]), np.array([0.35, 0.35]), 2, 1, 20, 50, 0.9, -1
)
assert_allclose(
data,
np.array([2 * correct_value, 2 * correct_value]),
rtol=1e-5,
)
data = tools.voltage_diff_pu(0.1, 0.35, 1, 2, 20, 50, 0.9, -1)
assert np.isclose(data, correct_value / 2)

def test_calculate_line_resistance(self):
# test single line
data = tools.calculate_line_resistance(2, 3, 1)
Expand Down Expand Up @@ -97,6 +147,7 @@ def test_drop_duplicated_columns(self):
assert (check_df.loc[:, "a"] == [4, 5, 6]).all()

def test_select_cable(self):
# no length given
cable_data, num_parallel_cables = tools.select_cable(self.edisgo, "mv", 5.1)
assert cable_data.name == "NA2XS2Y 3x1x150 RE/25"
assert num_parallel_cables == 1
Expand All @@ -109,6 +160,38 @@ def test_select_cable(self):
assert cable_data.name == "NAYY 4x1x150"
assert num_parallel_cables == 1

# length given
cable_data, num_parallel_cables = tools.select_cable(self.edisgo, "mv", 5.1, 2)
assert cable_data.name == "NA2XS2Y 3x1x150 RE/25"
assert num_parallel_cables == 1

cable_data, num_parallel_cables = tools.select_cable(self.edisgo, "mv", 40, 1)
assert cable_data.name == "NA2XS(FL)2Y 3x1x500 RM/35"
assert num_parallel_cables == 2

cable_data, num_parallel_cables = tools.select_cable(self.edisgo, "lv", 0.18, 1)
assert cable_data.name == "NAYY 4x1x300"
assert num_parallel_cables == 3

cable_data, num_parallel_cables = tools.select_cable(
self.edisgo, "lv", 0.18, 1, max_voltage_diff=0.01, max_cables=100
)
assert cable_data.name == "NAYY 4x1x300"
assert num_parallel_cables == 8

cable_data, num_parallel_cables = tools.select_cable(
self.edisgo,
"lv",
0.18,
1,
max_voltage_diff=0.01,
max_cables=100,
cos_phi=1,
inductive_reactance=False,
)
assert cable_data.name == "NAYY 4x1x300"
assert num_parallel_cables == 12

def test_get_downstream_buses(self):
# ######## test with LV bus ########
buses_downstream = tools.get_downstream_buses(
Expand Down
Loading