Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Extracting sites

Pulling a model timeseries out at a set of site coordinates is easy to get wrong by hand, especially near coastlines.

import os
import numpy as np
import xarray as xr
import matplotlib.pyplot as plt
import nc_time_axis  # registers the cftime axis converter for matplotlib
import x4c

x4c.set_style('journal')

# after set_style: it resets rcParams from matplotlibrc defaults, which includes the
# backend, so assert the inline backend last or figures are never captured
%matplotlib inline

# The tutorial runs against a reduced copy of a real CESM case. It is published as a
# GitHub Release asset rather than committed, so the first call downloads it into
# ~/.cache/x4c (override with $X4C_CACHE_DIR) and later calls reuse it. Set
# $X4C_SAMPLE_DIR to point at a copy you already have.
case_dir = x4c.fetch_sample_data(case='cesm1', verbose=False)
casename = os.path.basename(case_dir)
print('x4c', x4c.__version__)
x4c 2026.6.11
case = x4c.Timeseries(case_dir, grid_dict={'atm': 'ne16np4', 'ocn': 'g16'},
                      cesm_ver=1)
case.load('TS', timespan=(1, 10), verbose=False)

# EOFs need a regular grid, so regrid off the spectral-element grid first
ts = case.ds['TS'].x.regrid(2, 2).x.da
print(dict(ts.sizes))
>>> case.root_dir: /glade/u/home/fengzhu/.cache/x4c/sample_data/cesm1_sample_data/b.e13.B1850C5.ne16_g16.icesm131_d18O_fixer.Miocene.3xCO2.005
>>> case.path_pattern: comp/proc/tseries/*/casename.hstr.vn.timespan.nc
>>> case.grid_dict: {'atm': 'ne16np4', 'ocn': 'g16', 'lnd': 'ne16np4', 'rof': 'ne16np4', 'ice': 'g16'}
>>> case.casename: b.e13.B1850C5.ne16_g16.icesm131_d18O_fixer.Miocene.3xCO2.005
>>> case.paths["atm"]["cam.h0"] created
>>> case.paths["ocn"]["pop.h"] created
>>> case.paths["lnd"]["clm2.h0"] created
>>> case.paths["ice"]["cice.h"] created
>>> case.vns["atm"]["cam.h0"] created
>>> case.vns["ocn"]["pop.h"] created
>>> case.vns["lnd"]["clm2.h0"] created
>>> case.vns["ice"]["cice.h"] created
{'time': 120, 'lat': 90, 'lon': 180}

Extracting sites

.x.nearest2d() finds the nearest non-NaN grid cell to each target coordinate, which matters over the ocean: the nearest cell to a coastal site may be land. It returns a site dimension so a whole proxy network comes back as one array.

sites = {
    'equatorial Pacific': (0.0, 220.0),
    'north Atlantic':     (55.0, 330.0),
    'southern ocean':     (-60.0, 90.0),
}
lats = [v[0] for v in sites.values()]
lons = [v[1] for v in sites.values()]

pts = ts.x.nearest2d(lat=lats, lon=lons)
print(dict(pts.sizes))
for i, name in enumerate(sites):
    p = pts.isel(site=i)
    print(f'  {name:20s} nearest cell at lat={float(p.lat):+6.1f}, lon={float(p.lon):6.1f}'
          f'  mean TS = {float(p.mean()):.2f} K')
{'site': 3, 'time': 120}
  equatorial Pacific   nearest cell at lat=  -1.0, lon= 219.0  mean TS = 304.40 K
  north Atlantic       nearest cell at lat= +55.0, lon= 329.0  mean TS = 285.79 K
  southern ocean       nearest cell at lat= -61.0, lon=  89.0  mean TS = 284.37 K
fig, ax = plt.subplots(figsize=(9, 3))
for i, name in enumerate(sites):
    ax.plot(pts.time, pts.isel(site=i), lw=1, label=name)
ax.legend(frameon=False, ncol=3, fontsize=9)
ax.set(xlabel='Time', ylabel='TS [K]', title='Surface temperature at three sites')
x4c.showfig(fig)
<Figure size 900x300 with 1 Axes>

Three-dimensional sites

.x.nearest3d() does the same in depth as well, combining horizontal great-circle distance with the vertical offset. It works straight on the native POP grid, where latitude and longitude are 2-D arrays.

Target depths are given in metres; depth_unit describes the model coordinate ('cm' for POP).

case.load('TEMP', timespan=(1, 1), verbose=False)
temp = case.ds['TEMP'].x.da

pts3 = temp.x.nearest3d(
    lat=[0.0, -60.0], lon=[220.0, 90.0], depth=[25.0, 75.0],
    lat_coord='TLAT', lon_coord='TLONG', lat_dim='nlat', lon_dim='nlon',
    depth_coord='z_t', depth_dim='z_t', depth_unit='cm',
)
print(dict(pts3.sizes))
for i in range(pts3.sizes['site']):
    p = pts3.isel(site=i)
    print(f'  site {i}: lat={float(p.TLAT):+6.1f} lon={float(p.TLONG):6.1f} '
          f'z={float(p.z_t) / 100:6.1f} m  mean TEMP = {float(p.mean()):.2f} degC')
{'site': 2, 'time': 12}
  site 0: lat=  -0.3 lon= 219.6 z=  25.0 m  mean TEMP = 30.30 degC
  site 1: lat= -60.1 lon=  90.3 z=  75.0 m  mean TEMP = 8.50 degC