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
25 changes: 25 additions & 0 deletions doc/userguide/time_series.rst
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,31 @@ which time step maps to which case. Note that this **overwrites** any previously
(non-worst-case) time series. The definition of load and feed-in case is explained in
:ref:`load-feedin-case`.

**Simultaneity factors**

Conventional loads and generators use fixed scale factors from the config file
:ref:`config_timeseries` in section ``worst_case_scale_factor``.

For **charging points (CP)** in the load case, the simultaneity factor is determined
dynamically based on the number of charging points *n* and their nominal charging power.
Three power classes are supported: 3.7 kW (slow AC), 11 kW (fast AC) and 22 kW (fast AC).
Support points are taken from VDE FNN (2021): *Ermittlung von Gleichzeitigkeitsfaktoren
fuer Ladevorgaenge an privaten Ladepunkten* (suburban residential evening scenario,
conservative worst case). Interpolation is performed in log-space over *n*; for *n > 150*
the factor is clamped to the value at *n = 150*. In the feed-in case the CP factor is
always 0.0 (no reverse power flow from charging assumed), consistent with the dena study
*Integrierte Energiewende* (p. 90).

For **heat pumps (HP)** in the load case, the simultaneity factor depends only on the
number of heat pumps *n*. Support points are derived from the Kerber simultaneity
function *g(n) = g_inf + (1 − g_inf) · n^(−3/4)* (Kerber 2011, eq. 3.2) with an
asymptote *g_inf = 0.9* based on the Consentec/E.ON distribution network study (peak
load cold winter day). The same log-space interpolation and clamping scheme as for
charging points is applied. In the feed-in case the HP factor is always 0.0.

Support point values for both CP and HP are defined in the config file
:ref:`config_timeseries` in section ``simultaneity_curves``.

Predefined
~~~~~~~~~~

Expand Down
26 changes: 26 additions & 0 deletions edisgo/config/config_timeseries_default.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,32 @@ lv_feed-in_case_hp = 0.0
mv_load_case_hp = 0.8
lv_load_case_hp = 1.0

[simultaneity_curves]
# Support points for count- and charging-power-dependent simultaneity factors
# for charging points (CP) in the load case.
# Source: VDE FNN (2021): "Ermittlung von Gleichzeitigkeitsfaktoren fuer
# Ladevorgaenge an privaten Ladepunkten" (Determination of simultaneity factors
# for charging processes at private charging points), planning guide.
# Chosen scenario: suburban, residential area, evening (highest factor = conservative worst case).
# Interpolation over n in log-space. For n > 150 the factor is clamped to the value at n = 150,
# as the VDE FNN study covers only up to n = 150.
cp_simultaneity_n = 1, 5, 10, 20, 50, 150
cp_simultaneity_3.7kW = 1.0, 1.0, 0.8, 0.55, 0.4, 0.313
cp_simultaneity_11kW = 1.0, 0.8, 0.6, 0.35, 0.26, 0.173
cp_simultaneity_22kW = 1.0, 0.8, 0.5, 0.3, 0.18, 0.12
cp_simultaneity_power_classes = 3.7, 11, 22

# ---------------------------------------------------------------------------
# Heat pumps (HP): count-dependent simultaneity factor (one-dimensional).
# As no equivalent tabular source to the VDE FNN EV study exists for heat pumps,
# the support points were derived from the Kerber simultaneity function:
# g(n) = g_inf + (1 - g_inf) * n^(-3/4) (Kerber 2011, eq. 3.2, p. 23)
# HP asymptote g_inf = 0.9 (based on Consentec/E.ON distribution network study,
# peak load cold winter day). Same support point grid and clamping
# (n > 150 -> factor at n = 150) as for charging points.
hp_simultaneity_n = 1, 5, 10, 20, 50, 150
hp_simultaneity_factor = 1.0, 0.93, 0.92, 0.91, 0.9, 0.9
Comment thread
nader-00 marked this conversation as resolved.

[reactive_power_factor]

# power factors
Expand Down
206 changes: 200 additions & 6 deletions edisgo/network/timeseries.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,161 @@
logger = logging.getLogger(__name__)


