Skip to content

Make rules/functions modular and reusable #2194

Description

@FabianHofmann

When planning on reusing of parts of pypsa-eur in a modular fashion, user quickly face limitations. The following proposal serve as a starting point for a discussion how to make the workflow more "distributable" allowing to re-use rules and functions inside and outside of forks.

CORE IDEA: For each script create one high-level function which is runs independent of the snakemake object, has a clear signature and is importable.

Take build_renewable_profiles as an example. This could be easily restructured as:

# build_renewable_profiles.py  (same file)
def build_renewable_profiles(cutout, availability_matrix, params, client=None) -> xr.Dataset:
    ...  # hoisted out of the __main__ block; returns the Dataset
    return ds

if __name__ == "__main__":
    if "snakemake" not in globals():
        from scripts._helpers import mock_snakemake
        snakemake = mock_snakemake("build_renewable_profiles", ...)
    ds = build_renewable_profiles(cutout, avail, dict(snakemake.params), client=...)
    ds.to_netcdf(snakemake.output.profile)

Downstream then does from scripts.build_renewable_profiles import build_renewable_profiles.

In order to make this consistent we need to

  • not leak the snakemake object into the function. The function must take real arguments (cutout, matrix, a params dict/dataclass, plain paths) so it is callable without a internal dependency on the snakemake object.
  • define proper return values and not preempt the writing of the output.**. Keep dependency on relative paths low.

Another benefit is that pure functions are unit-testable without running Snakemake.

I iterated with Claude on this on details for time series, see below

Details ## Tiering of the time-series builders

Scan signals (counts of matching lines per script):

Script atlite dask ext. download xarray Tier
build_renewable_profiles 13 7 8 A
build_hydro_profile 9 1 A
build_temperature_profiles 8 4 4 A
build_solar_thermal_profiles 7 3 3 A
build_daily_heat_demand 7 3 3 A
build_line_rating 7 9 5 A
build_country_runoff 4 A
build_cop_profiles (pkg) A (done)
build_hourly_heat_demand 4 B
build_transport_demand 2 B
build_electricity_demand_base 5 B
build_electricity_demand ENTSO-E C
build_mobility_profiles C
build_monthly_prices C
build_co2_prices C
build_shipping_demand C

Tier A — cutout/atlite transforms → best fit

All share one signature: (cutout, clustered_regions/layout, params) → xarray.Dataset.
Deterministic functions of the cutout, no runtime network calls.

One design decision: the dask client. build_line_rating and
build_renewable_profiles construct and shutdown() the client inside the script.
Lifting as-is would couple every importer to a dask-cluster lifecycle. Make it an
injected, optional argument (client: Client | None = None) so the caller owns
parallelism.

Tier B — pure reshapers → trivial fit

No atlite, no download — dataframe/xarray reshaping only (daily→hourly,
profiles→demand, spatial distribution). Nearly pure already; only the
snakemake.input/output lines need peeling off into the shell.

Tier C — retrieval-coupled → fit only after splitting retrieve from transform

The download is half the script and is the part that is non-deterministic, needs API
keys, and must not live in an importable function. Move the download behind a thin
retrieve_* adapter; the parse→tidy→reindex-to-snapshots half becomes the importable
function. Same retrieve/transform discipline build_cop_profiles already demonstrates.

Proposed next steps (smallest valuable first)

  1. Add a high-level function to build_renewable_profiles and build_line_rating
    (in-script), injecting the dask client. Highest value, and they exercise the
    dask-injection decision.
  2. Same for Tier B scripts — mechanical, low-risk.
  3. For Tier C, split retrieve vs. transform; expose the transform as an in-script
    function, leave retrieval behind retrieve_*.

Optional later consolidation (non-breaking)

Once the in-script functions exist, a curated pypsa_eur/ package can re-export
them for a stable public API without touching call sites:

# pypsa_eur/weather_profiles.py
from scripts.build_renewable_profiles import build_renewable_profiles
from scripts.build_line_rating import build_line_rating
...

This is the half-step the in-script approach leaves open: it fully serves the
soft-fork / branch-off case today (we already vendor the repo and do
from scripts.gb_model._helpers import ...), and the pip-installable-library case
becomes a later re-export layer rather than a prerequisite refactor.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions