Skip to content
Open
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
39 changes: 37 additions & 2 deletions edisgo/flex_opt/reinforce_grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ def reinforce_grid(
mode: str | None = None,
without_generator_import: bool = False,
n_minus_one: bool = False,
crosssection_escalation: bool = False,
crosssection_escalation_existing: bool = False,
**kwargs,
) -> Results:
"""
Expand Down Expand Up @@ -82,6 +84,29 @@ def reinforce_grid(
Determines whether n-1 security should be checked. Currently, n-1 security
cannot be handled correctly, wherefore the case where this parameter is set to
True will lead to an error being raised. Default: False.
crosssection_escalation : bool
If True, only affects LV lines where more than two parallel standard lines
would otherwise be installed due to overloading. For those lines, a larger
LV cross-section (up to two parallel cables) is tried first via
:func:`~.tools.tools.select_cable`, never a cross-section below the
standard type. Falls back to today's parallel-standard-line behaviour if
no cross-section up to two parallel cables can carry the required apparent
power. MV lines are not affected, regardless of this parameter's value.
Note: cable costs in this package do not depend on cross-section (only on
length x quantity, see `costs.py`), so an escalated cross-section is never
more expensive in eDisGo's current cost model than the parallel lines it
replaces.
Default: False.
crosssection_escalation_existing : bool
If True, ALSO considers LV lines that are already the standard cross-
section for escalation, not only lines that would otherwise be
replaced by parallel standard lines. Requires
`crosssection_escalation=True`. Unlike `crosssection_escalation` alone,
this can replace cross-sections that were part of the original input
topology (e.g. native multi-cable ding0 lines), not only eDisGo's own
prior reinforcement decisions -- see PR description for why this is
called out separately.
Default: False.

Other Parameters
-----------------
Expand Down Expand Up @@ -144,6 +169,12 @@ def reinforce_grid(
if n_minus_one is True:
raise NotImplementedError("n-1 security can currently not be checked.")

if crosssection_escalation_existing and not crosssection_escalation:
raise ValueError(
"crosssection_escalation_existing=True requires "
"crosssection_escalation=True."
)

# check if provided mode is valid
if mode and mode not in ["mv", "mvlv", "lv"]:
raise ValueError(f"Provided mode {mode} is not a valid mode.")
Expand Down Expand Up @@ -279,7 +310,9 @@ def reinforce_grid(
if not crit_lines.empty:
# reinforce lines
lines_changes = reinforce_measures.reinforce_lines_overloading(
edisgo, crit_lines
edisgo, crit_lines,
crosssection_escalation=crosssection_escalation,
crosssection_escalation_existing=crosssection_escalation_existing,
)
# write changed lines to results.equipment_changes
_add_lines_changes_to_equipment_changes(
Expand Down Expand Up @@ -603,7 +636,9 @@ def reinforce_grid(
if not crit_lines.empty:
# reinforce lines
lines_changes = reinforce_measures.reinforce_lines_overloading(
edisgo, crit_lines
edisgo, crit_lines,
crosssection_escalation=crosssection_escalation,
crosssection_escalation_existing=crosssection_escalation_existing,
)
# write changed lines to results.equipment_changes
_add_lines_changes_to_equipment_changes(
Expand Down
152 changes: 145 additions & 7 deletions edisgo/flex_opt/reinforce_measures.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,13 @@
_dijkstra as dijkstra_shortest_path_length,
)

from edisgo.flex_opt import exceptions
from edisgo.network.grids import LVGrid, MVGrid
from edisgo.tools.tools import get_downstream_buses
from edisgo.tools.tools import (
calculate_apparent_power,
get_downstream_buses,
select_cable,
)

if TYPE_CHECKING:
from edisgo import EDisGo
Expand Down Expand Up @@ -554,7 +559,10 @@ def reinforce_lines_voltage_issues(edisgo_obj, grid, crit_nodes):
return lines_changes


def reinforce_lines_overloading(edisgo_obj, crit_lines):
def reinforce_lines_overloading(
edisgo_obj, crit_lines, crosssection_escalation=False,
crosssection_escalation_existing=False,
):
"""
Reinforce lines in MV and LV topology due to overloading.

Expand All @@ -571,6 +579,18 @@ def reinforce_lines_overloading(edisgo_obj, crit_lines):
time step the over-loading occured in as
:pandas:`pandas.Timestamp<Timestamp>`, and 'voltage_level' specifying
the voltage level the line is in (either 'mv' or 'lv').
crosssection_escalation : bool
If True, only affects LV lines where more than two parallel standard lines
would otherwise be installed. See
:func:`~.flex_opt.reinforce_grid.reinforce_grid` for the full parameter
description and the cost-model caveat in `Notes` there.
Default: False.
crosssection_escalation_existing : bool
If True, also attempts escalation for LV lines that are already the
standard cross-section. Requires `crosssection_escalation=True`. See
:func:`~.flex_opt.reinforce_grid.reinforce_grid` for the full
parameter description.
Default: False.

Returns
-------
Expand All @@ -593,11 +613,19 @@ def reinforce_lines_overloading(edisgo_obj, crit_lines):
lines_changes = {}
# reinforce mv lines
lines_changes.update(
_reinforce_lines_overloading_per_grid_level(edisgo_obj, "mv", crit_lines)
_reinforce_lines_overloading_per_grid_level(
edisgo_obj, "mv", crit_lines,
crosssection_escalation=crosssection_escalation,
crosssection_escalation_existing=crosssection_escalation_existing,
)
)
# reinforce lv lines
lines_changes.update(
_reinforce_lines_overloading_per_grid_level(edisgo_obj, "lv", crit_lines)
_reinforce_lines_overloading_per_grid_level(
edisgo_obj, "lv", crit_lines,
crosssection_escalation=crosssection_escalation,
crosssection_escalation_existing=crosssection_escalation_existing,
)
)

