diff --git a/config/config.default.yaml b/config/config.default.yaml index 62d69380..1598ca1c 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -640,6 +640,7 @@ solving: skip_iterations: true rolling_horizon: false seed: 123 + custom_extra_functionality: "../data/custom_extra_functionality.py" # options that go into the optimize function track_iterations: false min_iterations: 4 diff --git a/data/custom_extra_functionality.py b/data/custom_extra_functionality.py new file mode 100644 index 00000000..0ac24cea --- /dev/null +++ b/data/custom_extra_functionality.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2023- The PyPSA-Eur Authors +# +# SPDX-License-Identifier: MIT + + +def custom_extra_functionality(n, snapshots): + """ + Add custom extra functionality constraints. + """ + pass diff --git a/doc/configtables/solving.csv b/doc/configtables/solving.csv index 45d50d84..dcff54e4 100644 --- a/doc/configtables/solving.csv +++ b/doc/configtables/solving.csv @@ -6,6 +6,7 @@ options,,, -- skip_iterations,bool,"{'true','false'}","Skip iterating, do not update impedances of branches. Defaults to true." -- rolling_horizon,bool,"{'true','false'}","Whether to optimize the network in a rolling horizon manner, where the snapshot range is split into slices of size `horizon` which are solved consecutively." -- seed,--,int,Random seed for increased deterministic behaviour. +-- custom_extra_functionality,--,str,Path to a Python file with custom extra functionality code to be injected into the solving rules of the workflow relative to ``rules`` directory. -- track_iterations,bool,"{'true','false'}",Flag whether to store the intermediate branch capacities and objective function values are recorded for each iteration in ``network.lines['s_nom_opt_X']`` (where ``X`` labels the iteration) -- min_iterations,--,int,Minimum number of solving iterations in between which resistance and reactence (``x/r``) are updated for branches according to ``s_nom_opt`` of the previous run. -- max_iterations,--,int,Maximum number of solving iterations in between which resistance and reactence (``x/r``) are updated for branches according to ``s_nom_opt`` of the previous run. diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 270c8876..a4f20b86 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -79,6 +79,11 @@ Upcoming Release reconnected to the main Ukrainian grid with the configuration option `reconnect_crimea`. +* Add option to reference an additional source file where users can specify + custom ``extra_functionality`` constraints in the configuration file. The + default setting points to an empty hull at + ``data/custom_extra_functionality.py``. + * Validate downloads from Zenodo using MD5 checksums. This identifies corrupted or incomplete downloads. diff --git a/rules/common.smk b/rules/common.smk index 2c8cf69c..44e3a807 100644 --- a/rules/common.smk +++ b/rules/common.smk @@ -28,6 +28,13 @@ def memory(w): return int(factor * (10000 + 195 * int(w.clusters))) +def input_custom_extra_functionality(w): + path = config["solving"]["options"].get("custom_extra_functionality", False) + if path: + return workflow.source_path(path) + return [] + + # Check if the workflow has access to the internet by trying to access the HEAD of specified url def has_internet_access(url="www.zenodo.org") -> bool: import http.client as http_client diff --git a/rules/solve_electricity.smk b/rules/solve_electricity.smk index c396ebd5..7f6092be 100644 --- a/rules/solve_electricity.smk +++ b/rules/solve_electricity.smk @@ -11,6 +11,7 @@ rule solve_network: co2_sequestration_potential=config["sector"].get( "co2_sequestration_potential", 200 ), + custom_extra_functionality=input_custom_extra_functionality, input: network=RESOURCES + "networks/elec_s{simpl}_{clusters}_ec_l{ll}_{opts}.nc", config=RESULTS + "config.yaml", diff --git a/rules/solve_myopic.smk b/rules/solve_myopic.smk index 8a93d24a..7ca8857d 100644 --- a/rules/solve_myopic.smk +++ b/rules/solve_myopic.smk @@ -88,6 +88,7 @@ rule solve_sector_network_myopic: co2_sequestration_potential=config["sector"].get( "co2_sequestration_potential", 200 ), + custom_extra_functionality=input_custom_extra_functionality, input: network=RESULTS + "prenetworks-brownfield/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", diff --git a/rules/solve_overnight.smk b/rules/solve_overnight.smk index c7700760..a3fed042 100644 --- a/rules/solve_overnight.smk +++ b/rules/solve_overnight.smk @@ -11,6 +11,7 @@ rule solve_sector_network: co2_sequestration_potential=config["sector"].get( "co2_sequestration_potential", 200 ), + custom_extra_functionality=input_custom_extra_functionality, input: network=RESULTS + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", diff --git a/rules/solve_perfect.smk b/rules/solve_perfect.smk index ef4e367d..a7856fa9 100644 --- a/rules/solve_perfect.smk +++ b/rules/solve_perfect.smk @@ -118,6 +118,7 @@ rule solve_sector_network_perfect: co2_sequestration_potential=config["sector"].get( "co2_sequestration_potential", 200 ), + custom_extra_functionality=input_custom_extra_functionality, input: network=RESULTS + "prenetworks-brownfield/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_brownfield_all_years.nc", diff --git a/scripts/solve_network.py b/scripts/solve_network.py index d4523012..2f170dff 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -26,8 +26,11 @@ Additionally, some extra constraints specified in :mod:`solve_network` are added the workflow for all scenarios in the configuration file (``scenario:``) based on the rule :mod:`solve_network`. """ +import importlib import logging +import os import re +import sys import numpy as np import pandas as pd @@ -826,6 +829,14 @@ def extra_functionality(n, snapshots): add_carbon_budget_constraint(n, snapshots) add_retrofit_gas_boiler_constraint(n, snapshots) + if snakemake.params.custom_extra_functionality: + source_path = snakemake.params.custom_extra_functionality + assert os.path.exists(source_path), f"{source_path} does not exist" + sys.path.append(os.path.dirname(source_path)) + module_name = os.path.splitext(os.path.basename(source_path))[0] + module = importlib.import_module(module_name) + module.custom_extra_functionality(n, snapshots) + def solve_network(n, config, solving, opts="", **kwargs): set_of_options = solving["solver"]["options"]