Cavern storage capacity#

import os

import cartopy.crs as ccrs
import contextily as cx
import geopandas as gpd
import mapclassify as mc
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
from cartopy.mpl.ticker import LatitudeFormatter, LongitudeFormatter

# from matplotlib.lines import Line2D
from matplotlib_scalebar.scalebar import ScaleBar

from h2ss import capacity as cap
from h2ss import compare
from h2ss import data as rd
from h2ss import functions as fns
# basemap cache directory
cx.set_cache_dir(os.path.join("data", "basemaps"))

Halite data#

ds, extent = rd.kish_basin_data_depth_adjusted(
    dat_path=os.path.join("data", "kish-basin"),
    bathymetry_path=os.path.join("data", "bathymetry"),
)
xmin, ymin, xmax, ymax = extent.total_bounds

Constraints#

# exploration wells
_, wells_b = fns.constraint_exploration_well(
    data_path=os.path.join(
        "data",
        "exploration-wells",
        "Exploration_Wells_Irish_Offshore.shapezip.zip",
    )
)

# wind farms
wind_farms = fns.constraint_wind_farm(
    data_path=os.path.join(
        "data", "wind-farms", "marine-area-consent-wind.zip"
    )
)

# frequent shipping routes
_, shipping_b = fns.constraint_shipping_routes(
    data_path=os.path.join(
        "data", "shipping", "shipping_frequently_used_routes.zip"
    ),
    dat_extent=extent,
)

# shipwrecks
_, shipwrecks_b = fns.constraint_shipwrecks(
    data_path=os.path.join(
        "data", "shipwrecks", "IE_GSI_MI_Shipwrecks_IE_Waters_WGS84_LAT.zip"
    ),
    dat_extent=extent,
)

# subsea cables
_, cables_b = fns.constraint_subsea_cables(
    data_path=os.path.join("data", "subsea-cables", "KIS-ORCA.gpkg"),
    dat_extent=extent,
)
# distance from salt formation edge
edge_buffer = fns.constraint_halite_edge(dat_xr=ds)

Zones of interest#

zones, zds = fns.zones_of_interest(
    dat_xr=ds,
    constraints={"net_height": 120, "min_depth": 500, "max_depth": 2000},
)

Generate caverns#

caverns = fns.generate_caverns_hexagonal_grid(
    zones_df=zones,
    dat_extent=extent,
)
caverns = fns.cavern_dataframe(
    dat_zone=zds,
    cavern_df=caverns,
    depths={"min": 500, "min_opt": 1000, "max_opt": 1500, "max": 2000},
)
# label caverns by depth and heights
caverns = fns.label_caverns(
    cavern_df=caverns,
    heights=[120],
    depths={"min": 500, "min_opt": 1000, "max_opt": 1500, "max": 2000},
)
caverns, _ = fns.generate_caverns_with_constraints(
    cavern_df=caverns,
    exclusions={
        "wells": wells_b,
        "wind_farms": wind_farms,
        "shipwrecks": shipwrecks_b,
        "shipping": shipping_b,
        "cables": cables_b,
        "edge": edge_buffer,
    },
)
Without constraints...
Number of potential caverns: 568
------------------------------------------------------------
Excluding salt formation edges...
Number of potential caverns: 539
------------------------------------------------------------
Exclude shipping...
Number of potential caverns: 261
Caverns excluded: 51.58%
------------------------------------------------------------
Exclude wind farms...
Number of potential caverns: 218
Caverns excluded: 59.55%
------------------------------------------------------------
Exclude cables...
Number of potential caverns: 218
Caverns excluded: 59.55%
------------------------------------------------------------
Exclude wells...
Number of potential caverns: 218
Caverns excluded: 59.55%
------------------------------------------------------------
Exclude shipwrecks...
Number of potential caverns: 218
Caverns excluded: 59.55%
------------------------------------------------------------

Capacity#

Cavern volume#

caverns["cavern_total_volume"] = cap.cavern_volume(
    height=caverns["cavern_height"]
)
caverns["cavern_volume"] = cap.corrected_cavern_volume(
    v_cavern=caverns["cavern_total_volume"]
)

Mid-point temperature#

caverns["t_mid_point"] = cap.temperature_cavern_mid_point(
    height=caverns["cavern_height"], depth_top=caverns["cavern_depth"]
)

Operating pressure#

(
    caverns["p_operating_min"],
    caverns["p_operating_max"],
) = cap.pressure_operating(
    thickness_overburden=caverns["TopDepthSeabed"],
    depth_water=-caverns["Bathymetry"],
)

Hydrogen gas density#