if not crit_lines.empty:
Expand All @@ -609,7 +637,10 @@ def reinforce_lines_overloading(edisgo_obj, crit_lines):
return lines_changes


def _reinforce_lines_overloading_per_grid_level(edisgo_obj, voltage_level, crit_lines):
def _reinforce_lines_overloading_per_grid_level(
edisgo_obj, voltage_level, crit_lines, crosssection_escalation=False,
crosssection_escalation_existing=False,
):
"""
Reinforce lines in MV or LV topology due to overloading.

Expand Down Expand Up @@ -702,6 +733,61 @@ def _replace_by_parallel_standard_lines(lines):
# save old nominal power to calculate number of parallel standard lines
s_nom_old = edisgo_obj.topology.lines_df.loc[lines, "s_nom"]

# crosssection_escalation: only for lines that would otherwise need MORE
# THAN TWO parallel standard lines -- determine this without touching the
# topology yet, then only attempt select_cable() for those. LV only, see
# reinforce_grid() docstring for the MV follow-up and the cost-model
# caveat (cable costs here do not depend on cross-section, only on
# length x quantity, see costs.py -- an escalated cross-section is
# therefore never more expensive in this cost model).
if crosssection_escalation and voltage_level == "lv":
standard_type_data = edisgo_obj.topology.equipment_data["lv_cables"].loc[
standard_line_type
]
standard_single_capacity = calculate_apparent_power(
standard_type_data.U_n, standard_type_data.I_max_th, 1
)
number_parallel_lines_standard = np.ceil(
s_nom_old
* crit_lines.loc[lines, "max_rel_overload"]
/ standard_single_capacity
)
escalation_candidates = number_parallel_lines_standard[
number_parallel_lines_standard > 2
].index

fallback_lines = []
for line in escalation_candidates:
apparent_power = (
s_nom_old.at[line] * crit_lines.at[line, "max_rel_overload"]
)
try:
cable_type, n_new = select_cable(
edisgo_obj, level="lv", apparent_power=apparent_power,
max_cables=2,
)
except exceptions.MaximumIterationError:
fallback_lines.append(line)
continue
if cable_type.I_max_th < standard_type_data.I_max_th:
# defensive only -- see PR description; mathematically this
# branch should be unreachable given escalation_candidates
# already fail at n<=2 with the standard type. Never
# downgrade below the standard type.
fallback_lines.append(line)
continue
edisgo_obj.topology.change_line_type([line], cable_type.name)
edisgo_obj.topology.update_number_of_parallel_lines(
pd.Series({line: n_new})
)
lines_changes[line] = n_new

successfully_escalated = escalation_candidates.difference(fallback_lines)
lines = pd.Index(lines).difference(successfully_escalated)
s_nom_old = s_nom_old.loc[lines]
if lines.empty:
return

# change line type to standard line
edisgo_obj.topology.change_line_type(lines, standard_line_type)

Expand Down Expand Up @@ -738,11 +824,63 @@ def _replace_by_parallel_standard_lines(lines):
lines_standard = relevant_lines.loc[
relevant_lines.type_info == standard_line_type
]
lines_standard_escalated: list = []
if not lines_standard.empty:
_add_parallel_standard_lines(lines_standard.index)
# crosssection_escalation_existing: consider already-standard-type
# lines for escalation too, not only lines reaching
# _replace_by_parallel_standard_lines() below. LV only (see
# reinforce_grid() docstring); no separate voltage-path guard
# needed here -- this whole function tree is structurally
# unreachable from reinforce_lines_voltage_issues() (see PR
# description, Command D2d 1c/2d). Deliberately duplicates the
# small escalation-attempt loop from _replace_by_parallel_
# standard_lines() below rather than refactoring that
# already-tested path -- see PR description.
if (
crosssection_escalation
and crosssection_escalation_existing
and voltage_level == "lv"
):
standard_type_data = edisgo_obj.topology.equipment_data[
"lv_cables"
].loc[standard_line_type]
number_needed = np.ceil(
crit_lines.max_rel_overload[lines_standard.index]
* lines_standard["num_parallel"]
)
escalation_candidates = number_needed[number_needed > 2].index

for line in escalation_candidates:
apparent_power = (
edisgo_obj.topology.lines_df.at[line, "s_nom"]
* crit_lines.at[line, "max_rel_overload"]
)
try:
cable_type, n_new = select_cable(
edisgo_obj, level="lv", apparent_power=apparent_power,
max_cables=2,
)
except exceptions.MaximumIterationError:
continue
if cable_type.I_max_th < standard_type_data.I_max_th:
# defensive only, unreachable by construction -- same
# argument as _replace_by_parallel_standard_lines()
continue
edisgo_obj.topology.change_line_type([line], cable_type.name)
edisgo_obj.topology.update_number_of_parallel_lines(
pd.Series({line: n_new})
)
lines_changes[line] = n_new
lines_standard_escalated.append(line)

lines_standard_remaining = lines_standard.loc[
~lines_standard.index.isin(lines_standard_escalated)
]
if not lines_standard_remaining.empty:
_add_parallel_standard_lines(lines_standard_remaining.index)

# get lines that have not been updated yet (i.e. that are not standard
# lines)
# lines, or were already escalated above)
relevant_lines = relevant_lines.loc[
~relevant_lines.index.isin(lines_standard.index)
]
Expand Down
Loading
Loading