Skip to content

Commit

Permalink
Multi-modal network creation for global trade
Browse files Browse the repository at this point in the history
  • Loading branch information
thomas-fred committed Sep 12, 2024
1 parent 878c68b commit ba1eb31
Show file tree
Hide file tree
Showing 6 changed files with 483 additions and 16 deletions.
37 changes: 34 additions & 3 deletions config/config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
##########################
### TRANSPORT WORKFLOW ###
##########################
##################################
### TRANSPORT DAMAGES WORKFLOW ###
##################################

# OSM datasets in PBF format, principally from: https://download.geofabrik.de/ #
infrastructure_datasets:
Expand Down Expand Up @@ -66,6 +66,37 @@ keep_tags:
# Number of slices to cut dataset into -- must be a square number
slice_count: 64

#####################################
### TRANSPORT DISRUPTION WORKFLOW ###
#####################################

# country for which trade OD has been prepared, and we are to route land trade flows
study_country_iso_a3: "THA"

# transport cost information
# road
road_cost_USD_t_km: 0.05
road_cost_USD_t_h: 0.48
road_default_speed_limit_km_h: 80
# rail
rail_cost_USD_t_km: 0.05
rail_cost_USD_t_h: 0.38
rail_average_freight_speed_km_h: 40

# cost of changing transport mode in USD per tonne
# from mistral/ccg-critical-minerals/processed_data/transport_costs/intermodal.xlsx, 20240611
intermodal_cost_USD_t:
road_rail: 5
maritime_road: 4
maritime_rail: 5

# drop trade flows with less volume than this (accelerate flow allocation)
# N.B. 50t threshold preserves 91% of total volume and 88% of total value
# for combined agriculture & manufacturing flows between Thailand road nodes -> partner countries
minimum_flow_volume_t: 50

# if disrupting a network, remove edges experiencing hazard values in excess of this
edge_failure_threshold: 0.5

