diff --git a/Snakefile b/Snakefile index d48b4b0e..05a8377b 100644 --- a/Snakefile +++ b/Snakefile @@ -10,6 +10,7 @@ root_dir = config["root-directory"] + "/" if config["root-directory"] not in ["" __version__ = open(f"{root_dir}VERSION").readlines()[0].strip() test_dir = f"{root_dir}tests/" model_test_dir = f"{test_dir}model" +resources_test_dir = f"{test_dir}resources" template_dir = f"{root_dir}templates/" model_template_dir = f"{template_dir}models/" techs_template_dir = f"{model_template_dir}techs/" @@ -107,7 +108,7 @@ rule module_with_location_specific_data: biofuel_efficiency = config["parameters"]["biofuel-efficiency"], wildcard_constraints: # Exclude all outputs that have their own `techs_and_locations_template` implementation - group_and_tech = "(?!transmission\/|supply\/biofuel).*" + group_and_tech = "(?!transmission\/|supply\/biofuel|supply\/electrified-biofuel).*" conda: "envs/default.yaml" output: "build/models/{resolution}/techs/{group_and_tech}.yaml" script: "scripts/template_techs.py" @@ -156,14 +157,12 @@ rule model: "techs/demand/electrified-transport.yaml", "techs/storage/electricity.yaml", "techs/storage/hydro.yaml", - "techs/supply/biofuel.yaml", "techs/supply/hydro.yaml", "techs/supply/load-shedding.yaml", "techs/supply/open-field-solar-and-wind-onshore.yaml", "techs/supply/rooftop-solar.yaml", "techs/supply/wind-offshore.yaml", "techs/supply/nuclear.yaml", - "techs/conversion/electricity-from-biofuel.yaml" ] ), heat_timeseries_data = ( @@ -204,6 +203,14 @@ rule model: "techs/conversion/heat-from-biofuel.yaml", "techs/supply/historic-electrified-heat.yaml" ] + ), + optional_biofuel_modules = expand( + "build/models/{{resolution}}/{module}", + module=[ + "techs/supply/biofuel.yaml", + "techs/supply/electrified-biofuel.yaml", + "techs/conversion/electricity-from-biofuel.yaml" + ] ) params: year = config["scope"]["temporal"]["first-year"] @@ -252,6 +259,7 @@ rule test: message: "Run tests" input: test_dir = model_test_dir, + test_resources_dir = resources_test_dir, tests = map(str, Path(model_test_dir).glob("**/test_*.py")), example_model = "build/models/{resolution}/example-model.yaml", capacity_factor_timeseries = expand( diff --git a/docs/model/customisation.md b/docs/model/customisation.md index 87ed0575..3d57faf3 100644 --- a/docs/model/customisation.md +++ b/docs/model/customisation.md @@ -15,13 +15,13 @@ You have the following three options: With the Calliope model in your hands, you will be able to change any model parameter, any technology specifics, and the model definition to your liking. This kind of customisation can be useful to get to know the model and its parameters. To create reliable results, we advise making manual changes only to the model definition (`example-model.yaml`) as this makes it possible to trace those changes later. -A typical customisation here would be to change the solver from `gurobi` to an open-source solver, e.g. `cbc` (see [Calliope's documentation]( calliope_version }}/user/config_defaults.html#run-configuration)). +A typical customisation here would be to change the solver from `gurobi` to an open-source solver, e.g. `cbc` (see [Calliope's documentation](https://calliope.readthedocs.io/en/v{{ calliope_version }}/user/config_defaults.html#run-configuration)). We consider all Euro-Calliope model subcomponents (everything other than the model definition itself) as a toolbox from which you can choose to define your model -- see the [Import customisation option](./customisation.md#importing-modules). ## Importing modules The `example-model.yaml` definition file in each resolution sub-directory (e.g. `national/example-model.yaml`) specifies a list of other files to bring together to describe the model (under the `import` key). -This list can be changed by the modeller to select a combination of different files (see also [Calliope's documentation]( calliope_version }}/user/building.html#files-that-define-a-model)). +This list can be changed by the modeller to select a combination of different files (see also [Calliope's documentation](https://calliope.readthedocs.io/en/v{{ calliope_version }}/user/building.html#files-that-define-a-model)). These files represent "modules" of the model definition and contain everything necessary for a given technology or technology group to exist. For instance, `techs/supply/hydro.yaml` defines two technologies (under the `techs` key) which will convert river flows into electricity. It also places that technology in every relevant modelled location (under the `locations` key), along with any location-specific information that is needed; in this case, the maximum capacity of hydropower in that location. @@ -104,11 +104,15 @@ Here, we describe each module in terms of the technologies they contain (`callio === "Technologies" - **biofuel**: Biofuel supply, limited per model location to a total annual production. + **biofuel**: Biofuel supply, limited per model location to an hourly flow that can be stored before use in downstream technologies. - === "Overrides" +??? note "supply/electrified-biofuel.yaml" + + === "Technologies" - **biofuel_flow_limits**: Distribute annual total production limit evenly across all hours of the year and allow biofuel storage up to 50% of total annual production. + **electrified_biofuel**: Electrified biofuel supply (assuming anaerobic digestion). + This should be used in an electricity-only model, where biofuel supply is assumed to only be used for direct generation of electricity. + This simplifies the model compared to using `supply/biofuel.yaml` and `conversion/electricity-from-biofuel.yaml` by not introducing the `biofuel` energy carrier. ??? note "conversion/heat-from-biofuel.yaml" @@ -116,7 +120,7 @@ Here, we describe each module in terms of the technologies they contain (`callio **biofuel_boiler**: Biofuel-consuming boiler. - **biofuel_tech_heat_to_demand**: Dummy technology to convert biofuel boiler output to a carrier that can be used to meet heat demand. + **biofuel_tech_heat_to_demand**: "Dummy" technology to convert biofuel boiler output to a carrier that can be used to meet heat demand. ??? note "conversion/electricity-from-biofuel.yaml" @@ -243,7 +247,7 @@ Here, we describe each module in terms of the technologies they contain (`callio ## Overrides and scenarios -Calliope [overrides]( calliope_version }}/user/building.html#scenarios-and-overrides) enable models to be easily manipulated. +Calliope [overrides](https://calliope.readthedocs.io/en/v{{ calliope_version }}/user/building.html#scenarios-and-overrides) enable models to be easily manipulated. An override named `freeze-hydro-supply-capacities` can be used for example in this way: ``` bash @@ -262,7 +266,7 @@ For instance, `freeze-hydro-supply-capacities` and `freeze-hydro-storage-capacit You can also define your own overrides to manipulate any model component. We recommend you add these overrides into the model definition YAML file, to ensure they are easy to trace. -In Calliope, [scenarios]( calliope_version }}/user/building.html#scenarios-and-overrides) are groups of overrides and/or other scenarios. +In Calliope, [scenarios](https://calliope.readthedocs.io/en/v{{ calliope_version }}/user/building.html#scenarios-and-overrides) are groups of overrides and/or other scenarios. In Euro-Calliope, it can be helpful to define scenarios to help group similar overrides together. For instance, cost overrides from the Danish Energy Agency are defined in various files, since they are loaded in alongside the technologies they affect (the option to override offshore wind costs only exists when you load the `techs/supply/wind-offshore.yaml` module). You can pre-define scenarios in your model definition file, such as: diff --git a/rules/biofuels.smk b/rules/biofuels.smk index 84af68a7..303f2905 100644 --- a/rules/biofuels.smk +++ b/rules/biofuels.smk @@ -52,15 +52,18 @@ rule biofuels: rule biofuel_tech_module: - message: "Create biofuel tech definition file from template." + message: "Create {wildcards.tech_module} tech definition file from template." input: - template = techs_template_dir + "supply/biofuel.yaml.jinja", + template = techs_template_dir + "supply/{tech_module}.yaml.jinja", biofuel_cost = "build/data/regional/biofuel/{scenario}/costs-eur-per-mwh.csv".format( scenario=config["parameters"]["jrc-biofuel"]["scenario"] ), locations = "build/data/{{resolution}}/biofuel/{scenario}/potential-mwh-per-year.csv".format(scenario=config["parameters"]["jrc-biofuel"]["scenario"]) params: scaling_factors = config["scaling-factors"], + biofuel_efficiency = config["parameters"]["biofuel-efficiency"] conda: "../envs/default.yaml" - output: "build/models/{resolution}/techs/supply/biofuel.yaml" + output: "build/models/{resolution}/techs/supply/{tech_module}.yaml" + wildcard_constraints: + tech_module = "biofuel|electrified-biofuel" script: "../scripts/biofuels/template_bio.py" diff --git a/scripts/biofuels/template_bio.py b/scripts/biofuels/template_bio.py index f2f17f23..23060277 100644 --- a/scripts/biofuels/template_bio.py +++ b/scripts/biofuels/template_bio.py @@ -10,5 +10,6 @@ snakemake.output[0], biofuel_cost=biofuel_cost, scaling_factors=snakemake.params.scaling_factors, + biofuel_efficiency=snakemake.params.biofuel_efficiency, locations=locations, ) diff --git a/templates/models/techs/supply/biofuel.yaml.jinja b/templates/models/techs/supply/biofuel.yaml.jinja index 15c5b834..1a2630ac 100644 --- a/templates/models/techs/supply/biofuel.yaml.jinja +++ b/templates/models/techs/supply/biofuel.yaml.jinja @@ -4,31 +4,13 @@ techs: name: Biofuel supply stream parent: supply_plus carrier: biofuel - constraints: - lifetime: 1 # arbritrarily chosen to avoid Calliope errors costs.monetary: om_prod: {{ biofuel_cost * scaling_factors.specific_costs }} # {{ (1 / scaling_factors.specific_costs) | unit("EUR/MWh") }} locations: {% for id, location in locations.iterrows() %} {{ id }}.techs.biofuel_supply: - {% endfor %} - -group_constraints: - {% for id, location in locations.iterrows() %} - biofuel_max_prod_{{ id }}: - locs: [{{ id }}] - techs: [biofuel_supply] - carrier_prod_max: - biofuel: {{ location.biofuel_potential_mwh_per_year * scaling_factors.power }} # {{ (1 / scaling_factors.power) | unit("MWh") }} - {% endfor %} - -overrides: - biofuel_flow_limits: - locations: - {% for id, location in locations.iterrows() %} - {{ id }}.techs.biofuel_supply: - constraints: - resource: {{ location.biofuel_potential_mwh_per_year / 8760 * scaling_factors.power }} # {{ (1 / scaling_factors.power) | unit("MW") }} - storage_cap_equals: {{ location.biofuel_potential_mwh_per_year / 2 * scaling_factors.power }} # {{ (1 / scaling_factors.power) | unit("MWh") }} (0.5x annual yield) # ASSUME < 1 for numerical range - {% endfor %} + constraints: + resource: {{ location.biofuel_potential_mwh_per_year / 8760 * scaling_factors.power }} # {{ (1 / scaling_factors.power) | unit("MWh") }} + storage_cap_equals: {{ location.biofuel_potential_mwh_per_year / 2 * scaling_factors.power }} # {{ (1 / scaling_factors.power) | unit("MWh") }} (0.5x annual yield) # ASSUME < 1 for numerical range + {% endfor %} \ No newline at end of file diff --git a/templates/models/techs/supply/electrified-biofuel.yaml.jinja b/templates/models/techs/supply/electrified-biofuel.yaml.jinja new file mode 100644 index 00000000..7ebd3b4c --- /dev/null +++ b/templates/models/techs/supply/electrified-biofuel.yaml.jinja @@ -0,0 +1,23 @@ +techs: + electrified_biofuel: # from [@JRC:2014] Table 48 Anaerobic digestion + essentials: + name: Biofuel-derived electricity + parent: supply_plus + carrier: electricity + constraints: + energy_eff: 1.0 # efficiency modelled within the input resource stream to avoid poor numerical scaling + lifetime: 20 + costs.monetary: + energy_cap: {{2300000 * scaling_factors.specific_costs}} # {{ (1 / scaling_factors.specific_costs) | unit("EUR2013/MW") }} + om_annual: {{2300000 * 0.041 * scaling_factors.specific_costs}} # {{ (1 / scaling_factors.specific_costs) | unit("EUR2013/MW") }} 4.1% of CAPEX + om_con: {{ (biofuel_cost / biofuel_efficiency + 3.1) * scaling_factors.specific_costs }} # {{ (1 / scaling_factors.specific_costs) | unit("EUR2013/MW") }} + om_prod: 0 # 3.1 (EUR2013/MWh) added to om_con because value is very small and causing poor numerical range + +locations: + {% for id, location in locations.iterrows() %} + {{ id }}.techs: + electrified_biofuel: + constraints: + resource: {{ location.biofuel_potential_mwh_per_year * biofuel_efficiency / 8760 * scaling_factors.power }} # {{ (1 / scaling_factors.power) | unit("MW") }} + storage_cap_equals: {{ location.biofuel_potential_mwh_per_year * biofuel_efficiency / 2 * scaling_factors.power }} # {{ (1 / scaling_factors.power) | unit("MWh") }} (0.5x annual yield) # ASSUME < 1 for numerical range + {% endfor %} diff --git a/tests/model/test_model.py b/tests/model/test_model.py index 20c69cbc..013518cf 100644 --- a/tests/model/test_model.py +++ b/tests/model/test_model.py @@ -1,38 +1,48 @@ import pandas as pd import pytest -DEFAULT_TECHNOLOGIES = set([ - "battery", - "hydrogen", - "open_field_pv", - "wind_onshore_competing", - "wind_onshore_monopoly", - "roof_mounted_pv", - "wind_offshore", - "hydro_run_of_river", - "hydro_reservoir", - "pumped_hydro", - "biofuel_supply", - "electricity_from_biofuel", - "demand_elec", - "nuclear", -]) -DIRECTIONAL_PV = set([ - "roof_mounted_pv_s_flat", - "roof_mounted_pv_n", - "roof_mounted_pv_e_w", -]) -HEAT_TECHS = set([ - "biofuel_boiler", - "biofuel_tech_heat_to_demand", - "heat_pump", - "heat_pump_tech_heat_to_demand", - "electric_heater", - "electric_heater_tech_heat_to_demand", - "hp_heat_storage_small", - "electric_heater_heat_storage_small", - "biofuel_heat_storage_small", -]) +DEFAULT_TECHNOLOGIES = set( + [ + "battery", + "hydrogen", + "open_field_pv", + "wind_onshore_competing", + "wind_onshore_monopoly", + "roof_mounted_pv", + "wind_offshore", + "hydro_run_of_river", + "hydro_reservoir", + "pumped_hydro", + "demand_elec", + "nuclear", + ] +) +DIRECTIONAL_PV = set( + [ + "roof_mounted_pv_s_flat", + "roof_mounted_pv_n", + "roof_mounted_pv_e_w", + ] +) +HEAT_TECHS = set( + [ + "biofuel_boiler", + "biofuel_tech_heat_to_demand", + "heat_pump", + "heat_pump_tech_heat_to_demand", + "electric_heater", + "electric_heater_tech_heat_to_demand", + "hp_heat_storage_small", + "electric_heater_heat_storage_small", + "biofuel_heat_storage_small", + ] +) +BIOFUEL_TECHS = set( + [ + "biofuel_supply", + "electricity_from_biofuel", + ] +) # Only includes scenarios with non-default technology sets TECHNOLOGIES = { "connected_all_neighbours": DEFAULT_TECHNOLOGIES | set(["ac_transmission"]), @@ -41,11 +51,19 @@ - set(["roof_mounted_pv"]), "shed-load": DEFAULT_TECHNOLOGIES | set(["load_shedding"]), "all-overrides": ( - (DEFAULT_TECHNOLOGIES | DIRECTIONAL_PV | set(["load_shedding"]) | HEAT_TECHS) + ( + DEFAULT_TECHNOLOGIES + | DIRECTIONAL_PV + | set(["load_shedding"]) + | HEAT_TECHS + | BIOFUEL_TECHS + ) - set(["roof_mounted_pv"]) ), "electrified-heat": DEFAULT_TECHNOLOGIES | set(["historic_electrified_heat"]), + "electrified-biofuel": DEFAULT_TECHNOLOGIES | set(["electrified_biofuel"]), "heat": DEFAULT_TECHNOLOGIES | HEAT_TECHS, + "biofuel": DEFAULT_TECHNOLOGIES | BIOFUEL_TECHS, } OPTIONAL_LOCATIONAL_TECHNOLOGIES = ["nuclear"] diff --git a/tests/model/test_runner.py b/tests/model/test_runner.py index d2ff8447..880e6487 100644 --- a/tests/model/test_runner.py +++ b/tests/model/test_runner.py @@ -9,9 +9,7 @@ def run_test(snakemake): - with open( - os.path.join(snakemake.input.test_dir, "..", "resources", "test.yaml") - ) as f: + with open(os.path.join(snakemake.input.test_resources_dir, "test.yaml")) as f: test_config = yaml.safe_load(f) override_dict = test_config["test-model"]["overrides"][ diff --git a/tests/resources/test.yaml b/tests/resources/test.yaml index 6ddbb267..ada35c7d 100644 --- a/tests/resources/test.yaml +++ b/tests/resources/test.yaml @@ -18,18 +18,19 @@ test-model: alternative-cost: ["dea-renewable-cost-pv-open-field", "dea-renewable-cost-wind-onshore", "dea-renewable-cost-wind-offshore", "dea-renewable-cost-pv-roof-mounted", "schroeder-hydro-cost"] shed-load: ["load-shedding"] keep-historic-transport: ["keep-historic-electricity-demand-from-road-transport"] - heat: ["heat_end_use"] + heat: ["heat_carrier"] electrified-heat: ["electrified_heat"] - biofuel-flow-limited: ["biofuel_flow_limits"] + biofuel: ["biofuel_carrier"] + electrified-biofuel: ["electrified_biofuel"] regional: default: ["connect_all_neighbours", "run_barrier_no_crossover"] all-overrides: ["connect_all_neighbours", "directional-rooftop-pv", "exclusive-energy-to-power-ratios", - "dea-renewable-cost-pv-open-field", "dea-renewable-cost-wind-onshore", "dea-renewable-cost-wind-offshore", "dea-renewable-cost-pv-roof-mounted", "schroeder-hydro-cost", "freeze-hydro-supply-capacities", "freeze-hydro-storage-capacities", "load-shedding", "heat_end_use", "biofuel_flow_limits"] + "dea-renewable-cost-pv-open-field", "dea-renewable-cost-wind-onshore", "dea-renewable-cost-wind-offshore", "dea-renewable-cost-pv-roof-mounted", "schroeder-hydro-cost", "freeze-hydro-supply-capacities", "freeze-hydro-storage-capacities", "load-shedding", "heat_carrier", "biofuel_carrier"] ehighways: default: ["connect_all_neighbours", "run_barrier_no_crossover"] all-overrides: ["connect_all_neighbours", "directional-rooftop-pv", "exclusive-energy-to-power-ratios", - "dea-renewable-cost-pv-open-field", "dea-renewable-cost-wind-onshore", "dea-renewable-cost-wind-offshore", "dea-renewable-cost-pv-roof-mounted", "schroeder-hydro-cost", "freeze-hydro-supply-capacities", "freeze-hydro-storage-capacities", "load-shedding", "heat_end_use", "biofuel_flow_limits"] + "dea-renewable-cost-pv-open-field", "dea-renewable-cost-wind-onshore", "dea-renewable-cost-wind-offshore", "dea-renewable-cost-pv-roof-mounted", "schroeder-hydro-cost", "freeze-hydro-supply-capacities", "freeze-hydro-storage-capacities", "load-shedding", "heat_carrier", "biofuel_carrier"] overrides: continental: {} national: @@ -43,25 +44,39 @@ test-model: import: - "build/models/national/techs/demand/electrified-heat.yaml" - "build/models/national/techs/supply/historic-electrified-heat.yaml" - heat_end_use: + heat_carrier: import: - "build/models/national/techs/demand/heat.yaml" - "build/models/national/techs/storage/heat.yaml" - "build/models/national/techs/conversion/heat-from-electricity.yaml" - "build/models/national/techs/conversion/heat-from-biofuel.yaml" - "build/models/national/techs/supply/historic-electrified-heat.yaml" + electrified_biofuel: + import: + - "build/models/national/techs/supply/electrified-biofuel.yaml" + biofuel_carrier: + import: + - "build/models/national/techs/supply/biofuel.yaml" + - "build/models/national/techs/conversion/electricity-from-biofuel.yaml" regional: connect_all_neighbours: import: - 'build/models/regional/techs/transmission/electricity-linked-neighbours.yaml' - heat_end_use: + heat_carrier: import: - "build/models/regional/techs/demand/heat.yaml" - "build/models/regional/techs/storage/heat.yaml" - "build/models/regional/techs/conversion/heat-from-electricity.yaml" - "build/models/regional/techs/conversion/heat-from-biofuel.yaml" - "build/models/regional/techs/supply/historic-electrified-heat.yaml" + electrified_biofuel: + import: + - "build/models/regional/techs/supply/electrified-biofuel.yaml" + biofuel_carrier: + import: + - "build/models/regional/techs/supply/biofuel.yaml" + - "build/models/regional/techs/conversion/electricity-from-biofuel.yaml" run_barrier_no_crossover: run.solver_options: {Method: 2, Crossover: 0} @@ -69,13 +84,20 @@ test-model: connect_all_neighbours: import: - 'build/models/ehighways/techs/transmission/electricity-linked-neighbours.yaml' - heat_end_use: + heat_carrier: import: - "build/models/ehighways/techs/demand/heat.yaml" - "build/models/ehighways/techs/storage/heat.yaml" - "build/models/ehighways/techs/conversion/heat-from-electricity.yaml" - "build/models/ehighways/techs/conversion/heat-from-biofuel.yaml" - "build/models/ehighways/techs/supply/historic-electrified-heat.yaml" + electrified_biofuel: + import: + - "build/models/ehighways/techs/supply/electrified-biofuel.yaml" + biofuel_carrier: + import: + - "build/models/ehighways/techs/supply/biofuel.yaml" + - "build/models/ehighways/techs/conversion/electricity-from-biofuel.yaml" run_barrier_no_crossover: run.solver_options: {Method: 2, Crossover: 0}