Skip to content

potentials.py

Austrian KLIEN regional capacity potential overwrites for the modify_prenetwork step.

Caps extendable AT generator p_nom_max values by the regional potentials of the Austrian KLIEN study (ground PV, building PV, onshore wind), following a brownfield-deduction pattern so the bounds represent the additional capacity still available to the solver.

  • :func:apply_klien_potential_limits — entry point called from :func:mods.network.modify_prenetwork; reads the pre-processed KLIEN potential CSVs, aggregates them to the network clustering, and writes the remaining headroom into p_nom_max.

TYNDP PEMMDB trajectory bands follow the same pattern but live in :mod:mods.network.trajectories.

apply_klien_potential_limits(n, snakemake)

Cap extendable AT generator p_nom_max values by regional KLIEN study potentials.

Reads pre-processed capacity potential CSVs (in MW) produced by build_klien_potentials, subtracts already-committed brownfield capacity, and writes the remaining headroom into p_nom_max for every extendable AT generator whose carrier appears in klien_potential_limits.technologies.

Only generators on buses whose index starts with "AT" are affected. Non-AT generators (e.g. DE, CH) are left unchanged. The function skips silently when klien_potential_limits.technologies is an empty list.

When klien_potential_limits.use_technical_potentials is true, the column C_technical_potential is used regardless of year, ambition, or climate_scenario.

The NUTS3-level potential CSVs are always read and aggregated to the network's clustering resolution via :func:mods.clustering.utils.combine_regions_by_clustering (AT10 collapses NUTS3 -> NUTS2; AT35 keeps NUTS3).

Supported carriers and their CSV source:

  • solar rooftopnuts3_pv_buildings.csv
  • solar, solar-hsatnuts3_pv_ground.csv (shared land area)
  • onwindnuts3_wind.csv

Parameters:

Name Type Description Default
n pypsa.Network

The pre-network to be modified in place.

required
snakemake snakemake.script.Snakemake

Snakemake workflow object; config is read via snakemake.params and file paths via snakemake.input.

required

Raises:

Type Description
ValueError

If any entry in technologies is not a recognised carrier, or if climate_scenario, year, or ambition are unrecognised.

KeyError

If the requested scenario column is absent from a potential CSV.

Notes

Brownfield capacity is estimated via n.statistics.installed_capacity(), which captures carry-over from previous myopic periods.

Source code in mods/network/potentials.py
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
def apply_klien_potential_limits(n: pypsa.Network, snakemake: Snakemake) -> None:
    """
    Cap extendable AT generator ``p_nom_max`` values by regional KLIEN study potentials.

    Reads pre-processed capacity potential CSVs (in MW) produced by
    ``build_klien_potentials``, subtracts already-committed brownfield capacity,
    and writes the remaining headroom into ``p_nom_max`` for every extendable
    AT generator whose carrier appears in ``klien_potential_limits.technologies``.

    Only generators on buses whose index starts with ``"AT"`` are affected.
    Non-AT generators (e.g. DE, CH) are left unchanged.  The function skips
    silently when ``klien_potential_limits.technologies`` is an empty list.

    When ``klien_potential_limits.use_technical_potentials`` is true, the column
    ``C_technical_potential`` is used regardless of ``year``, ``ambition``, or
    ``climate_scenario``.

    The NUTS3-level potential CSVs are always read and aggregated to the
    network's clustering resolution via
    :func:`mods.clustering.utils.combine_regions_by_clustering` (AT10 collapses
    NUTS3 -> NUTS2; AT35 keeps NUTS3).

    Supported carriers and their CSV source:

    * ``solar rooftop`` — ``nuts3_pv_buildings.csv``
    * ``solar``, ``solar-hsat`` — ``nuts3_pv_ground.csv`` (shared land area)
    * ``onwind`` — ``nuts3_wind.csv``

    Parameters
    ----------
    n
        The pre-network to be modified in place.
    snakemake
        Snakemake workflow object; config is read via ``snakemake.params`` and
        file paths via ``snakemake.input``.

    Raises
    ------
    ValueError
        If any entry in ``technologies`` is not a recognised carrier, or if
        ``climate_scenario``, ``year``, or ``ambition`` are unrecognised.
    KeyError
        If the requested scenario column is absent from a potential CSV.

    Notes
    -----
    Brownfield capacity is estimated via ``n.statistics.installed_capacity()``,
    which captures carry-over from previous myopic periods.
    """
    technologies = snakemake.params["klien_potential_limits_technologies"]
    if not technologies:
        logger.info("KLIEN potential limits: technologies list is empty — skipping.")
        return

    unknown = set(technologies) - set(_PYPSA_TO_KLIEN_MAPPING.keys())
    if unknown:
        raise ValueError(
            f"Unknown technologies in klien_potential_limits.technologies: {unknown}. "
            f"Valid options: {list(_PYPSA_TO_KLIEN_MAPPING.keys())}."
        )

    col = _resolve_scenario_column(snakemake)

    # Always read the NUTS3-level KLIEN potentials and aggregate them to the
    # network's clustering resolution. For AT10 clusterings this collapses
    # NUTS3 -> NUTS2 (potentials are additive); for AT35 the NUTS3 regions are
    # kept as-is.
    clustering = snakemake.config["mods"]["modify_nuts3_shapes"]
    paths = {
        "buildings": snakemake.input.nuts3_buildings,
        "ground": snakemake.input.nuts3_ground,
        "wind": snakemake.input.nuts3_wind,
    }

    # Load each CSV type once; map each requested carrier to its potential dict.
    klien_types_needed = {_PYPSA_TO_KLIEN_MAPPING[t] for t in technologies}
    carrier_potential = {}
    for klien_type in klien_types_needed:
        df = pd.read_csv(Path(paths[klien_type]), index_col=0)
        potential_dict = combine_regions_by_clustering(df[col], clustering).to_dict()
        for tech in _KLIEN_TO_PYPSA_MAPPING[klien_type]:
            carrier_potential[tech] = potential_dict

    brownfield = n.statistics.installed_capacity(
        groupby=["location", "carrier"],
        components="Generator",
        carrier=list(technologies),
        aggregate_across_components=True,
        nice_names=False,
        drop_zero=False,
    )
    brownfield_at = brownfield[
        brownfield.index.get_level_values("location").str.startswith("AT")
    ]

    for (location, carrier), brownfield_value in brownfield_at.items():
        potential = carrier_potential[carrier][location]

        mask_ext = (
            (n.generators.index.str.startswith(f"{location} "))
            & (n.generators["carrier"] == carrier)
            & (n.generators["p_nom_extendable"])
        )

        if not any(mask_ext):
            continue

        # Make sure that the upper limit can always be reached
        new_upper_limit = max(0.0, potential, brownfield_value)

        for gen_idx in n.generators.index[mask_ext]:
            _set_p_nom_max(n, gen_idx, new_upper_limit)

    logger.info(f"AT KLIEN potential limits applied for: {list(technologies)}.")