Add configurable timestep selection for individual eDisGo grids in eGo
Summary
Today, the timeindex analysed for each eDisGo grid in an eGo run is effectively
fixed: it is set inside the oedb_ts task via the timeindex kwarg in the
uc4_example.yaml preset (full year / a fixed range). All older eGo mechanisms
for timestep selection are dead code (see "Background" below).
We want a clean, configurable way to determine the analysed timeindex per grid,
with the selection logic living in eDisGo and the configuration exposed in
eGo. Two selection modes:
manual — an explicitly defined timeindex (as currently done in
uc4_example.yaml), given either as an ISO-8601 list of timesteps or as
start + (periods | end) + freq.
auto — call eDisGo's built-in get_most_critical_time_steps
(edisgo/tools/temporal_complexity_reduction.py:837) to pick the most critical
steps from the full-year series, then reduce all time series to that index.
The eGo configuration must allow:
- setting one timeindex/selection config for all analysed grids, and
- setting an individual selection config per grid.
Background (current state)
- eDisGo built-in reducers available:
get_most_critical_time_steps — flat DatetimeIndex of most critical
individual steps (temporal_complexity_reduction.py:837).
get_most_critical_time_intervals — worst contiguous 168 h weeks
(temporal_complexity_reduction.py:639). Deferred — not needed in v1.
- In eGo,
get_most_critical_time_intervals is imported and defined in
_run_edisgo_task_temporal_complexity_reduction
(eGo/ego/tools/edisgo_integration.py:1487-1684) but is DEAD — its only
caller is the commented-out legacy run_edisgo block.
- All other eGo timestep mechanisms are dead too:
set_timeindex(...) in the
legacy setup task, timeindex rewrite from etrago_obj.snapshots,
reduce_timeseries_data_to_given_timeindex per interval, eTraGo
start_snapshot/end_snapshot, the stale
tasks: ["3_temporal_complexity_reduction", ...] config key, etc.
- The active path is
appl.py → EDisGoNetworks → _run_one_grid_via_runner
→ edisgo.run.run_edisgo(cfg, ...). The cfg from _build_run_edisgo_config
(edisgo_integration.py:718-759) has zero timestep fields; everything is
delegated to the eDisGo runner + the uc4_example preset.
So: timestep selection must be configured on the eDisGo side (preset/runner),
and eGo must learn to inject that config.
Proposed design
eDisGo side — new task select_timesteps
Add a single task in edisgo/run/tasks/timeseries.py (alongside the existing
timeindex/oedb tasks), registered once, that branches on mode and respects an
enabled flag:
@register_task("select_timesteps")
def task_select_timesteps(edisgo, ctx, *, enabled=False, mode="manual",
timesteps=None, start=None, periods=None, end=None, freq="h",
percentage=0.1, num_steps_loading=None, num_steps_voltage=None,
weight_by_costs=True, use_troubleshooting_mode=True,
run_initial_analyze=True, persist=True, reduce_kwargs=None):
if not enabled:
return edisgo
if mode == "manual":
# build ti from timesteps OR start/periods/freq OR start/end/freq
if timesteps is not None:
ti = pd.DatetimeIndex(pd.to_datetime(timesteps))
elif start is not None and end is not None:
ti = pd.date_range(start=start, end=end, freq=freq)
elif start is not None and periods is not None:
ti = pd.date_range(start=start, periods=periods, freq=freq)
else:
raise ValueError("manual: need 'timesteps' or 'start'+('periods'|'end').")
if edisgo.timeseries.timeindex.empty:
# pre-oedb_ts: just set the index; oedb downloads only these steps
edisgo.set_timeindex(ti)
else:
# post-loading: reduce all TS to the chosen subset
reduce_timeseries_data_to_given_timeindex(edisgo, ti, **(reduce_kwargs or {}))
elif mode == "auto":
if edisgo.timeseries.timeindex.empty:
raise ValueError("auto: TS must be loaded earlier in the pipeline.")
# set reactive internally (do NOT flip the reactive_power_set flag)
edisgo.set_time_series_reactive_power_control()
ti = get_most_critical_time_steps(
edisgo, percentage=percentage,
num_steps_loading=num_steps_loading,
num_steps_voltage=num_steps_voltage,
weight_by_costs=weight_by_costs,
use_troubleshooting_mode=use_troubleshooting_mode,
run_initial_analyze=run_initial_analyze,
)
if len(ti) == 0:
ctx.logger.warning("auto: no critical steps; skipping reduction.")
return edisgo
reduce_timeseries_data_to_given_timeindex(edisgo, ti, **(reduce_kwargs or {}))
else:
raise ValueError(f"unknown mode={mode!r}")
if persist and ctx.results_dir is not None:
ctx.results_dir.mkdir(parents=True, exist_ok=True)
pd.DataFrame({"timestep": edisgo.timeseries.timeindex}).to_csv(
ctx.results_dir / "timestep_selection.csv", index=False)
return edisgo
Notes:
- Reduction uses
reduce_timeseries_data_to_given_timeindex
(edisgo/tools/tools.py:1073), not plain set_timeindex, because it slices
EV / HP / DSM / overlying-grid series consistently (it covers
overlying_grid=True). Plain set_timeindex does not slice the DataFrames.
- A selection CSV is persisted to
<results_dir>/timestep_selection.csv for
reproducibility.
eDisGo side — preset places the task TWICE (one active per run)
The two modes need different positions in the pipeline:
- manual must run before
oedb_ts so OEDB downloads only the requested
timestamps (cheap). It only sets the index when the timeindex is still empty.
- auto must run after
import_overlying_grid_data because it needs all
active-power TS (including overlying-grid-distributed generator TS) plus
reactive power to run a meaningful power flow inside
get_most_critical_time_steps.
The runner walks the pipeline as an ordered list (name, params) and looks up
the same registered function each time — there is no dedup on task names, and
each step carries its own independent params dict. So select_timesteps can
appear twice, each defaulting to enabled: false (net effect: full year, like
today). Only the position matching the chosen mode is enabled at run time.
Proposed uc4_example.yaml pipeline:
pipeline:
- setup_grid
- import_generators
- import_home_batteries
- import_heat_pumps
- import_dsm
- import_electromobility: {charging_strategy: null, flexibility_bands_ucs: [home, work, public, hpc]}
- select_timesteps: {enabled: false, mode: manual} # manual position (pre-oedb_ts)
- oedb_ts: {dispatchable: {other: 0.7}, timeindex: {start: "2035-01-01", periods: 8760, freq: h}}
- apply_charging_strategy: {strategy: dumb}
- apply_heat_pump_strategy: {strategy: uncontrolled}
- import_overlying_grid_data
- select_timesteps: {enabled: false, mode: auto} # auto position (post-overlying-grid)
- reactive_power
- optimize: {flexible: [heat_pumps, storage, charging_points, dsm], method: soc, opf_version: 3}
- reinforce
- save: {archive: true, save_opf_results: true}
Required supporting change in oedb_ts (timeseries.py:177): only call
set_timeindex when the index is still empty, so a manual pre-step can set a
smaller index and oedb_ts respects it instead of overwriting with the full
year:
if timeindex is not None and edisgo.timeseries.timeindex.empty:
...
edisgo.set_timeindex(ti_df)
The oedb_ts.timeindex kwarg is then the full-year default used when both
selectors are disabled or when auto mode is active. (Note: today the preset has
periods: 24 — change to periods: 8760 so auto mode has a full year to reduce
from.)
eDisGo side — validator
In edisgo/run/validator.py:
- Allow
select_timesteps anywhere after a TS task (auto) or before it (manual).
- Do not add it to
_TS_TASKS — it is a reducer, not a TS setter, so it must
not be treated as satisfying "timeseries set".
- Add a rule: at most one
select_timesteps step may be enabled: true per run
(else risk of double reduction).
eGo side — scenario configuration
New optional eDisGo.timestep_selection block. Polymorphic shape: a flat dict =
global config for all grids; a {default, per_grid} dict = per-grid config.
Global:
"timestep_selection": {
"mode": "auto", "percentage": 0.1,
"weight_by_costs": true, "run_initial_analyze": true
}
Per-grid (presence of per_grid key is the discriminator):
"timestep_selection": {
"default": { "mode": "auto", "percentage": 0.1 },
"per_grid": {
"32377": { "mode": "manual", "timesteps": ["2035-06-21T08:00:00", "2035-06-21T18:00:00"] },
"30885": { "mode": "auto", "percentage": 0.2, "num_steps_loading": 48 },
"1056": null
}
}
Resolver rules:
- Per-grid keys are stringified
mv_grid_id (JSON requires string keys).
- Resolution precedence:
per_grid[str(id)] > per_grid[int(id)] > default > None.
- Per-grid config replaces
default wholesale (no deep merge — avoids
ambiguity when modes differ).
null for a grid = explicit opt-out (full year).
- A grid missing from
per_grid falls back to default, or to full year if no
default.
default may itself be mode: manual with explicit timesteps.
eGo side — plumbing (eGo/ego/tools/edisgo_integration.py)
- Cache
self._timestep_selection = self._edisgo_args.get("timestep_selection")
in _set_scenario_settings (~line 508).
- Add module-level helpers
_resolve_timestep_selection(block, mv_grid_id) and
_inject_select_timesteps_step(pipeline, ts_block).
- In
_build_run_edisgo_config, before return cfg: resolve the block for the
grid; if non-None, load_config(cfg), find the matching select_timesteps
step (manual position if mode==manual, auto position if mode==auto),
set its params and force enabled: true, write back as cfg["pipeline"], and
cfg.pop("extends") to prevent a double preset merge.
Resolved decisions
- Selection logic stays in eDisGo; eGo only configures it.
get_most_critical_time_steps is left as-is (no changes to the function).
- Reactive power must be the last TS step. Auto mode therefore sets reactive
internally (set_time_series_reactive_power_control) without flipping the
reactive_power_set flag, just to run a meaningful PF for step selection; the
final reactive_power task then re-runs on the reduced active-power series.
(Verified that analyze does not auto-set reactive power; with no reactive
TS, PyPSA fills q_set with 0, which is why PF "worked" but gave wrong voltage
results — unsuitable for critical-step selection.)
manual accepts either timesteps or start+(periods|end)+freq.
- A manual
default is supported.
- Single task body branched on
mode, placed at two positions in the preset,
each enabled: false by default; eGo enables exactly one based on mode.
auto_intervals mode (get_most_critical_time_intervals) is not needed in
v1.
Open questions
run_initial_analyze=True for auto mode adds one full-year power flow per
grid (≈ hours for ~100 grids). Keep it as the default, or default to False
plus an explicit analyze step in the preset?
- Auto-mode reactive handling — confirm Option A (task sets reactive
internally without flipping the flag, final reactive_power re-runs on the
reduced index) vs. Option B (relax the validator and place reactive_power
before select_timesteps).
oedb_ts default periods — confirm changing the preset from 24 to
8760 (full year) so auto mode has a full year to reduce from.
- CSV-import propagation —
_set_scenario_settings
(edisgo_integration.py:476-486) can overwrite _json_file["eDisGo"] from a
prior run's edisgo_args.json. Decide how timestep_selection propagates
across re-runs.
- Duplicate
EDisGoNetworks in io.py:569-625 — verify there is no second
code path that needs the same plumbing.
Implementation order
- Add
task_select_timesteps in eDisGo.
oedb_ts "only set timeindex if empty" fix.
- Validator rule (placement + single-enabled + not a TS-setter).
- Preset YAML: two
select_timesteps entries (enabled: false), oedb_ts
periods → 8760.
- eDisGo unit tests: manual list / manual range / auto / empty-result warning /
CSV persistence.
- Validator tests.
- eGo
_timestep_selection cache.
- eGo
_resolve_timestep_selection + _inject_select_timesteps_step.
- eGo
_build_run_edisgo_config splice + extends handling.
- Scenario JSON example (no block / global / per-grid with opt-out).
- E2E smoke test.
- CSV-import propagation handling.
- Check duplicate
EDisGoNetworks in io.py.
Repos affected
- openego/eDisGo — task, preset, validator, tests.
- openego/eGo — scenario config, plumbing in
edisgo_integration.py.
Add configurable timestep selection for individual eDisGo grids in eGo
Summary
Today, the timeindex analysed for each eDisGo grid in an eGo run is effectively
fixed: it is set inside the
oedb_tstask via thetimeindexkwarg in theuc4_example.yamlpreset (full year / a fixed range). All older eGo mechanismsfor timestep selection are dead code (see "Background" below).
We want a clean, configurable way to determine the analysed timeindex per grid,
with the selection logic living in eDisGo and the configuration exposed in
eGo. Two selection modes:
manual— an explicitly defined timeindex (as currently done inuc4_example.yaml), given either as an ISO-8601 list of timesteps or asstart+ (periods|end) +freq.auto— call eDisGo's built-inget_most_critical_time_steps(
edisgo/tools/temporal_complexity_reduction.py:837) to pick the most criticalsteps from the full-year series, then reduce all time series to that index.
The eGo configuration must allow:
Background (current state)
get_most_critical_time_steps— flatDatetimeIndexof most criticalindividual steps (
temporal_complexity_reduction.py:837).get_most_critical_time_intervals— worst contiguous 168 h weeks(
temporal_complexity_reduction.py:639). Deferred — not needed in v1.get_most_critical_time_intervalsis imported and defined in_run_edisgo_task_temporal_complexity_reduction(
eGo/ego/tools/edisgo_integration.py:1487-1684) but is DEAD — its onlycaller is the commented-out legacy
run_edisgoblock.set_timeindex(...)in thelegacy setup task, timeindex rewrite from
etrago_obj.snapshots,reduce_timeseries_data_to_given_timeindexper interval, eTraGostart_snapshot/end_snapshot, the staletasks: ["3_temporal_complexity_reduction", ...]config key, etc.appl.py→EDisGoNetworks→_run_one_grid_via_runner→
edisgo.run.run_edisgo(cfg, ...). The cfg from_build_run_edisgo_config(
edisgo_integration.py:718-759) has zero timestep fields; everything isdelegated to the eDisGo runner + the
uc4_examplepreset.So: timestep selection must be configured on the eDisGo side (preset/runner),
and eGo must learn to inject that config.
Proposed design
eDisGo side — new task
select_timestepsAdd a single task in
edisgo/run/tasks/timeseries.py(alongside the existingtimeindex/oedb tasks), registered once, that branches on
modeand respects anenabledflag:Notes:
reduce_timeseries_data_to_given_timeindex(
edisgo/tools/tools.py:1073), not plainset_timeindex, because it slicesEV / HP / DSM / overlying-grid series consistently (it covers
overlying_grid=True). Plainset_timeindexdoes not slice the DataFrames.<results_dir>/timestep_selection.csvforreproducibility.
eDisGo side — preset places the task TWICE (one active per run)
The two modes need different positions in the pipeline:
oedb_tsso OEDB downloads only the requestedtimestamps (cheap). It only sets the index when the timeindex is still empty.
import_overlying_grid_databecause it needs allactive-power TS (including overlying-grid-distributed generator TS) plus
reactive power to run a meaningful power flow inside
get_most_critical_time_steps.The runner walks the pipeline as an ordered list
(name, params)and looks upthe same registered function each time — there is no dedup on task names, and
each step carries its own independent params dict. So
select_timestepscanappear twice, each defaulting to
enabled: false(net effect: full year, liketoday). Only the position matching the chosen mode is enabled at run time.
Proposed
uc4_example.yamlpipeline:Required supporting change in
oedb_ts(timeseries.py:177): only callset_timeindexwhen the index is still empty, so a manual pre-step can set asmaller index and
oedb_tsrespects it instead of overwriting with the fullyear:
The
oedb_ts.timeindexkwarg is then the full-year default used when bothselectors are disabled or when auto mode is active. (Note: today the preset has
periods: 24— change toperiods: 8760so auto mode has a full year to reducefrom.)
eDisGo side — validator
In
edisgo/run/validator.py:select_timestepsanywhere after a TS task (auto) or before it (manual)._TS_TASKS— it is a reducer, not a TS setter, so it mustnot be treated as satisfying "timeseries set".
select_timestepsstep may beenabled: trueper run(else risk of double reduction).
eGo side — scenario configuration
New optional
eDisGo.timestep_selectionblock. Polymorphic shape: a flat dict =global config for all grids; a
{default, per_grid}dict = per-grid config.Global:
Per-grid (presence of
per_gridkey is the discriminator):Resolver rules:
mv_grid_id(JSON requires string keys).per_grid[str(id)] > per_grid[int(id)] > default > None.defaultwholesale (no deep merge — avoidsambiguity when modes differ).
nullfor a grid = explicit opt-out (full year).per_gridfalls back todefault, or to full year if nodefault.defaultmay itself bemode: manualwith explicit timesteps.eGo side — plumbing (
eGo/ego/tools/edisgo_integration.py)self._timestep_selection = self._edisgo_args.get("timestep_selection")in
_set_scenario_settings(~line 508)._resolve_timestep_selection(block, mv_grid_id)and_inject_select_timesteps_step(pipeline, ts_block)._build_run_edisgo_config, beforereturn cfg: resolve the block for thegrid; if non-None,
load_config(cfg), find the matchingselect_timestepsstep (manual position if
mode==manual, auto position ifmode==auto),set its params and force
enabled: true, write back ascfg["pipeline"], andcfg.pop("extends")to prevent a double preset merge.Resolved decisions
get_most_critical_time_stepsis left as-is (no changes to the function).internally (
set_time_series_reactive_power_control) without flipping thereactive_power_setflag, just to run a meaningful PF for step selection; thefinal
reactive_powertask then re-runs on the reduced active-power series.(Verified that
analyzedoes not auto-set reactive power; with no reactiveTS, PyPSA fills
q_setwith 0, which is why PF "worked" but gave wrong voltageresults — unsuitable for critical-step selection.)
manualaccepts eithertimestepsorstart+(periods|end)+freq.defaultis supported.mode, placed at two positions in the preset,each
enabled: falseby default; eGo enables exactly one based on mode.auto_intervalsmode (get_most_critical_time_intervals) is not needed inv1.
Open questions
run_initial_analyze=Truefor auto mode adds one full-year power flow pergrid (≈ hours for ~100 grids). Keep it as the default, or default to
Falseplus an explicit
analyzestep in the preset?internally without flipping the flag, final
reactive_powerre-runs on thereduced index) vs. Option B (relax the validator and place
reactive_powerbefore
select_timesteps).oedb_tsdefaultperiods— confirm changing the preset from24to8760(full year) so auto mode has a full year to reduce from._set_scenario_settings(
edisgo_integration.py:476-486) can overwrite_json_file["eDisGo"]from aprior run's
edisgo_args.json. Decide howtimestep_selectionpropagatesacross re-runs.
EDisGoNetworksinio.py:569-625— verify there is no secondcode path that needs the same plumbing.
Implementation order
task_select_timestepsin eDisGo.oedb_ts"only set timeindex if empty" fix.select_timestepsentries (enabled: false),oedb_tsperiods → 8760.
CSV persistence.
_timestep_selectioncache._resolve_timestep_selection+_inject_select_timesteps_step._build_run_edisgo_configsplice +extendshandling.EDisGoNetworksinio.py.Repos affected
edisgo_integration.py.