caverns["rho_min"], caverns["rho_max"] = cap.density_hydrogen_gas(
    p_operating_min=caverns["p_operating_min"],
    p_operating_max=caverns["p_operating_max"],
    t_mid_point=caverns["t_mid_point"],
)

Working mass of hydrogen#

(
    caverns["working_mass"],
    caverns["mass_operating_min"],
    caverns["mass_operating_max"],
) = cap.mass_hydrogen_working(
    rho_h2_min=caverns["rho_min"],
    rho_h2_max=caverns["rho_max"],
    v_cavern=caverns["cavern_volume"],
)

Energy storage capacity in GWh#

caverns["capacity"] = cap.energy_storage_capacity(
    m_working=caverns["working_mass"]
)

Stats#

# proportion of working gas to total gas
caverns["working_mass_pct"] = caverns["working_mass"] / (
    caverns["working_mass"] + caverns["mass_operating_min"]
)
caverns.drop(
    columns=[
        "x",
        "y",
        "BaseDepth",
        "TopDepth",
        "TopTWT",
        "BaseDepthSeabed",
        "cavern_height",
        "cavern_total_volume",
        "cavern_volume",
    ]
).describe()
Thickness TopDepthSeabed Bathymetry NetToGross ThicknessNet cavern_depth t_mid_point p_operating_min p_operating_max rho_min rho_max working_mass mass_operating_min mass_operating_max capacity working_mass_pct
count 218.000000 218.000000 218.000000 218.000000 218.000000 218.000000 218.000000 2.180000e+02 2.180000e+02 218.000000 218.000000 2.180000e+02 2.180000e+02 2.180000e+02 218.000000 218.000000
mean 429.724354 1074.020877 -50.338910 0.651980 283.015980 1154.020877 328.675783 8.061902e+06 2.149840e+07 5.608686 13.867715 3.260108e+06 2.213931e+06 5.474039e+06 108.634041 0.597511
std 72.560913 365.142804 13.127985 0.043909 69.215822 365.142804 13.692855 2.580140e+06 6.880373e+06 1.508555 3.480605 7.786651e+05 5.954757e+05 1.373908e+06 25.946851 0.007822
min 358.331500 422.745339 -73.351486 0.593197 212.561041 502.745339 304.252950 3.445968e+06 9.189247e+06 2.691914 6.945301 1.678950e+06 1.062586e+06 2.741537e+06 55.946359 0.581421
25% 384.750500 792.651330 -62.446541 0.617639 237.636862 872.651330 318.124425 6.071888e+06 1.619170e+07 4.470379 11.273416 2.685380e+06 1.764604e+06 4.449985e+06 89.482842 0.591309
50% 413.253450 1048.329146 -53.034470 0.644009 266.138995 1128.329146 327.712343 7.887979e+06 2.103461e+07 5.589250 13.892191 3.277441e+06 2.206259e+06 5.483701e+06 109.211632 0.597670
75% 446.045600 1353.385847 -41.985056 0.674348 300.789815 1433.385847 339.151969 1.004202e+07 2.677871e+07 6.804421 16.649315 3.886101e+06 2.685927e+06 6.572028e+06 129.493525 0.603458
max 822.710300 1900.680682 -18.320551 0.750000 617.032725 1980.680682 359.675526 1.382934e+07 3.687824e+07 8.691559 20.764429 4.765555e+06 3.430842e+06 8.196397e+06 158.798899 0.612412
# fixed values
caverns[["cavern_height", "cavern_total_volume", "cavern_volume"]].iloc[0]
cavern_height             120.000000
cavern_total_volume    563903.723294
cavern_volume          394732.606306
Name: 0, dtype: float64
# totals
caverns[
    [
        "cavern_volume",
        "working_mass",
        "capacity",
        "mass_operating_min",
        "mass_operating_max",
    ]
].sum()
cavern_volume         8.605171e+07
working_mass          7.107035e+08
capacity              2.368222e+04
mass_operating_min    4.826370e+08
mass_operating_max    1.193341e+09
dtype: float64
# compare with Ireland's electricity demand in 2050 (Deane, 2021)
compare.electricity_demand_ie(data=caverns["capacity"])
Energy capacity as a percentage of Ireland's electricity demand
in 2050 (84–122 TWh electricity), assuming a conversion efficiency
of 50%: 9.71–14.10%
Energy capacity as a percentage of Ireland's hydrogen demand
in 2050, assuming it is 17% of the electricity demand
(84–122 TWh electricity): 114.19–165.84%
# compare with Ireland's hydrogen demand in 2050
compare.hydrogen_demand_ie(data=caverns["capacity"])
Energy capacity as a percentage of Ireland's domestic hydrogen
demand in 2050 (4.6–39 TWh hydrogen): 60.72–514.83%
Energy capacity as a percentage of Ireland's domestic and
non-domestic hydrogen demand in 2050 (19.8–74.6 TWh hydrogen): 31.75–119.61%
# total capacity at various depth/height combinations
s = caverns.groupby(["depth", "halite"], sort=False)[["capacity"]].sum()
s["%"] = s["capacity"] / caverns[["capacity"]].sum().iloc[0] * 100
s
capacity %
depth halite
1,000 - 1,500 Rossall 11585.091442 48.918940
500 - 1,000 Rossall 3884.416135 16.402246
1,500 - 2,000 Rossall 4630.799533 19.553907
1,000 - 1,500 Preesall 102.631996 0.433371
500 - 1,000 Preesall 1353.174027 5.713881
1,500 - 2,000 Preesall 151.843643 0.641171
1,000 - 1,500 Fylde 574.503947 2.425887
1,500 - 2,000 Fylde 597.204799 2.521743
500 - 1,000 Fylde 802.555520 3.388852
s.groupby("depth").sum()[["capacity"]]
capacity
depth
1,000 - 1,500 12262.227385
1,500 - 2,000 5379.847975
500 - 1,000 6040.145681
s.groupby("halite").sum()[["capacity"]]
capacity
halite
Fylde 1974.264266
Preesall 1607.649665
Rossall 20100.307110
# number of caverns
s = caverns.groupby(["depth", "halite"], sort=False)[["capacity"]].count()
s["%"] = s["capacity"] / len(caverns) * 100
s
capacity %
depth halite
1,000 - 1,500 Rossall 99 45.412844
500 - 1,000 Rossall 49 22.477064
1,500 - 2,000 Rossall 32 14.678899
1,000 - 1,500 Preesall 1 0.458716
500 - 1,000 Preesall 17 7.798165
1,500 - 2,000 Preesall 1 0.458716
1,000 - 1,500 Fylde 5 2.293578
1,500 - 2,000 Fylde 4 1.834862
500 - 1,000 Fylde 10 4.587156
s.groupby("depth").sum()[["capacity"]]
capacity
depth
1,000 - 1,500 105
1,500 - 2,000 37
500 - 1,000 76
s.groupby("halite").sum()[["capacity"]]
capacity
halite
Fylde 19
Preesall 19
Rossall 180
compare.distance_from_pipeline(
    caverns, os.path.join("data", "pipelines", "pipelines.zip")
)
Distance to nearest pipeline from caverns: 13.65–38.10 km (mean: 18.47 km)

