Custom Shift Detection Methods¶
So far TOAD only comes with ASDETECT [Boulton+Lenton2019]. But you can also implement your own shifts detection algorithm. You basically just need a function which transforms the timeseries in each grid cell into a time series of “abruptness”, with floating point values between -1 (negative shift) and 1 (positive shift).
# Prerequisites
import xarray as xr
import matplotlib.pyplot as plt
import numpy as np
plt.rcParams["figure.dpi"] = 300
plt.rcParams["figure.figsize"] = (12, 5)
data = (
xr.open_dataset("test_data/garbe_2020_antarctica.nc")
.coarsen(x=2, y=2, GMST=2, boundary="trim")
.reduce(np.mean)
)
data = data.drop_vars("thk_dts")
data
/Users/jakobharteg/miniconda3/envs/toad312/lib/python3.12/site-packages/pyproj/network.py:59: UserWarning: pyproj unable to set PROJ database path.
_set_context_ca_bundle_path(ca_bundle_path)
<xarray.Dataset> Size: 6MB
Dimensions: (GMST: 175, y: 95, x: 95)
Coordinates:
* GMST (GMST) float64 1kB 0.0501 0.1301 0.2101 ... 13.81 13.89 13.97
* y (y) float64 760B -3.016e+06 -2.952e+06 ... 2.936e+06 3e+06
* x (x) float64 760B -3.016e+06 -2.952e+06 ... 2.936e+06 3e+06
Data variables:
thk (GMST, y, x) float32 6MB 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0
Attributes: (12/13)
CDI: Climate Data Interface version 1.9.6 (http://mpimet.mpg.d...
proj4: +lon_0=0.0 +ellps=WGS84 +datum=WGS84 +lat_ts=-71.0 +proj=...
CDO: Climate Data Operators version 1.9.6 (http://mpimet.mpg.d...
source: PISM (development v1.0-535-gb3de48787 committed by Julius...
institution: PIK / Potsdam Institute for Climate Impact Research
author: Julius Garbe (julius.garbe@pik-potsdam.de)
... ...
title: garbe_2020_antarctica
Conventions: CF-1.9
projection: Polar Stereographic South (71S,0E)
ice_density: 910. kg m-3
NCO: netCDF Operators version 4.7.8 (Homepage = http://nco.sf....
Modifications: Modified by Jakob Harteg (jakob.harteg@pik-potsdam.de) Se...Let’s build a simple shifts detection algorithm
def compute_shift_intensity(values_1d: np.ndarray, window_size=5):
"""
Simple function to compute shift intensity for each timestep [-1, 1]
where -1 is strong negative shift, +1 is strong positive shift
"""
data = np.array(values_1d)
window_size = max(3, len(data) // 15) # Ensure reasonable window size
# Initialize results array
intensity = np.zeros(len(data))
# Calculate max possible difference for normalization
max_diff = np.max(data) - np.min(data)
if max_diff == 0:
return intensity
# For each point (except edges), compare before/after windows
for i in range(window_size, len(data) - window_size):
window1 = data[i - window_size : i]
window2 = data[i : i + window_size]
mean1 = np.mean(window1)
mean2 = np.mean(window2)
# Normalize difference to [-1, 1]
# Positive means increasing, negative means decreasing
intensity[i] = (mean2 - mean1) / max_diff
intensity[i] = min(intensity[i], 1)
intensity[i] = max(intensity[i], -1)
return intensity
So this function takes a single timeseries with values and timesteps
# get sample timeseries from dataset
ts = data.sel(x=-1032000, y=-1032000, method="nearest").thk
# Calculate shift intensity
intensity = compute_shift_intensity(ts.values)
# Plot original data and shift intensity
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 4), sharex=True)
ax1.plot(ts.GMST, ts.values)
ax1.set_title("Original Time Series")
ax2.plot(ts.GMST, intensity, color="red")
ax2.axhline(y=0, color="k", linestyle="--", alpha=0.3)
ax1.set_ylabel("Ice thickness")
ax2.set_ylabel("Shift Intensity")
ax2.set_xlabel("GMST")
ax2.set_ylim(-1, 1)
(-1.0, 1.0)
Now we can define a new shifts method by abstracting the ShiftsMethod class in toad.shifts.
from toad.shifts import ShiftsMethod
class MY_SHFITS_METHOD(ShiftsMethod):
# You can pass parameters to the method like this
def __init__(self, window_size=5):
self.window_size = window_size
# This function is called by TOAD on each gridcell independently.
def fit_predict(
self,
values_1d: np.ndarray,
times_1d: np.ndarray,
) -> np.ndarray:
shifts = compute_shift_intensity(values_1d, window_size=self.window_size)
return shifts
And then we can pass this method into compute_shifts.
from toad import TOAD
td = TOAD(data, time_dim="GMST")
td.compute_shifts("thk", method=MY_SHFITS_METHOD(window_size=10))
INFO: New shifts variable thk_dts: min/mean/max=-0.981/-0.057/0.937 using 3589 grid cells. Skipped 60.2% grid cells: 0 NaN, 5436 constant.
Let’s inspect the detection time series distribution. He were see we detect mostly negative shifts.
td.plot.shifts_distribution()
(<Figure size 3600x600 with 1 Axes>,
array([<Axes: ylabel='#thk_dts'>], dtype=object))
Let’s compute clusters now and plot the results
from sklearn.cluster import HDBSCAN
td.compute_clusters(
"thk",
method=HDBSCAN(min_cluster_size=10),
overwrite=True,
)
# plot 5 largest clusters
td.plot.overview(
var="thk",
cluster_ids=range(5),
map_style={"projection": "south_pole"},
);
INFO: New cluster variable thk_dts_cluster: Identified 12 clusters in 2,414 pts; Left 7.0% as noise (169 pts).