#########################
### FLOODING WORKFLOW ###
Expand Down
4 changes: 2 additions & 2 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
name: open-gira
channels:
- conda-forge # majority of dependencies
- bioconda # snakemake
- defaults
dependencies:
- python=3.10
Expand All @@ -25,6 +24,7 @@ dependencies:
- gdal>=3.3 # command-line tools for spatial data
- geopandas==0.14.4 # geospatial dataframes
- geopy # geocoding client
- igraph # graph algorithms and data structures
- ipykernel # notebook support
- jupyter # notebook support
- jq # JSON processing tool
Expand All @@ -50,7 +50,7 @@ dependencies:
- rioxarray # xarray datasets from raster files
- scipy # scientific computing library
- spatialpandas # plotting large datasets
- snakemake==7.18.2 # workflow management
- bioconda::snakemake==7.18.2 # workflow management
# https://github.com/snakemake/snakemake/issues/1891
- tabulate==0.8.10 # snakemake dependency with bug in 9.0.0, pin previous
- tqdm==4.62.3 # progress bars
Expand Down
26 changes: 15 additions & 11 deletions workflow/Snakefile
Original file line number Diff line number Diff line change
Expand Up @@ -76,27 +76,29 @@ if not config["best_estimate_windspeed_failure_threshold"] in config["transmissi

# Constrain wildcards to NOT use _ or /
wildcard_constraints:
ADMIN_SLUG="admin-level-[0-4]",
AGG_FUNC_SLUG="agg-sum",
CHUNK_SLUG="chunk-[\d]+",
COST_OR_FRACTION="cost|fraction",
DATASET="[^_/]+",
DIRECT_DAMAGE_TYPES="fraction_per_RP|cost_per_RP|EAD|EAD_and_cost_per_RP",
EVENTS_OR_FIXED="events|fixed",
FILTER_SLUG="filter-[^_/]+",
FILENAME="[^/]+",
HAZARD_SLUG="hazard-[^_/]+",
IRIS_SCENARIO="PRESENT|SSP1-2050|SSP2-2050|SSP5-2050",
# the output dir must end in 'results'
# e.g. '/data/open-gira/results', './results', '/my-results', etc.
# this prevents us matching past it into other folders for an incorrect OUTPUT_DIR,
# but is more flexible than reading a value from, for example, config.yaml
OUTPUT_DIR="^.*results",
DATASET="[^_/]+",
PROJECT_SLUG="project-[^_/]+",
SLICE_SLUG="slice-[0-9]+",
FILTER_SLUG="filter-[^_/]+",
HAZARD_SLUG="hazard-[^_/]+",
ADMIN_SLUG="admin-level-[0-4]",
AGG_FUNC_SLUG="agg-sum",
FILENAME="[^/]+",
STORM_BASIN="EP|NA|NI|SI|SP|WP",
STORM_RP="[0-9]+",
IRIS_SCENARIO="PRESENT|SSP1-2050|SSP2-2050|SSP5-2050",
STORM_MODEL="constant|CMCC-CM2-VHR4|CNRM-CM6-1-HR|EC-Earth3P-HR|HadGEM3-GC31-HM",
STORM_MODEL_FUTURE="CMCC-CM2-VHR4|CNRM-CM6-1-HR|EC-Earth3P-HR|HadGEM3-GC31-HM",
STORM_RP="[0-9]+",
STORM_SET="(?:IBTrACS|STORM|IRIS)[^\/]*",
EVENTS_OR_FIXED="events|fixed",
COST_OR_FRACTION="cost|fraction",
DIRECT_DAMAGE_TYPES="fraction_per_RP|cost_per_RP|EAD|EAD_and_cost_per_RP",
SAMPLE="\d+",
# may be upper or lower, one 'f' or two
TIFF_FILE="[^\/\.\s]+\.[tT][iI][fF][fF]?",
Expand Down Expand Up @@ -130,6 +132,8 @@ include: "transport/create_network.smk"
include: "transport/osm_to_geoparquet.smk"
include: "transport/create_overall_bbox.smk"
include: "transport/join_data.smk"
include: "transport/maritime/maritime.smk"
include: "transport/multi-modal/multi_modal.smk"

include: "flood/aqueduct.smk"
include: "flood/trim_hazard_data.smk"
Expand Down
155 changes: 155 additions & 0 deletions workflow/transport/maritime/maritime.smk
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
rule create_maritime_network:
input:
nodes = "{OUTPUT_DIR}/input/networks/maritime/nodes.gpq",
edges_no_geom = "{OUTPUT_DIR}/input/networks/maritime/edges_by_cargo/maritime_base_network_general_cargo.pq",
edges_visualisation = "{OUTPUT_DIR}/input/networks/maritime/edges.gpq",
output:
nodes = "{OUTPUT_DIR}/maritime_network/nodes.gpq",
edges = "{OUTPUT_DIR}/maritime_network/edges.gpq",
run:
import igraph as ig
import geopandas as gpd
from shapely.geometry import Point
from shapely.ops import linemerge
from tqdm import tqdm

from open_gira.network_creation import preprocess_maritime_network

# possible cargo types = ("container", "dry_bulk", "general_cargo", "roro", "tanker")
# for now, just use 'general_cargo'
maritime_nodes, maritime_edges_no_geom = preprocess_maritime_network(
input.nodes,
input.edges_no_geom
)

if config["study_country_iso_a3"] == "THA":
# put Bangkok port in the right place...
maritime_nodes.loc[maritime_nodes.name == "Bangkok_Thailand", "geometry"] = Point((100.5753, 13.7037))

# %%
# Jasper's maritime edges in 'edges_by_cargo' do not contain geometry
# this is because the AIS data that they were derived from only contain origin and destination port, not route
# this is a pain for visualisation, so we will create a geometry for each from `maritime_vis_edges`

maritime_vis_edges = gpd.read_parquet(input.edges_visualisation)
vis_graph = ig.Graph.DataFrame(maritime_vis_edges, directed=True, use_vids=False)

maritime_edges = maritime_edges_no_geom.copy()
change_of_port_mask = maritime_edges_no_geom.from_port != maritime_edges_no_geom.to_port
port_pairs_to_generate_geom_for = maritime_edges_no_geom[change_of_port_mask]
for index, row in tqdm(port_pairs_to_generate_geom_for.iterrows(), total=len(port_pairs_to_generate_geom_for)):
edge_list = vis_graph.get_shortest_path(row.from_port, row.to_port, weights="distance", output="epath")
route_edges = maritime_vis_edges.iloc[edge_list]
route_linestring = linemerge(list(route_edges.geometry))
maritime_edges.loc[index, "geometry"] = route_linestring

maritime_edges = gpd.GeoDataFrame(maritime_edges).set_crs(epsg=4326)

maritime_nodes.to_parquet(output.nodes)
maritime_edges.to_parquet(output.edges)


rule plot_maritime_network:
input:
nodes = "{OUTPUT_DIR}/maritime_network/nodes.gpq",
edges = "{OUTPUT_DIR}/maritime_network/edges.gpq",
output:
edges_plot = "{OUTPUT_DIR}/maritime_network/edges.png",
run:
import geopandas as gpd
import matplotlib
import matplotlib.pyplot as plt
import numpy as np

from open_gira.plot.utils import chop_at_antimeridian

matplotlib.use("Agg")
plt.style.use("bmh")

world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))
world.geometry = world.geometry.boundary