def _parse_float_list(value: str) -> np.ndarray:
"""
Parse a comma-separated config string into a float array.

Used to read simultaneity curve support points (``n`` and factor values)
from the ``[simultaneity_curves]`` config section, where eDisGo's config
parser keeps comma-separated values as raw strings.

Parameters
----------
value : str
Comma-separated string, e.g. ``"1, 5, 10, 20, 50, 150"``.

Returns
-------
:numpy:`numpy.ndarray<ndarray>`
Parsed float values.
"""
return np.array([float(x.strip()) for x in value.split(",")])


def _interp_log_n(n: int, ns: np.ndarray, factors: np.ndarray) -> float:
"""
Interpolate a simultaneity factor in log-space over device count ``n``.

Shared by :func:`_cp_simultaneity_factor` and
:func:`_hp_simultaneity_factor`. Interpolation is performed on
``log(n)`` because the underlying support points are logarithmically
spaced and simultaneity factors decay roughly logarithmically with
device count. ``numpy.interp`` clamps to ``factors[0]`` / ``factors[-1]``
for ``n`` outside the range of ``ns`` (no extrapolation).

Parameters
----------
n : int
Device count to interpolate at. Must be >= 1.
ns : :numpy:`numpy.ndarray<ndarray>`
Support points for device count (ascending).
factors : :numpy:`numpy.ndarray<ndarray>`
Simultaneity factor at each support point in `ns`.

Returns
-------
float
Interpolated (or clamped) simultaneity factor.
"""
return float(np.interp(np.log(n), np.log(ns), factors))


def _cp_simultaneity_factor(n: int, charging_power_kw: float, config) -> float:
"""
Return the simultaneity factor for *n* charging points at a given nominal
charging power.

Simultaneity factors are read from the ``[simultaneity_curves]`` section of
the eDisGo config and express the ratio of peak collective demand to the sum
of individual nominal powers as a function of device count.

Interpolation is performed in log-space (``numpy.interp`` on ``log(n)``).
Clamping: for ``n`` below the smallest support point the factor of that
point is used; for ``n`` above the largest support point (n = 150 in the
default config) the factor at that point is used. This matches the
validity range of the source data.

Source
------
VDE FNN (2021): "Ermittlung von Gleichzeitigkeitsfaktoren für Ladevorgänge
an privaten Ladepunkten", Planungshilfe. Chosen scenario: suburban
residential area, evening peak (highest simultaneity = conservative worst
case).

Parameters
----------
n : int
Number of charging points in the group (same voltage level and use
case). Must be >= 1.
charging_power_kw : float
Nominal charging power per charging point in **kW**. The nearest
available power class from ``cp_simultaneity_power_classes`` is
selected.
config : :class:`~edisgo.tools.config.Config`
eDisGo configuration object containing section
``simultaneity_curves`` with keys ``cp_simultaneity_n``,
``cp_simultaneity_power_classes``, and one curve entry per power class
(e.g. ``cp_simultaneity_11kw``).

Returns
-------
float
Simultaneity factor in [0, 1].
"""
curves = config["simultaneity_curves"]

ns = _parse_float_list(curves["cp_simultaneity_n"])
power_classes = _parse_float_list(curves["cp_simultaneity_power_classes"])

nearest_class = power_classes[np.argmin(np.abs(power_classes - charging_power_kw))]
key = f"cp_simultaneity_{nearest_class:g}kw"
factors = _parse_float_list(curves[key])

return _interp_log_n(n, ns, factors)


