Feature/1414 add rail transport dataset - #1430
Conversation
Ingests the prebuilt rail-transport-demand GeoPackage + load_profiles.csv from the data bundle and writes time-varying loads to grid.egon_etrago_load and grid.egon_etrago_load_timeseries. Converters (16.7-Hz traction) map to EHV buses, DC rectifier substations (tram/U-Bahn + S-Bahn) to MV grid district buses; p_set[h] = energy_mwh_a * normalized profile[h].
New TaskGroup rail_transport_demand depending on osm, mv_grid_districts, substation_voronoi, setup_etrago, data_bundle and scenario_parameters.
Define annual_demand (MWh, eGon unit convention) for status2024, reGon2037 and reGon2045 from the NEP gross-rail figures (bus excluded; futures scale the 12.74 TWh base by the NEP growth).
Add status2024/reGon2037/reGon2045 rows to insert_scenarios (mobility sector only for now; other sectors left empty until reGon defines them).
Read annual_demand (MWh) per reGon scenario via get_sector_parameters and scale the MW time series by total(scn) / total(status2024); target status2024/reGon2037/reGon2045 (drop eGon2035/eGon100RE).
Drop the hardcoded p[['bus_id','geometry']] slice (districts from select_geodataframe carry a 'geom' geometry column, not 'geometry'). Pass the district gdfs straight to sjoin/sjoin_nearest, which join on each frame's active geometry regardless of column name.
sanitycheck_rail_transport_demand validates the eTraGo rail loads per reGon scenario: carriers present, valid grid buses, full 8760-h non-negative NaN-free p_set, and energy scaling vs scenario_parameters. Registered unconditionally; --no-verify due to pre-existing lint.
|
I will keep my mini / test DAGS in the .gitignore for now and remove them once we can merge this PR. Is this issue with the CI already kown? The pipeline fails for and it states that the DB is not availabale. Below claude inpected the issue and found a possible cause: Error from Tox setp: Quick claude analysisDiagnosis Root cause: The recently merged feature "Store datasets sources and targets in separate table" (36e4238) added a self.register() call to Dataset.post_init. So now, constructing any Dataset object immediately opens a PostgreSQL connection: src/egon/data/datasets/init.py:453def register(self):
SourcesTargetsModel.__table__.create(bind=db.engine(), checkfirst=True) # <-- live connection
with db.session_scope() as session:
...The two failing tests never needed a DB before: test_pipeline_importability just imports the pipeline — but importing builds every Dataset, which now hits the DB. There's a deeper smell too: Airflow re-parses DAG files constantly, so requiring a DB connection just to import the DAG is fragile even in production. Recommended fix def register(self):
try:
SourcesTargetsModel.__table__.create(bind=db.engine(), checkfirst=True)
with db.session_scope() as session:
... # existing body
except OperationalError as e:
logger.warning(
f"Could not register sources/targets for '{self.name}' "
f"(database unavailable): {e}. Skipping registration."
)This is minimal and low-risk. The alternative — adding a postgres service container to build.yml — is more invasive, slower, and the test suite clearly wasn't designed to assume one. Want me to apply the tolerant-register() fix? I'd do it on your current branch (feature/1414-add-rail-transport-dataset), though note the underlying bug lives on dev, so ideally the fix lands there too. |
Move the rail_transport_demand import to top level (aliased as rail_demand to avoid re-importing the module-level SCENARIOS) and disable too-many-locals (repo convention, cf. mastr.py).
|
@nesnoj The Codacy errors are false flags IMO - ofc its not good to use f string with user input to construct a SQL query but here we dont have any user input but the defined input from sources / targets |
|
This is the "mini" DAG that aims to isolate only to relevant datasets related to the new RailTransitDemand dataset. It runs sucessfully: Mini DAG for Testing RailTransitDemand dataset"""Isolated DAG: build every upstream dataset RailTransitDemand needs, then
run it -- so the reGon rail transport demand can be tested end-to-end against
a freshly filled DB, without the whole eGon pipeline.
Runs the transitive dependency closure of RailTransitDemand:
setup, setup_etrago, osm, data_bundle, vg250, scenario_parameters,
substation_extraction, osmtgmod, substation_voronoi, mv_grid_districts,
then rail_transit_demand.
The TaskGroup ids ("input_data", "electricity_grid") mirror pipeline.py so the
fully-qualified task references inside Osmtgmod / SubstationVoronoi resolve.
NOTE: osmtgmod is heavy (it builds the ehv/hv grid model). Gitignored dev DAG.
"""
import os
from airflow.operators.python import PythonOperator
from airflow.utils.dates import days_ago
from airflow.utils.task_group import TaskGroup
import airflow
from egon.data.config import set_numexpr_threads
from egon.data.config import settings as egon_settings
from egon.data.datasets import database
from egon.data.datasets.data_bundle import DataBundle
from egon.data.datasets.etrago_setup import EtragoSetup
from egon.data.datasets.mv_grid_districts import mv_grid_districts_setup
from egon.data.datasets.osm import OpenStreetMap
from egon.data.datasets.osmtgmod import Osmtgmod
from egon.data.datasets.rail_transport_demand import RailTransitDemand
from egon.data.datasets.sanity_checks import (
sanitycheck_rail_transport_demand,
)
from egon.data.datasets.scenario_parameters import ScenarioParameters
from egon.data.datasets.substation import SubstationExtraction
from egon.data.datasets.substation_voronoi import SubstationVoronoi
from egon.data.datasets.vg250 import Vg250
set_numexpr_threads()
prefix = egon_settings()["egon-data"].get("--prefix")
prefix = "" if prefix is None else f"{prefix}-"
with airflow.DAG(
f"{prefix}egon-rail-transport-isolated",
description="Isolated DAG: rail transport demand + its upstream chain.",
default_args={"start_date": days_ago(1)},
template_searchpath=[
os.path.abspath(
os.path.join(
os.path.dirname(__file__), "..", "..", "processing", "vg250"
)
)
],
is_paused_upon_creation=False,
schedule_interval=None,
) as rail_pipeline:
tasks = rail_pipeline.task_dict
with TaskGroup(group_id="setup") as setup_group:
setup = database.Setup()
setup_etrago = EtragoSetup(dependencies=[setup])
with TaskGroup(group_id="input_data") as input_data_group:
osm = OpenStreetMap(dependencies=[setup])
data_bundle = DataBundle(dependencies=[setup])
vg250 = Vg250(dependencies=[setup])
scenario_parameters = ScenarioParameters(dependencies=[setup])
with TaskGroup(group_id="electricity_grid") as electricity_grid_group:
substation_extraction = SubstationExtraction(
dependencies=[osm, vg250]
)
osmtgmod = Osmtgmod(
dependencies=[
scenario_parameters,
setup_etrago,
substation_extraction,
tasks["input_data.osm.download"],
]
)
substation_voronoi = SubstationVoronoi(
dependencies=[
tasks["electricity_grid.osmtgmod.substation.extract"],
vg250,
]
)
mv_grid_districts = mv_grid_districts_setup(
dependencies=[substation_voronoi]
)
with TaskGroup(group_id="mobility_demand") as mobility_demand_group:
rail_transit_demand = RailTransitDemand(
dependencies=[
data_bundle,
osm,
mv_grid_districts,
substation_voronoi,
setup_etrago,
scenario_parameters,
]
)
with TaskGroup(group_id="sanity_checks") as sanity_checks_group:
# Validate the rail loads/timeseries the dataset just wrote. Only the
# rail check runs here (the full SanityChecks suite needs other
# scenarios/datasets not built by this isolated DAG).
rail_sanity_check = PythonOperator(
task_id="sanitycheck_rail_transport_demand",
python_callable=sanitycheck_rail_transport_demand,
)
for last_task in rail_transit_demand.tasks.last:
last_task.set_downstream(rail_sanity_check) |
Fixes #1414
Adds a new dataset
RailTransitDemandthat spatially and temporally allocates Germany's rail-transport electricity demand onto the eTraGo grid model as loads, building on eGon's own OSM data. Three carriers are covered: 16.7-Hz traction (rail_traction), DC tram/U-Bahn (rail_transit_dc) and DC S-Bahn (rail_sbahn_dc).What it does
openstreetmap.osm_point/osm_ways/osm_polygon(viahstore(tags)->'power'/'voltage'/'frequency'); 16.7-Hz converter locations come from a curated list in the data bundle (OSM under-tags them).p_set[h] = energy_mwh_a · factor(scn) · profile_2011[h].scenario_parameters: introduces three reGon scenarios —status2024,reGon2037,reGon2045— with the gross rail demand stored asmobility → rail_transport_demand → annual_demand(MWh, eGon unit convention).status2024uses the absolute series 1:1; the futures scale bytotal(scn)/total(status2024), derived from the NEP figures (Schiene+Bus combined; bus excluded as the ~1.3 TWh remainder to NEP's 14 TWh). Only the data that eGon cannot derive itself (load profiles, GTFS-based energy weights, curated converter list) lives in the data bundle.Notes on scope/assumptions: the NEP does not split rail vs. bus, so the future growth uses the combined NEP ratio as a rail-growth proxy (documented in code). The reGon scenarios currently carry only the mobility sector; they are intended to grow into full scenarios later.
📋 Pull Request Guidelines
🧑💻 Contributor Checklist
Before requesting a review, make sure you've completed all of the following:
(for more information on local test, check
toxin the Contributing section)(CI tests are automatically executed when creating a PR, you can see the results of the checks below)
(optional if no dataset changes are involved)
(module/dataset docstrings; project docs maintained externally)
(
RailTransitDemand0.0.3,ScenarioParameters0.0.21 → 0.0.22)CHANGELOG.rstabout the changesAUTHORS.rstOptional:
🔍 Reviewer Checklist
During your review, please check the following:
CHANGELOG.rstupdated accordingly?📝 Additional Notes (optional)
osm+mv_grid_districts+substation_voronoi+setup_etrago+scenario_parameters, trigger the dataset and check:and confirm
scenario.egon_scenario_parameterscontainsstatus2024/reGon2037/reGon2045.data_bundle_egon_data/rail_transport_demand/(load profiles + GTFS energy weights + curated converter list).scenario_parametersfiles carry pre-existing flake8 violations (unrelated to this change); only the newly added lines were modified.