Cavern storage capacity for variable heights#

import os

import cartopy.crs as ccrs
import contextily as cx
import geopandas as gpd
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": 85, "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=[85, 155, 311],
    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: 979
------------------------------------------------------------
Excluding salt formation edges...
Number of potential caverns: 945
------------------------------------------------------------
Exclude shipping...
Number of potential caverns: 470
Caverns excluded: 50.26%
------------------------------------------------------------
Exclude wind farms...
Number of potential caverns: 397
Caverns excluded: 57.99%
------------------------------------------------------------
Exclude cables...
Number of potential caverns: 397
Caverns excluded: 57.99%
------------------------------------------------------------
Exclude wells...
Number of potential caverns: 397
Caverns excluded: 57.99%
------------------------------------------------------------
Exclude shipwrecks...
Number of potential caverns: 397
Caverns excluded: 57.99%
------------------------------------------------------------
compare.distance_from_pipeline(
    caverns, os.path.join("data", "pipelines", "pipelines.zip")
)
Distance to nearest pipeline from caverns: 13.64–38.85 km (mean: 20.12 km)

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",
    ]
).describe()
Thickness TopDepthSeabed Bathymetry NetToGross ThicknessNet cavern_height cavern_depth cavern_total_volume cavern_volume 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 397.000000 397.000000 397.000000 397.000000 397.000000 397.000000 397.000000 3.970000e+02 3.970000e+02 397.000000 3.970000e+02 3.970000e+02 397.000000 397.000000 3.970000e+02 3.970000e+02 3.970000e+02 397.000000 397.000000
mean 392.930219 1053.426092 -48.053324 0.621216 247.266901 119.010076 1133.426092 5.582864e+05 3.908005e+05 327.884917 7.909528e+06 2.109208e+07 5.510773 13.635721 3.229138e+06 2.192091e+06 5.421229e+06 107.602049 0.598015
std 69.206387 380.768364 13.463288 0.049267 66.464138 52.350990 380.768364 2.970658e+05 2.079460e+05 14.418667 2.689247e+06 7.171324e+06 1.570477 3.623729 1.929922e+06 1.334774e+06 3.263707e+06 64.309305 0.008141
min 316.764900 421.304826 -76.084564 0.554740 175.722230 85.000000 501.304826 3.652962e+05 2.557073e+05 303.542681 3.425755e+06 9.135347e+06 2.682620 6.922279 1.084112e+06 6.859656e+05 1.770077e+06 36.125011 0.581349
25% 347.147300 735.352513 -60.478745 0.582849 202.334555 85.000000 815.352513 3.652962e+05 2.557073e+05 315.545404 5.637987e+06 1.503463e+07 4.199732 10.628343 1.750725e+06 1.151149e+06 2.901874e+06 58.338062 0.592018
50% 375.486600 1030.925564 -43.757942 0.609068 228.696918 85.000000 1110.925564 3.652962e+05 2.557073e+05 327.077021 7.787613e+06 2.076697e+07 5.500506 13.687629 2.445582e+06 1.678632e+06 4.123323e+06 81.492214 0.598092
75% 417.528300 1320.705498 -41.484966 0.647964 270.543345 155.000000 1400.705498 7.625113e+05 5.337579e+05 338.127504 9.816541e+06 2.617744e+07 6.667109 16.341690 4.542525e+06 3.068601e+06 7.611126e+06 151.367032 0.604855
max 822.710300 1900.680682 -18.320551 0.750000 617.032725 311.000000 1980.680682 1.647734e+06 1.153413e+06 359.019276 1.382934e+07 3.687824e+07 8.706567 20.796696 1.138274e+07 7.872472e+06 1.925521e+07 379.298161 0.612466
# totals
caverns[
    [
        "cavern_volume",
        "working_mass",
        "capacity",
        "mass_operating_min",
        "mass_operating_max",
    ]
].sum()
cavern_volume         1.551478e+08
working_mass          1.281968e+09
capacity              4.271801e+04
mass_operating_min    8.702602e+08
mass_operating_max    2.152228e+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%: 17.51–25.43%
Energy capacity as a percentage of Ireland's hydrogen demand
in 2050, assuming it is 17% of the electricity demand
(84–122 TWh electricity): 205.97–299.15%
# 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): 109.53–928.65%
Energy capacity as a percentage of Ireland's domestic and
non-domestic hydrogen demand in 2050 (19.8–74.6 TWh hydrogen): 57.26–215.75%
# total capacity at various depth/height combinations
s = caverns.groupby(["depth", "cavern_height", "halite"], sort=False)[
    ["capacity"]
].sum()
s["%"] = s["capacity"] / caverns[["capacity"]].sum().iloc[0] * 100
s
capacity %
depth cavern_height halite
1,000 - 1,500 311.0 Rossall 3613.977881 8.460079
155.0 Rossall 12006.087146 28.105444
85.0 Rossall 6218.033824 14.556000
500 - 1,000 311.0 Rossall 1580.239728 3.699235
155.0 Rossall 2408.323030 5.637722
1,500 - 2,000 155.0 Rossall 5229.407317 12.241691
500 - 1,000 85.0 Rossall 2572.953053 6.023110
1,500 - 2,000 85.0 Rossall 1839.994778 4.307304
1,000 - 1,500 155.0 Preesall 138.531699 0.324293
85.0 Preesall 70.172538 0.164269
500 - 1,000 155.0 Preesall 908.967039 2.127831
85.0 Preesall 1417.392302 3.318020
1,500 - 2,000 85.0 Preesall 1375.233638 3.219330
1,000 - 1,500 155.0 Fylde 481.957684 1.128231
85.0 Fylde 733.310380 1.716630
1,500 - 2,000 155.0 Fylde 201.300795 0.471232
500 - 1,000 155.0 Fylde 107.786410 0.252321
85.0 Fylde 1419.559349 3.323093
1,500 - 2,000 85.0 Fylde 394.784768 0.924165
s.groupby("depth").sum()[["capacity"]]
capacity
depth
1,000 - 1,500 23262.071153
1,500 - 2,000 9040.721295
500 - 1,000 10415.220910
s.groupby("cavern_height").sum()[["capacity"]]
capacity
cavern_height
85.0 16041.434630
155.0 21482.361120
311.0 5194.217609
s.groupby("halite").sum()[["capacity"]]
capacity
halite
Fylde 3338.699386
Preesall 3910.297216
Rossall 35469.016757
# number of caverns
s = caverns.groupby(["depth", "cavern_height", "halite"], sort=False)[
    ["capacity"]
].count()
s["%"] = s["capacity"] / len(caverns) * 100
s
capacity %
depth cavern_height halite
1,000 - 1,500 311.0 Rossall 11 2.770781
155.0 Rossall 75 18.891688
85.0 Rossall 84 21.158690
500 - 1,000 311.0 Rossall 6 1.511335
155.0 Rossall 22 5.541562
1,500 - 2,000 155.0 Rossall 27 6.801008
500 - 1,000 85.0 Rossall 54 13.602015
1,500 - 2,000 85.0 Rossall 19 4.785894
1,000 - 1,500 155.0 Preesall 1 0.251889
85.0 Preesall 1 0.251889
500 - 1,000 155.0 Preesall 8 2.015113
85.0 Preesall 29 7.304786
1,500 - 2,000 85.0 Preesall 14 3.526448
1,000 - 1,500 155.0 Fylde 3 0.755668
85.0 Fylde 10 2.518892
1,500 - 2,000 155.0 Fylde 1 0.251889
500 - 1,000 155.0 Fylde 1 0.251889
85.0 Fylde 27 6.801008
1,500 - 2,000 85.0 Fylde 4 1.007557
s.groupby("depth").sum()[["capacity"]]
capacity
depth
1,000 - 1,500 185
1,500 - 2,000 65
500 - 1,000 147
s.groupby("cavern_height").sum()[["capacity"]]
capacity
cavern_height
85.0 242
155.0 138
311.0 17
s.groupby("halite").sum()[["capacity"]]
capacity
halite
Fylde 46
Preesall 53
Rossall 298

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, 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(list(int(x) for x in caverns["cavern_height"].unique()))

    # 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="Cavern height [m]", visible=False)
    )

    colours = [int(n * 255 / (len(classes) - 1)) for n in range(len(classes))]
    for n, y in enumerate(colours):
        c = cavern_df[cavern_df["cavern_height"] == classes[n]]
        label1 = f"{classes[n]}"
        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"],
                ],
                [30, 60, 30],
            ):
                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[1:]},
                column="cavern_height",
                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.tight_layout()
    # plt.savefig(
    #     os.path.join("graphics", "Figure_7.png"),
    #     format="png",
    #     dpi=600,
    # )
    plt.show()
plot_map_alt(ds, caverns, zones)
../_images/b4ac9bd46a2eda0bcf2d9b2cba1b35d61dc87f0f4beacacffbeeeb451cdaca11.png