From f27ba493971254facd8c9d9204371312e77cceb3 Mon Sep 17 00:00:00 2001 From: energyls Date: Wed, 10 Jun 2026 12:02:52 +0200 Subject: [PATCH 1/4] feat: include new assumptions in config --- config/config.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/config/config.yaml b/config/config.yaml index f287fac..461fc64 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -218,6 +218,13 @@ trade: ports: "predefined" # "closest": route from region centroid (searoute snaps to nearest sea node); "predefined": route from hand-curated port city coordinates parsed from the notes column in trade_opt.csv diversity_factor: False # "False" or float as max constraint for one single hbi supply route in the trade model. 1: no limit, 0.5: max 50% of demand can be supplied by one single supply route, etc. # Note: The diversity factor constraints the trade route, not the producer itself. (Import from exporter route R <= diversity_factor * demand of importer I.). As a result, if the trade passes through one region, it is limited based on the demand of this particular region * diversity_factor. Therefore the trade routes in a scenario with a diversity_factor of 100% may differ to a scenario where diversity_factor=False. + shipping: + port_loading: 7 # USD/t, for each loading, see 10.1038/s41467-023-38123-2 supplementary S12 + nh3_cost: 460 # USD/t_NH3, see 10.1038/s41467-023-38123-2 supplementary S12 + nh3_consumption: 0.2 # t_NH3/nm, see 10.1038/s41467-023-38123-2 supplementary S12 + charter_rate: 0.0008 # USD/(t*km), see 10.1038/s41467-023-38123-2 supplementary S12 + panamax_load: 80000 # t + nm_to_km: 1.852 # 1 nautical mile equals 1.852 kilometers grid_electricity: marginal_cost: 80 # EUR/MWh, guesstimate for average grid electricity cost in 2030 From 4b61f47192d4846149fef336f25f91c24ce6f230 Mon Sep 17 00:00:00 2001 From: energyls Date: Wed, 10 Jun 2026 12:03:04 +0200 Subject: [PATCH 2/4] chore: remove depreciated dataset --- rules/trade_model.smk | 1 - 1 file changed, 1 deletion(-) diff --git a/rules/trade_model.smk b/rules/trade_model.smk index ac71ce2..4bf4f3f 100644 --- a/rules/trade_model.smk +++ b/rules/trade_model.smk @@ -21,7 +21,6 @@ rule model_trade: region=config["regions"], intertwo=["steel"], ), - transport_costs="data/transport_costs/steel_r_iron_r.csv", trade_options="resources/trade_opt_chokepoints.csv", bus_locations="data/bus_locations.csv", demand="data/un_enerdata_demand_2050_final.csv", From d5ed536cc2b0eb831951e670527bd2844130b7e6 Mon Sep 17 00:00:00 2001 From: energyls Date: Wed, 10 Jun 2026 12:03:14 +0200 Subject: [PATCH 3/4] feat: revise transport methodology --- workflow/scripts/model_trade.py | 99 ++++++++++----------------------- 1 file changed, 28 insertions(+), 71 deletions(-) diff --git a/workflow/scripts/model_trade.py b/workflow/scripts/model_trade.py index d8f49ef..84a9276 100644 --- a/workflow/scripts/model_trade.py +++ b/workflow/scripts/model_trade.py @@ -260,47 +260,29 @@ def building_model( return n -def create_links(transport_costs, trade_options): +def create_links(trade_options): # for in range of length of input csv with all the different links, region_from = column , region_to = column 2 # create links with the correct corresponding costs - # marginal and fixed cost for the different type of transport - ship_mc = float( - transport_costs.loc[transport_costs["transport_type"] == "shipping"] - .loc[:, "marginal_cost"] - .values[0] - ) - pipe_mc = float( - transport_costs.loc[transport_costs["transport_type"] == "pipeline"] - .loc[:, "marginal_cost"] - .values[0] - ) - ship_c = float( - transport_costs.loc[transport_costs["transport_type"] == "shipping"] - .loc[:, "fixed_cost"] - .values[0] - ) + shipping = snakemake.config["trade"]["shipping"] - ship_iron_ore_mc = ( - transport_costs.loc[transport_costs["transport_type"] == "shipping_iron_ore"] - .loc[:, "marginal_cost"] - .values[0] - ) + port_loading = shipping["port_loading"] + nh3_cost = shipping["nh3_cost"] # USD/t_NH3 + nh3_consumption = shipping["nh3_consumption"] # t_NH3/nm + charter_rate = shipping["charter_rate"] # USD/(t*km) + panamax_load = shipping["panamax_load"] # t + nm_to_km = shipping["nm_to_km"] + eur_usd = snakemake.config["techno-economic parameters"]["eur_usd"] - ship_interone_mc = ( - transport_costs.loc[transport_costs["transport_type"] == f"shipping_{interone}"] - .loc[:, "marginal_cost"] - .values[0] - ) - input_demand = 0.42 # MWh/km for LH2, IEA future of hydrogen 2019 - boat_capacity = 363000 # MWh for LH2, IEA future of hydrogen 2019 - speed = 30 # km/h, IEA future of hydrogen 2019 - BOG = 0.2 / 100 # %/day, IEA future of hydrogen 2019 + variable_cost = ( + (nh3_cost * nh3_consumption / nm_to_km) / panamax_load + charter_rate + ) / eur_usd # EUR/(t*km) + fixed_cost = port_loading * 2 / eur_usd # EUR/t - logger.info(f"ship + pipe cost {ship_mc} {ship_c} {pipe_mc}") - logger.info(f"shipping cost {interone} {ship_interone_mc} EUR/(t*km)") - logger.info(f"shipping cost iron ore {ship_iron_ore_mc} EUR/(t*km)") + logger.info( + f"variable shipping cost {variable_cost:.6f} EUR/(t*km) and fixed cost {fixed_cost:.6f} EUR/t applied to shipping links" + ) # if there should be a link, create a link # do this for both shipping and pipeline @@ -310,27 +292,10 @@ def create_links(transport_costs, trade_options): r_from = trade_options["region_from"][r] r_to = trade_options["region_to"][r] - # If shipping costs are made up from marginal and capital - # total_cost = ship_c + int( - # float(trade_options["shipping_distance [km]"][r]) * ship_mc - # ) - # If shipping costs are made up from marginal only - total_cost_interone = ship_interone_mc * float( - trade_options["shipping_distance [km]"][r] - ) - total_cost = total_cost_interone - - # calculating efficiency - days_at_sea = ( - float(trade_options["shipping_distance [km]"][r]) / speed - ) / 24 - tot_BOG = 1 - (1 - BOG) ** days_at_sea - tot_fuel_demand = ( - (2 * float(trade_options["shipping_distance [km]"][r])) - * input_demand - / boat_capacity - ) - eff = 1 - max(tot_BOG, tot_fuel_demand) + shipping_cost = ( + variable_cost * float(trade_options["shipping_distance [km]"][r]) + + fixed_cost + ) # EUR/t n.add( "Link", @@ -338,24 +303,18 @@ def create_links(transport_costs, trade_options): carrier="shipping_" + interone, bus0=r_from + "_" + interone, bus1=r_to + "_" + interone, - efficiency=eff, # %, calculated above - marginal_cost=total_cost, # EUR/MWh or EUR/t + efficiency=1, + marginal_cost=shipping_cost, capital_cost=1 / 1000, # to prevent optimisation shenenigans p_nom_extendable=True, ) logger.info( - "shipping %s link made from %s to %s - eff %s", + "shipping %s link made from %s to %s", interone, r_from, r_to, - eff, ) - # Add iron ore shipping link - total_cost_iron_ore = ship_iron_ore_mc * float( - trade_options["shipping_distance [km]"][r] - ) # TODO Capital cost are not separate but included - n.add( "Link", "shipping iron ore {}-{}".format(r_from, r_to), @@ -363,15 +322,14 @@ def create_links(transport_costs, trade_options): bus0=r_from + "_ore", bus1=r_to + "_ore", efficiency=1, - marginal_cost=total_cost_iron_ore, # EUR/t_ironore + marginal_cost=shipping_cost, # EUR/t_ironore capital_cost=1 / 1000, # to prevent optimisation shenenigans p_nom_extendable=True, ) logger.info( - "iron ore shipping link made from %s_ore to %s_ore - eff %s", + "iron ore shipping link made from %s_ore to %s_ore", r_from, r_to, - eff, ) # checking if the row connects with pipeline @@ -1125,11 +1083,11 @@ def _link_weight(link_name): "model_trade", cost_year="2050", interone="hbi", - intertwo="steel", + intertwo="eaf", final="steel", scenario="default", wacc="regional", - chain_id="labour_2050", + chain_id="newre_2050", ) final = snakemake.wildcards["final"] @@ -1160,7 +1118,6 @@ def _link_weight(link_name): logger.info("starting up with all regions--- ") # making dataframes - transport_costs = pd.read_csv(snakemake.input.transport_costs, header=0) trade_options = pd.read_csv(snakemake.input.trade_options, header=0) supply_curves_interone = snakemake.input.supply_curves_interone supply_curves_intertwo = snakemake.input.supply_curves_intertwo @@ -1222,7 +1179,7 @@ def _link_weight(link_name): # building transport network connecting the individual buses logger.info("building transportation links") - create_links(transport_costs, trade_options) + create_links(trade_options) # Cost penalty if snakemake.config["scenario"][scenario]["modifiers"]["cost_penalty"] is None: From 97f59a51748eeb7dcfdc9bfb15010f15fad7d4ac Mon Sep 17 00:00:00 2001 From: energyls Date: Wed, 10 Jun 2026 12:04:45 +0200 Subject: [PATCH 4/4] feat: add notebook to analyse transport cost --- .../notebooks/analysis-transport-cost.ipynb | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 workflow/notebooks/analysis-transport-cost.ipynb diff --git a/workflow/notebooks/analysis-transport-cost.ipynb b/workflow/notebooks/analysis-transport-cost.ipynb new file mode 100644 index 0000000..ac2aa97 --- /dev/null +++ b/workflow/notebooks/analysis-transport-cost.ipynb @@ -0,0 +1,102 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "b0311363", + "metadata": {}, + "outputs": [], + "source": [ + "import pypsa\n", + "import pandas as pd" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a98efb58", + "metadata": {}, + "outputs": [], + "source": [ + "n = pypsa.Network(\"../../results/chain_id~newre_2050/cost_year~2050/interone~hbi/intertwo~eaf/wacc~regional/final~steel/scenario~default/network.nc\")" + ] + }, + { + "cell_type": "markdown", + "id": "5a18fba6", + "metadata": {}, + "source": [ + "### Transport efficiency" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6edd6dee", + "metadata": {}, + "outputs": [], + "source": [ + "n.links[n.links.carrier == \"shipping_iron_ore\"].efficiency.describe()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2a3a5201", + "metadata": {}, + "outputs": [], + "source": [ + "n.links[n.links.carrier == \"shipping_hbi\"].efficiency.describe()" + ] + }, + { + "cell_type": "markdown", + "id": "b69794c5", + "metadata": {}, + "source": [ + "### Transport cost" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b6b82f5f", + "metadata": {}, + "outputs": [], + "source": [ + "n.links[n.links.carrier == \"shipping_iron_ore\"].marginal_cost.describe()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e5942c78", + "metadata": {}, + "outputs": [], + "source": [ + "n.links[n.links.carrier == \"shipping_hbi\"].marginal_cost.describe()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "shift", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}