Calculate pairwise distances between a contour and grid cells¶
In this notebook, we will demonstrate how to calculate the distance from every grid cell to the nearest grid cell along a contour. We will demonstrate this by computing the distance from each grid cell in a data array to the sea ice edge. We will use monthly sea ice concentration from ACCESS-OM2-01 over the Southern Ocean to perform these calculations.
i, j.Loading modules¶
[1]:
import intake
import xarray as xr
import numpy as np
import datetime as dt
from sklearn.neighbors import BallTree
import matplotlib.pyplot as plt
Creating a session in the COSIMA cookbook¶
[2]:
from dask.distributed import Client
client = Client(threads_per_worker = 1)
client
/g/data/xp65/public/apps/med_conda/envs/analysis3-25.08/lib/python3.11/site-packages/distributed/node.py:187: UserWarning: Port 8787 is already in use.
Perhaps you already have a cluster running?
Hosting the HTTP server on port 40215 instead
warnings.warn(
[2]:
Client
Client-11a12ea0-78c7-11f0-b090-00000190fe80
| Connection method: Cluster object | Cluster type: distributed.LocalCluster |
| Dashboard: /proxy/40215/status |
Cluster Info
LocalCluster
a7eb649a
| Dashboard: /proxy/40215/status | Workers: 48 |
| Total threads: 48 | Total memory: 188.56 GiB |
| Status: running | Using processes: True |
Scheduler Info
Scheduler
Scheduler-e6339785-e211-400b-a595-8b9a94a16f66
| Comm: tcp://127.0.0.1:41361 | Workers: 0 |
| Dashboard: /proxy/40215/status | Total threads: 0 |
| Started: Just now | Total memory: 0 B |
Workers
Worker: 0
| Comm: tcp://127.0.0.1:40045 | Total threads: 1 |
| Dashboard: /proxy/33431/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:35547 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-7bhsctb_ | |
Worker: 1
| Comm: tcp://127.0.0.1:33119 | Total threads: 1 |
| Dashboard: /proxy/44713/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:43023 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-gzvtdyw8 | |
Worker: 2
| Comm: tcp://127.0.0.1:42199 | Total threads: 1 |
| Dashboard: /proxy/42837/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:35095 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-uysktptr | |
Worker: 3
| Comm: tcp://127.0.0.1:33931 | Total threads: 1 |
| Dashboard: /proxy/32941/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:36735 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-92y1zn58 | |
Worker: 4
| Comm: tcp://127.0.0.1:40917 | Total threads: 1 |
| Dashboard: /proxy/37429/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:34231 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-2n77l245 | |
Worker: 5
| Comm: tcp://127.0.0.1:36507 | Total threads: 1 |
| Dashboard: /proxy/44333/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:41883 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-avwsznw9 | |
Worker: 6
| Comm: tcp://127.0.0.1:39789 | Total threads: 1 |
| Dashboard: /proxy/40955/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:35251 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-5lem1986 | |
Worker: 7
| Comm: tcp://127.0.0.1:39205 | Total threads: 1 |
| Dashboard: /proxy/46365/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:42207 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-dsagzkb4 | |
Worker: 8
| Comm: tcp://127.0.0.1:43859 | Total threads: 1 |
| Dashboard: /proxy/42315/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:42651 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-bn1zt_6b | |
Worker: 9
| Comm: tcp://127.0.0.1:40379 | Total threads: 1 |
| Dashboard: /proxy/43973/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:43349 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-1_ol2w3_ | |
Worker: 10
| Comm: tcp://127.0.0.1:37269 | Total threads: 1 |
| Dashboard: /proxy/39699/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:41755 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-l5ohc4et | |
Worker: 11
| Comm: tcp://127.0.0.1:40297 | Total threads: 1 |
| Dashboard: /proxy/36789/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:40433 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-1122_k2r | |
Worker: 12
| Comm: tcp://127.0.0.1:45085 | Total threads: 1 |
| Dashboard: /proxy/41535/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:37025 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-vifkv9_5 | |
Worker: 13
| Comm: tcp://127.0.0.1:44877 | Total threads: 1 |
| Dashboard: /proxy/37139/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:39301 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-apxvv4um | |
Worker: 14
| Comm: tcp://127.0.0.1:42229 | Total threads: 1 |
| Dashboard: /proxy/44519/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:46569 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-tgz35nzd | |
Worker: 15
| Comm: tcp://127.0.0.1:45083 | Total threads: 1 |
| Dashboard: /proxy/44697/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:38633 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-uh0ntsep | |
Worker: 16
| Comm: tcp://127.0.0.1:34447 | Total threads: 1 |
| Dashboard: /proxy/33541/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:35893 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-zo3ofpae | |
Worker: 17
| Comm: tcp://127.0.0.1:42475 | Total threads: 1 |
| Dashboard: /proxy/43119/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:42331 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-sm0ji_qk | |
Worker: 18
| Comm: tcp://127.0.0.1:33659 | Total threads: 1 |
| Dashboard: /proxy/33117/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:44323 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-ja180bc0 | |
Worker: 19
| Comm: tcp://127.0.0.1:36663 | Total threads: 1 |
| Dashboard: /proxy/41997/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:38475 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-tg68r2on | |
Worker: 20
| Comm: tcp://127.0.0.1:43095 | Total threads: 1 |
| Dashboard: /proxy/40903/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:43131 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-vwowix7v | |
Worker: 21
| Comm: tcp://127.0.0.1:46681 | Total threads: 1 |
| Dashboard: /proxy/38741/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:32793 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-tdpxkioy | |
Worker: 22
| Comm: tcp://127.0.0.1:37005 | Total threads: 1 |
| Dashboard: /proxy/33183/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:36819 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-9u1bcgfb | |
Worker: 23
| Comm: tcp://127.0.0.1:43127 | Total threads: 1 |
| Dashboard: /proxy/44245/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:38975 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-gele627d | |
Worker: 24
| Comm: tcp://127.0.0.1:41509 | Total threads: 1 |
| Dashboard: /proxy/42241/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:44757 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-1py6y26r | |
Worker: 25
| Comm: tcp://127.0.0.1:35813 | Total threads: 1 |
| Dashboard: /proxy/45665/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:36579 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-2gu9i9n6 | |
Worker: 26
| Comm: tcp://127.0.0.1:39211 | Total threads: 1 |
| Dashboard: /proxy/46623/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:38141 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-5np3mjh3 | |
Worker: 27
| Comm: tcp://127.0.0.1:38071 | Total threads: 1 |
| Dashboard: /proxy/36319/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:46819 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-d3yc6l3e | |
Worker: 28
| Comm: tcp://127.0.0.1:34385 | Total threads: 1 |
| Dashboard: /proxy/35777/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:43557 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-rh2nfpgn | |
Worker: 29
| Comm: tcp://127.0.0.1:35273 | Total threads: 1 |
| Dashboard: /proxy/36781/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:33359 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-9t1om534 | |
Worker: 30
| Comm: tcp://127.0.0.1:33687 | Total threads: 1 |
| Dashboard: /proxy/44689/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:41341 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-ne09e0hn | |
Worker: 31
| Comm: tcp://127.0.0.1:35689 | Total threads: 1 |
| Dashboard: /proxy/45031/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:37793 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-187i7zou | |
Worker: 32
| Comm: tcp://127.0.0.1:39099 | Total threads: 1 |
| Dashboard: /proxy/37901/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:42365 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-v5ud9242 | |
Worker: 33
| Comm: tcp://127.0.0.1:32803 | Total threads: 1 |
| Dashboard: /proxy/42059/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:38909 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-n_zitx8m | |
Worker: 34
| Comm: tcp://127.0.0.1:37453 | Total threads: 1 |
| Dashboard: /proxy/33303/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:40469 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-66aqd0_z | |
Worker: 35
| Comm: tcp://127.0.0.1:35525 | Total threads: 1 |
| Dashboard: /proxy/32813/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:33757 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-ysm8p6tp | |
Worker: 36
| Comm: tcp://127.0.0.1:39883 | Total threads: 1 |
| Dashboard: /proxy/41081/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:44353 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-t87hxad5 | |
Worker: 37
| Comm: tcp://127.0.0.1:43855 | Total threads: 1 |
| Dashboard: /proxy/41485/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:42413 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-bizqkrji | |
Worker: 38
| Comm: tcp://127.0.0.1:39319 | Total threads: 1 |
| Dashboard: /proxy/40739/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:38777 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-l3w3sa26 | |
Worker: 39
| Comm: tcp://127.0.0.1:44159 | Total threads: 1 |
| Dashboard: /proxy/36889/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:45509 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-h2q6hwsj | |
Worker: 40
| Comm: tcp://127.0.0.1:40803 | Total threads: 1 |
| Dashboard: /proxy/36863/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:39947 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-zgngwsuw | |
Worker: 41
| Comm: tcp://127.0.0.1:43281 | Total threads: 1 |
| Dashboard: /proxy/39743/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:32915 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-iu3d1d9p | |
Worker: 42
| Comm: tcp://127.0.0.1:40897 | Total threads: 1 |
| Dashboard: /proxy/35627/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:37289 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-x6_y5afk | |
Worker: 43
| Comm: tcp://127.0.0.1:46033 | Total threads: 1 |
| Dashboard: /proxy/43335/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:46287 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-a45b0je2 | |
Worker: 44
| Comm: tcp://127.0.0.1:37835 | Total threads: 1 |
| Dashboard: /proxy/34499/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:42865 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-0syf_wud | |
Worker: 45
| Comm: tcp://127.0.0.1:39061 | Total threads: 1 |
| Dashboard: /proxy/35489/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:41353 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-aaoj4quq | |
Worker: 46
| Comm: tcp://127.0.0.1:46777 | Total threads: 1 |
| Dashboard: /proxy/35247/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:41841 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-4hvnt4h1 | |
Worker: 47
| Comm: tcp://127.0.0.1:36635 | Total threads: 1 |
| Dashboard: /proxy/36453/status | Memory: 3.93 GiB |
| Nanny: tcp://127.0.0.1:34097 | |
| Local directory: /jobfs/147046130.gadi-pbs/dask-scratch-space/worker-x1xw29_w | |
Accessing ACCESS-OM2-01 data¶
First we load the ACCESS-NRI default intake catalog.
[3]:
catalog = intake.cat.access_nri
We use monthly sea ice outputs. Here, we load only two months of sea ice data.
[4]:
cat_subset = catalog['01deg_jra55v140_iaf_cycle4']
ds = cat_subset.search(variable='aice_m',file_id='seaIce.1mon.d2:2.nc:5.ni:3600.nj:2700'
).to_dask(
xarray_combine_by_coords_kwargs=dict(compat="override",
data_vars="minimal",
coords="minimal"),
xarray_open_kwargs={"chunks" : "auto"},
)
var_ice = ds['aice_m']
var_ice = var_ice.sel(time=slice('1978-01', '1978-03'))
var_ice
/g/data/xp65/public/apps/med_conda/envs/analysis3-25.08/lib/python3.11/site-packages/intake_esm/core.py:301: FutureWarning: When grouping with a length-1 list-like, you will need to pass a length-1 tuple to get_group in a future version of pandas. Pass `(name,)` instead of `name` to silence this warning.
records = grouped.get_group(internal_key).to_dict(orient='records')
[4]:
<xarray.DataArray 'aice_m' (time: 3, nj: 2700, ni: 3600)> Size: 117MB
dask.array<getitem, shape=(3, 2700, 3600), dtype=float32, chunksize=(1, 2700, 3600), chunktype=numpy.ndarray>
Coordinates:
* time (time) datetime64[ns] 24B 1978-01-01 1978-02-01 1978-03-01
TLON (nj, ni) float32 39MB dask.array<chunksize=(2700, 3600), meta=np.ndarray>
TLAT (nj, ni) float32 39MB dask.array<chunksize=(2700, 3600), meta=np.ndarray>
ULON (nj, ni) float32 39MB dask.array<chunksize=(2700, 3600), meta=np.ndarray>
ULAT (nj, ni) float32 39MB dask.array<chunksize=(2700, 3600), meta=np.ndarray>
Dimensions without coordinates: nj, ni
Attributes:
units: 1
long_name: ice area (aggregate)
cell_measures: area: tarea
cell_methods: time: mean
time_rep: averagedThe sea ice outputs need some processing before we can start our calculations. You can check this example for a guide on how to load and plot sea ice data.
We will follow these processing steps:
Correct time dimension values by subtracting 12 hours,
Attach the ocean model grid so we can calculate distances.
[5]:
# Loading area_t to get ocean grid
var_search = catalog['01deg_jra55v140_iaf_cycle4'].search(variable='area_t')
var_search = var_search.search(path=var_search.df['path'][0])
ds = var_search.to_dask(xarray_open_kwargs={"chunks": "auto"})
area_t = ds['area_t']
# Apply time correction so data appears in the middle (12:00) of the day rather than at the beginning of the day (00:00)
var_ice['time'] = var_ice.time.to_pandas() - dt.timedelta(hours = 12)
# Change coordinates so they match ocean dimensions
var_ice.coords['ni'] = area_t['xt_ocean'].values
var_ice.coords['nj'] = area_t['yt_ocean'].values
# Rename coordinate variables so they match ocean data
var_ice = var_ice.rename(({'ni': 'xt_ocean', 'nj': 'yt_ocean'}))
# Subsetting data for the Southern Ocean
var_ice = var_ice.sel(yt_ocean = slice(-80, -45))
var_ice
/g/data/xp65/public/apps/med_conda/envs/analysis3-25.08/lib/python3.11/site-packages/intake_esm/core.py:301: FutureWarning: When grouping with a length-1 list-like, you will need to pass a length-1 tuple to get_group in a future version of pandas. Pass `(name,)` instead of `name` to silence this warning.
records = grouped.get_group(internal_key).to_dict(orient='records')
[5]:
<xarray.DataArray 'aice_m' (time: 3, yt_ocean: 713, xt_ocean: 3600)> Size: 31MB
dask.array<getitem, shape=(3, 713, 3600), dtype=float32, chunksize=(1, 713, 3600), chunktype=numpy.ndarray>
Coordinates:
TLON (yt_ocean, xt_ocean) float32 10MB dask.array<chunksize=(713, 3600), meta=np.ndarray>
TLAT (yt_ocean, xt_ocean) float32 10MB dask.array<chunksize=(713, 3600), meta=np.ndarray>
ULON (yt_ocean, xt_ocean) float32 10MB dask.array<chunksize=(713, 3600), meta=np.ndarray>
ULAT (yt_ocean, xt_ocean) float32 10MB dask.array<chunksize=(713, 3600), meta=np.ndarray>
* time (time) datetime64[ns] 24B 1977-12-31T12:00:00 ... 1978-02-28T12...
* xt_ocean (xt_ocean) float64 29kB -279.9 -279.8 -279.7 ... 79.75 79.85 79.95
* yt_ocean (yt_ocean) float64 6kB -79.97 -79.93 -79.88 ... -45.11 -45.04
Attributes:
units: 1
long_name: ice area (aggregate)
cell_measures: area: tarea
cell_methods: time: mean
time_rep: averagedFinding sea ice edge¶
The sea ice edge is the defined as the northernmost areas where sea ice concentration (SIC) is over \(10\%\). This is a multistep process:
Identify cells where \(\text{SIC} >= 0.1\), classifying them with a value of 1;
Calculate the cumulative sum along latitude;
Apply mask to constrain to only cells with \(\text{SIC} >= 0.1\);
Choose the maximum for each longitude.
We’ll demonstrate this process over the first timestep of data.
[6]:
is_ice = var_ice.isel(time=0) >= 0.1
cumulative_ice = is_ice.cumsum(dim='yt_ocean')
ice_edge = (cumulative_ice == cumulative_ice.max('yt_ocean')) * is_ice
ice_edge.plot()
[6]:
<matplotlib.collections.QuadMesh at 0x15128dd6a450>
Checking results in relation to SIC data¶
[7]:
var_ice.where(var_ice >= 0.1).isel(time=0).plot()
ice_edge.plot.contour(colors=["red"])
[7]:
<matplotlib.contour.QuadContourSet at 0x15128e1af990>
Above we plotted only cells with SIC greater or equal to 0.1. The red line represents the sea ice edge we identified in the previous step. We can be satisfied that we identified the sea ice edge correctly.
Getting coordinate pairs for our entire grid¶
We will use the latitude and longitude values in our data to create coordinate pairs. We only need to get this information once if we are calculating distances from the same grid.
[8]:
grid_x, grid_y = np.meshgrid(
var_ice.xt_ocean.values,
var_ice.yt_ocean.values,
)
# Changing shape so there are two values per row
grid_coords = np.vstack([grid_y.flat, grid_x.flat]).T
grid_coords
[8]:
array([[ -79.96816911, -279.95 ],
[ -79.96816911, -279.85 ],
[ -79.96816911, -279.75 ],
...,
[ -45.03607147, 79.75 ],
[ -45.03607147, 79.85 ],
[ -45.03607147, 79.95 ]])
Getting coordinate pairs for sea ice edge¶
We will find the index for the sea ice edge so we can identify the latitude at which the sea ice edge occurs. This will be combined with all longitude values to create the coordinate pairs for the sea ice edge.
Note that this step has to be done once for every time step as the sea ice edge changes over time.
[9]:
# Getting the indices for cell with maximum value along yt_ocean dimension
ice_yt = ice_edge.yt_ocean[ice_edge.argmax(dim='yt_ocean')]
ice_coords = np.vstack([ice_yt, ice_edge.xt_ocean]).T
ice_coords
[9]:
array([[ -61.84325357, -279.95 ],
[ -61.84325357, -279.85 ],
[ -61.84325357, -279.75 ],
...,
[ -61.60640174, 79.75 ],
[ -61.65391774, 79.85 ],
[ -61.84325357, 79.95 ]])
Using Nearest Neighbours algorithm to calculate distance to closest sea ice edge cell¶
We will build a data structure called BallTree for our calculations, from the points comprising the sea ice edge. This structure allows us to efficiently query the nearest point within the sea ice edge set to any arbitrary point, according to some kind of metric. Because we’re on a sphere, we use the haversine (great circle)
distance, which also requires that we transform coordinates from degrees to radians. scikit-learn also offers a KDTree for a similar purpose, but this doesn’t support the haversine distance as a metric, so we opt for the ball tree instead.
The advantage of using a data structure like a ball tree is that we trade off slightly more time and memory to construct the tree for more efficient querying. This makes it feasible to query the closest sea ice edge cell to every point on the grid, which may otherwise have excessive time and/or memory requirements for a brute force approach.
[10]:
%%time
# First we set up our Ball Tree. Coordinates must be given in radians.
ball_tree = BallTree(np.deg2rad(ice_coords), metric='haversine')
CPU times: user 6.5 ms, sys: 0 ns, total: 6.5 ms
Wall time: 5.71 ms
[11]:
%%time
# The nearest neighbour calculation will give two outputs: distances and indices
# We only need the distances for now, so ignore the second output
distances_radians, _ = ball_tree.query(np.deg2rad(grid_coords), return_distance=True)
CPU times: user 30.4 s, sys: 2.57 s, total: 33 s
Wall time: 22.1 s
Transforming distance from radians to kilometers¶
Because the haversine distance operates in terms of radians only, we will need to multiply by the resulting distances by Earth’s radius to get distances in radians. We will also reshape the results so it matches our original grid.
[12]:
distances_km = distances_radians * 6371
distances_t0 = xr.DataArray(
data=distances_km.reshape(var_ice.yt_ocean.size, -1),
dims=['yt_ocean', 'xt_ocean'],
coords={'yt_ocean': var_ice.yt_ocean, 'xt_ocean': var_ice.xt_ocean}
)
distances_t0
[12]:
<xarray.DataArray (yt_ocean: 713, xt_ocean: 3600)> Size: 21MB
array([[ 805.00144028, 803.19226827, 801.38249999, ..., 810.42536522,
808.61799012, 806.81001469],
[ 806.69292563, 804.88000362, 803.06648432, ..., 812.12809432,
810.31697234, 808.50524899],
[ 808.40803346, 806.59143086, 804.77423025, ..., 813.85423949,
812.03943914, 810.22403667],
...,
[1607.76336769, 1606.55053756, 1605.36467069, ..., 1611.56299844,
1610.26966326, 1609.00309777],
[1615.51041181, 1614.30178279, 1613.12003095, ..., 1619.29692977,
1618.0080497 , 1616.74585538],
[1623.26793377, 1622.06347241, 1620.88580274, ..., 1627.04144284,
1625.75698268, 1624.4991249 ]])
Coordinates:
* yt_ocean (yt_ocean) float64 6kB -79.97 -79.93 -79.88 ... -45.11 -45.04
* xt_ocean (xt_ocean) float64 29kB -279.9 -279.8 -279.7 ... 79.75 79.85 79.95Turning results into data array¶
We will also apply a mask to our results, so we will only keep values for cells where SIC was greater or equal to 0.1.
[13]:
distances_t0.where(is_ice).plot()
/g/data/xp65/public/apps/med_conda/envs/analysis3-25.08/lib/python3.11/site-packages/distributed/client.py:3363: UserWarning: Sending large graph of size 20.73 MiB.
This may cause some slowdown.
Consider loading the data with Dask directly
or using futures or delayed objects to embed the data into the graph without repetition.
See also https://docs.dask.org/en/stable/best-practices.html#load-data-with-dask for more information.
warnings.warn(
[13]:
<matplotlib.collections.QuadMesh at 0x15128ea64d50>
Calculating results for other time steps¶
We can wrap this up into a function to calculate distances for a given timestep. This function returns the ice mask as a second value, so that we can make the plots in the same format as demonstrated so far.
[14]:
def ice_edge_distances(ice_ds):
x, y = np.meshgrid(np.deg2rad(var_ice.xt_ocean.values),
np.deg2rad(var_ice.yt_ocean.values))
grid_coords = np.vstack([y.flat, x.flat]).T
is_ice = ice_ds >= 0.1
cumulative_ice = is_ice.cumsum(dim='yt_ocean')
ice_edge = (cumulative_ice == cumulative_ice.max('yt_ocean')) * is_ice
ice_yt = ice_edge.yt_ocean[ice_edge.argmax(dim='yt_ocean')]
ice_coords = np.vstack([ice_yt, ice_edge.xt_ocean]).T
ball_tree = BallTree(np.deg2rad(ice_coords), metric='haversine')
distances_radians, _ = ball_tree.query(grid_coords, return_distance=True)
distances_km = distances_radians * 6371
distances = xr.DataArray(
data=distances_km.reshape(ice_ds.yt_ocean.size, -1),
dims=['yt_ocean', 'xt_ocean'],
coords={'yt_ocean': ice_ds.yt_ocean, 'xt_ocean': ice_ds.xt_ocean},
name='ice_edge_distance'
)
return distances, is_ice
[15]:
distances_t1, mask = ice_edge_distances(var_ice.isel(time=1))
distances_t1.where(mask).plot()
/g/data/xp65/public/apps/med_conda/envs/analysis3-25.08/lib/python3.11/site-packages/distributed/client.py:3363: UserWarning: Sending large graph of size 20.73 MiB.
This may cause some slowdown.
Consider loading the data with Dask directly
or using futures or delayed objects to embed the data into the graph without repetition.
See also https://docs.dask.org/en/stable/best-practices.html#load-data-with-dask for more information.
warnings.warn(
[15]:
<matplotlib.collections.QuadMesh at 0x15128ec02790>
Stacking results into one data array¶
[16]:
dist_ice = xr.concat([distances_t0.where(is_ice), distances_t1.where(mask)], dim='time')
dist_ice
[16]:
<xarray.DataArray (time: 2, yt_ocean: 713, xt_ocean: 3600)> Size: 41MB
dask.array<concatenate, shape=(2, 713, 3600), dtype=float64, chunksize=(1, 713, 3600), chunktype=numpy.ndarray>
Coordinates:
* yt_ocean (yt_ocean) float64 6kB -79.97 -79.93 -79.88 ... -45.11 -45.04
* xt_ocean (xt_ocean) float64 29kB -279.9 -279.8 -279.7 ... 79.75 79.85 79.95
TLON (yt_ocean, xt_ocean) float32 10MB dask.array<chunksize=(713, 3600), meta=np.ndarray>
TLAT (yt_ocean, xt_ocean) float32 10MB dask.array<chunksize=(713, 3600), meta=np.ndarray>
ULON (yt_ocean, xt_ocean) float32 10MB dask.array<chunksize=(713, 3600), meta=np.ndarray>
ULAT (yt_ocean, xt_ocean) float32 10MB dask.array<chunksize=(713, 3600), meta=np.ndarray>
* time (time) datetime64[ns] 16B 1977-12-31T12:00:00 1978-01-31T12:00:00[17]:
dist_ice.plot(col='time')
/g/data/xp65/public/apps/med_conda/envs/analysis3-25.08/lib/python3.11/site-packages/distributed/client.py:3363: UserWarning: Sending large graph of size 40.32 MiB.
This may cause some slowdown.
Consider loading the data with Dask directly
or using futures or delayed objects to embed the data into the graph without repetition.
See also https://docs.dask.org/en/stable/best-practices.html#load-data-with-dask for more information.
warnings.warn(
[17]:
<xarray.plot.facetgrid.FacetGrid at 0x151529d74f50>
We are done! We should be able to follow the same workflow to calculate distances to any line/point of your interest.
[18]:
client.close()