def _hp_simultaneity_factor(n: int, config) -> float:
"""
Return the simultaneity factor for *n* heat pumps.

One-dimensional analogue of :func:`_cp_simultaneity_factor`: heat pumps
have no equivalent of a charging-power class, so the factor depends only
on device count. Simultaneity factors are read from the
``[simultaneity_curves]`` section of the eDisGo config and express the
ratio of peak collective demand to the sum of individual nominal powers
as a function of device count.

Interpolation is performed in log-space (``numpy.interp`` on ``log(n)``),
using the same method as for charging points. Clamping: for ``n`` below
the smallest support point the factor of that point is used; for ``n``
above the largest support point (n = 150 in the default config) the
factor at that point is used (no extrapolation).

Source
------
Support points are derived from the Kerber simultaneity function::

g(n) = g_inf + (1 - g_inf) * n ** (-3 / 4)

(Kerber, G. (2011): "Aufnahmefähigkeit von Niederspannungsverteilnetzen
für die Einspeisung aus Photovoltaikkleinanlagen", TU München, eq. 3.2,
p. 23). The asymptotic value ``g_inf = 0.9`` is based on the
Consentec/E.ON distribution grid study assumption for the peak load case
on a cold winter day.

Parameters
----------
n : int
Number of heat pumps in the group (same voltage level). Must be
>= 1.
config : :class:`~edisgo.tools.config.Config`
eDisGo configuration object containing section
``simultaneity_curves`` with keys ``hp_simultaneity_n`` and
``hp_simultaneity_factor``.

Returns
-------
float
Simultaneity factor in [0, 1].
"""
curves = config["simultaneity_curves"]

ns = _parse_float_list(curves["hp_simultaneity_n"])
factors = _parse_float_list(curves["hp_simultaneity_factor"])

return _interp_log_n(n, ns, factors)


class TimeSeries:
"""
Holds component-specific active and reactive power time series.
Expand Down Expand Up @@ -996,9 +1151,32 @@ def _worst_case_charging_points(self, cases, df, configs):
for s in sectors:
for case in cases:
for voltage_level in ["mv", "lv"]:
power_scaling.at[f"{case}_{voltage_level}", s] = (
worst_case_scale_factors[f"{voltage_level}_{case}_cp_{s}"]
)
if case == "feed-in_case":
# feed-in case unchanged: assumed 0.0 per config
power_scaling.at[f"{case}_{voltage_level}", s] = (
worst_case_scale_factors[f"{voltage_level}_{case}_cp_{s}"]
)
else:
# load_case: simultaneity factor depends on group size and
# mean charging power (source: VDE FNN 2021).
group = df[
(df.voltage_level == voltage_level) & (df.sector == s)
]
n = len(group)
if n == 0:
# 0.0 here means "no charging point exists for this
# voltage_level/sector combination", not the
# feed-in case's deliberately-zero config factor
# above; this is just an empty-group placeholder.
power_scaling.at[f"{case}_{voltage_level}", s] = 0.0
else:
# For mixed-power groups the mean p_set is used for
# power-class selection (VDE FNN 2021, simplified
# assignment over mean charging power).
charging_power_kw = group["p_set"].mean() * 1000
power_scaling.at[f"{case}_{voltage_level}", s] = (
_cp_simultaneity_factor(n, charging_power_kw, configs)
)

# calculate active power of charging points
active_power = pd.concat(
Expand Down Expand Up @@ -1082,9 +1260,25 @@ def _worst_case_heat_pumps(self, cases, df, configs):
power_scaling = pd.Series(dtype=float)
for case in cases:
for voltage_level in ["mv", "lv"]:
power_scaling.at[f"{case}_{voltage_level}"] = worst_case_scale_factors[
f"{voltage_level}_{case}_hp"
]
if case == "feed-in_case":
# feed-in case unchanged: assumed 0.0 per config
power_scaling.at[f"{case}_{voltage_level}"] = (
worst_case_scale_factors[f"{voltage_level}_{case}_hp"]
)
else:
# load_case: simultaneity factor depends on group size
# (source: Kerber 2011 / Consentec, see _hp_simultaneity_factor).
n = len(df[df.voltage_level == voltage_level])
if n == 0:
# 0.0 here means "no heat pump exists in this
# voltage_level", not the feed-in case's
# deliberately-zero config factor above; this is just
# an empty-group placeholder.
power_scaling.at[f"{case}_{voltage_level}"] = 0.0
else:
power_scaling.at[f"{case}_{voltage_level}"] = (
_hp_simultaneity_factor(n, configs)
)

# calculate active power of heat pumps
active_power = power_scaling.to_frame("p_set").dot(df.loc[:, ["p_set"]].T)
Expand Down
Loading
Loading