Map#

# create exclusion buffer
buffer = pd.concat([wells_b, shipwrecks_b, shipping_b, cables_b]).dissolve()
def plot_map_alt(
    dat_xr,
    cavern_df,
    zones_gdf,
    classes,
    # quantity="capacity",
    # quantity_label="Hydrogen storage \ncapacity [GWh]",
    # top_depth=True,
    fontsize=11.5,
):
    """Helper function to plot caverns within the zones of interest"""
    plt.figure(figsize=(20, 11.5))
    axis1 = plt.axes(projection=ccrs.epsg(rd.CRS))
    legend_handles1 = []
    classes = sorted(classes)

    # halite boundary - use buffering to smooth the outline
    shape = rd.halite_shape(dat_xr=dat_xr).buffer(1000).buffer(-1000)
    shape.plot(
        ax=axis1,
        edgecolor="darkslategrey",
        color="none",
        linewidth=2,
        alpha=0.5,
        zorder=2,
    )
    legend_handles1.append(
        mpatches.Patch(
            facecolor="none",
            linewidth=2,
            edgecolor="darkslategrey",
            label="Kish Basin boundary",
            alpha=0.5,
        )
    )

    zones_gdf.plot(
        ax=axis1, zorder=1, linewidth=0, facecolor="white", alpha=0.45
    )
    zones_gdf.plot(
        ax=axis1,
        zorder=2,
        edgecolor="slategrey",
        linestyle="dotted",
        linewidth=1.25,
        facecolor="none",
    )
    legend_handles1.append(
        mpatches.Patch(
            facecolor="none",
            linestyle="dotted",
            edgecolor="slategrey",
            label="Area of interest",
            linewidth=1.25,
        )
    )

    pd.concat([buffer, wind_farms]).dissolve().clip(shape).plot(
        ax=axis1,
        facecolor="none",
        linewidth=0.65,
        edgecolor="slategrey",
        zorder=2,
        alpha=0.5,
        hatch="//",
    )
    legend_handles1.append(
        mpatches.Patch(
            facecolor="none",
            hatch="//",
            edgecolor="slategrey",
            label="Exclusion",
            alpha=0.65,
            linewidth=0.5,
        )
    )

    legend_handles1.append(
        mpatches.Patch(
            label="Hydrogen storage \ncapacity [GWh]", visible=False
        )
    )

    colours = [int(n * 255 / (len(classes) - 1)) for n in range(len(classes))]
    for n, y in enumerate(colours):
        if n == 0:
            # c = cavern_df[cavern_df["capacity"] < classes[0]]
            label1 = f"< {classes[0]:.0f}"
        elif n == len(colours) - 1:
            # c = cavern_df[cavern_df["capacity"] >= classes[-2]]
            label1 = f"≥ {classes[-2]:.0f}"
        else:
            # c = cavern_df[
            #     (cavern_df["capacity"] >= classes[n - 1])
            #     & (cavern_df["capacity"] < classes[n])
            # ]
            label1 = f"{classes[n - 1]:.0f} – < {classes[n]:.0f}"
        # if top_depth:
        #     for df, markersize in zip(
        #         [
        #             c[c["depth"] == "500 - 1,000"],
        #             c[c["depth"] == "1,000 - 1,500"],
        #             c[c["depth"] == "1,500 - 2,000"],
        #         ],
        #         [20, 50, 20],
        #     ):
        #         if len(df) > 0:
        #             df.centroid.plot(
        #                 ax=axis1,
        #                 zorder=3,
        #                 linewidth=0,
        #                 marker=".",
        #                 markersize=markersize,
        #                 color=sns.color_palette("flare", 256)[y],
        #             )
        # else:
        gpd.GeoDataFrame(cavern_df, geometry=cavern_df.centroid).plot(
            ax=axis1,
            scheme="UserDefined",
            classification_kwds={"bins": classes},
            column="capacity",
            zorder=3,
            marker=".",
            cmap="flare",
            markersize=40,
        )
        legend_handles1.append(
            mpatches.Patch(
                facecolor=sns.color_palette("flare", 256)[y], label=label1
            )
        )

    # if top_depth:
    #     legend_handles1.append(
    #         mpatches.Patch(label="Cavern top depth [m]", visible=False)
    #     )
    #     for markersize, label1 in zip(
    #         [6, 3], ["1,000–1,500", "500–1,000 or \n1,500–2,000"]
    #     ):
    #         legend_handles1.append(
    #             Line2D(
    #                 [0],
    #                 [0],
    #                 marker=".",
    #                 linewidth=0,
    #                 label=label1,
    #                 color="darkslategrey",
    #                 markersize=markersize,
    #             )
    #         )

    plt.xlim(shape.bounds["minx"][0] - 1000, shape.bounds["maxx"][0] + 1000)
    plt.ylim(shape.bounds["miny"][0] - 1000, shape.bounds["maxy"][0] + 1000)

    basemap = cx.providers.CartoDB.VoyagerNoLabels
    cx.add_basemap(
        axis1, crs=rd.CRS, source=basemap, zoom=12, attribution=False
    )
    axis1.text(
        shape.bounds["minx"][0] - 800,
        shape.bounds["miny"][0] - 800,
        basemap["attribution"],
        fontsize=10,
    )

    axis1.gridlines(
        draw_labels={"bottom": "x", "left": "y"},
        alpha=0.25,
        color="darkslategrey",
        xlabel_style={"fontsize": fontsize},
        ylabel_style={"fontsize": fontsize, "rotation": 89.9},
        xformatter=LongitudeFormatter(auto_hide=False, dms=True),
        yformatter=LatitudeFormatter(auto_hide=False, dms=True),
    )
    axis1.add_artist(
        ScaleBar(
            1,
            box_alpha=0,
            location="lower right",
            color="darkslategrey",
            width_fraction=0.0075,
            font_properties={"size": fontsize},
        )
    )
    plt.legend(
        loc="lower right",
        bbox_to_anchor=(1, 0.05),
        handles=legend_handles1,
        fontsize=fontsize,
    )
    plt.title(None)

    plt.tight_layout()
    # plt.savefig(
    #     os.path.join("graphics", "Figure_8.png"),
    #     format="png",
    #     dpi=600,
    # )
    plt.show()
plot_map_alt(
    ds, caverns, zones, list(mc.Quantiles(caverns["capacity"], k=6).bins)
)
../_images/aefec1db1cbf4cc65240912c237831da5be1e9492fa497aa0944f51403a9750e.png