maritime_nodes = gpd.read_parquet(input.nodes)
maritime_edges = gpd.read_parquet(input.edges)

# whole network
f, ax = plt.subplots(figsize=(16, 7))
chop_at_antimeridian(maritime_edges, drop_null_geometry=True).plot(
ax=ax,
linewidth=0.5,
alpha=0.8
)
world.plot(ax=ax, lw=0.5, alpha=0.2)
ax.set_xticks(np.linspace(-180, 180, 13))
ax.set_yticks([-60, -30, 0, 30, 60])
ax.set_ylim(-65, 85)
ax.set_xlim(-180, 180)
ax.grid(alpha=0.3)
ax.set_xlabel("Longitude [deg]")
ax.set_ylabel("Latitude [deg]")
f.savefig(output.edges_plot)


rule plot_port_connections:
input:
nodes = "{OUTPUT_DIR}/maritime_network/nodes.gpq",
edges = "{OUTPUT_DIR}/maritime_network/edges.gpq",
output:
port_trade_plots = directory("{OUTPUT_DIR}/maritime_network/port_trade_plots"),
run:
import os

import geopandas as gpd
import matplotlib
import matplotlib.pyplot as plt
from tqdm import tqdm

from open_gira.plot.utils import chop_at_antimeridian

matplotlib.use("Agg")
plt.style.use("bmh")

maritime_nodes = gpd.read_parquet(input.nodes)
maritime_edges = gpd.read_parquet(input.edges)

# disambiguate the global view and plot the routes from each port, one port at a time
world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))
world.geometry = world.geometry.boundary

ports = maritime_edges.from_port.unique()
os.makedirs(output.port_trade_plots)
for port_id in tqdm(ports):
filepath = os.path.join(output.port_trade_plots, f"{port_id}.png")
f, ax = plt.subplots(figsize=(10,10))
port = maritime_nodes[maritime_nodes.id == f"{port_id}_land"]
routes = maritime_edges[maritime_edges.from_port == port_id]
if all(routes.geometry.isna()):
continue
try:
chop_at_antimeridian(routes, drop_null_geometry=True).plot(
column="to_port",
categorical=True,
ax=ax
)
except ValueError:
print(f"Failed to plot {port_id}, skipping...")
continue
maritime_nodes[maritime_nodes.id == f"{port_id}_land"].plot(
ax=ax,
markersize=500,
marker="*",
facecolor="none",
color="r"
)
xmin, xmax = ax.get_xlim()
ymin, ymax = ax.get_ylim()
world.plot(ax=ax, linewidth=0.5, alpha=0.4)
ax.set_xlim(xmin, xmax)
ax.set_ylim(ymin, ymax)
port_name, = port.name
ax.set_title(f"{port_id} ({port_name.replace('_', ', ')}) estimated routes")
ax.get_xaxis().set_visible(False)
ax.get_yaxis().set_visible(False)

f.savefig(filepath)
plt.close(f)
Loading

0 comments on commit ba1eb31

Please sign in to comment.