Subsetting SCHISM NetCDF data
This example shows how to subset the suxarray data.
# Import packages
from pathlib import Path
import numpy as np
import xarray as xr
import suxarray as sx
# Where the test data is located.
# The test data is a subset from a Bay-Delta SCHISM model at Carquinez Strait.
dir_data = Path('../../tests/testdata')
# Read the test data
files_out2d = [str(p) for p in [dir_data / 'out2d_1.nc', dir_data / 'out2d_2.nc']]
files_zcoords = [str(p) for p in [dir_data / 'zCoordinates_1.nc', dir_data / 'zCoordinates_2.nc']]
grid = sx.open_grid(files_out2d, files_zcoords)
# Read salinity data directly in native float32 format
files_salinity = [dir_data / 'salinity_1.nc', dir_data / 'salinity_2.nc']
ds_salinity = xr.open_mfdataset(files_salinity, parallel=True)
ds_salinity.attrs = {
"Conventions": "CF-1.8",
"title": "Bay-Delta SCHISM model at Carquinez Strait",
"source": "SCHISM model",
}
sxds = sx.read_schism_nc(grid, ds_salinity)
ds_salinity.attrs
{'Conventions': 'CF-1.8',
'title': 'Bay-Delta SCHISM model at Carquinez Strait',
'source': 'SCHISM model'}
sxds.attrs
{'Conventions': 'CF-1.8',
'title': 'Bay-Delta SCHISM model at Carquinez Strait',
'source': 'SCHISM model'}
Visualize the original data
Let’s visualize the surface salinity data at the first time step from the dataset.
# Select the surface level first, then calculate the topological mean to elements (faces)
sxds_salinity_face = sxds.salinity.isel(n_layer=-1).topological_mean(destination="face")
# Visualize the result
sxds_salinity_face.isel(time=0).plot().opts(width=600, height=350)
Subset the data
Let’s subset the small blob at the top left corner of the original data set. We will use bounding_box_xy method in subset accessor. This method accepts the bounding box coordinates in the form of two tuples. The first tuple is the range of x, and the second tuple the range of y coordinates. Note that the visualization uses the longitude and latitude coordinates, but the dataset keeps the original projected x and y coordinates, and they are used by this subset method.
bounding_x = [569268., 570000.]
bounding_y = [4213114., 4213630.]
sxda_subset = sxds.salinity.subset.bounding_box_xy(bounding_x, bounding_y)
sxda_subset.isel(time=0, n_layer=-1).topological_mean(destination="face").plot().opts(width=600, height=350)
sxda_subset
<xarray.SxDataArray 'salinity' (time: 96, n_node: 62, n_layer: 23)> Size: 1MB
dask.array<getitem, shape=(96, 62, 23), dtype=float64, chunksize=(48, 62, 23), chunktype=numpy.ndarray>
Coordinates:
* time (time) datetime64[ns] 768B 2014-12-17T16:30:00 ... 2...
node_x (n_node) float64 496B dask.array<chunksize=(62,), meta=np.ndarray>
node_y (n_node) float64 496B dask.array<chunksize=(62,), meta=np.ndarray>
SCHISM_hgrid_node_x (n_node) float64 496B dask.array<chunksize=(62,), meta=np.ndarray>
SCHISM_hgrid_node_y (n_node) float64 496B dask.array<chunksize=(62,), meta=np.ndarray>
node_z (n_node) float32 248B dask.array<chunksize=(62,), meta=np.ndarray>
Dimensions without coordinates: n_node, n_layer
Attributes:
i23d: 2
location: node
grid_mapping: crs
mesh: SCHISM_hgridCalculate Volume under subset
from suxarray.grid.integrate import calc_volumes_from_projected, _calculate_volumes_uxarray, _calculate_mass
sxda_volumes = sx.SxDataArray(
xr.DataArray(
calc_volumes_from_projected(sxda_subset),
dims=["time", "n_layer", "n_face"],
coords={"time": sxda_subset.time},
name="volume",
),
sxgrid=sxda_subset.sxgrid,
)
# Visualize total water column volume per face at first timestep
sxda_volumes.sum(dim="n_layer").isel(time=0).plot()
Compare planar vs uxarray volume
We compute volumes using both implementations, then compare per-layer values and total water-column volumes.
# Compute uxarray-based per-layer face volumes (lat/lon face areas scaled by R^2)
volumes_ux = _calculate_volumes_uxarray(sxda_subset)
sxda_volumes_ux = sx.SxDataArray(
xr.DataArray(
volumes_ux,
dims=["time", "n_layer", "n_face"],
coords={"time": sxda_subset.time},
name="volume_ux",
),
sxgrid=sxda_subset.sxgrid,
)
sxda_volumes_ux
<xarray.SxDataArray 'volume_ux' (time: 96, n_layer: 22, n_face: 66)> Size: 1MB
array([[[ 0. , 0. , 0. , ...,
0. , 0. , 0. ],
[ 0. , 0. , 0. , ...,
0. , 0. , 0. ],
[ 0. , 0. , 0. , ...,
0. , 0. , 0. ],
...,
[6716.0632124 , 5864.38091235, 6530.21158725, ...,
470.28959784, 710.04427853, 575.72713514],
[6265.7617329 , 5541.57429678, 6092.52200288, ...,
470.28171698, 710.0320048 , 575.71737839],
[5845.67645761, 5240.50953111, 5684.13009277, ...,
470.28976202, 710.04450169, 575.72713514]],
[[ 0. , 0. , 0. , ...,
0. , 0. , 0. ],
[ 0. , 0. , 0. , ...,
0. , 0. , 0. ],
[ 0. , 0. , 0. , ...,
0. , 0. , 0. ],
...
434.42529919, 660.95692554, 532.88465672],
[6222.50068794, 5499.71410167, 6051.2940302 , ...,
434.41807507, 660.94532129, 532.87548537],
[5805.3152958 , 5200.87567394, 5645.66589242, ...,
434.42562756, 660.95692554, 532.88485186]],
[[ 0. , 0. , 0. , ...,
0. , 0. , 0. ],
[ 0. , 0. , 0. , ...,
0. , 0. , 0. ],
[ 0. , 0. , 0. , ...,
0. , 0. , 0. ],
...,
[6692.95996173, 5842.25828605, 6508.63028394, ...,
452.99850691, 686.66995425, 555.22781132],
[6244.20752314, 5520.64313315, 6072.38743961, ...,
452.99079023, 686.65812684, 555.2182497 ],
[5825.56724483, 5220.69009412, 5665.34477109, ...,
452.99850691, 686.66995425, 555.22781132]]],
shape=(96, 22, 66))
Coordinates:
* time (time) datetime64[ns] 768B 2014-12-17T16:30:00 ... 2014-12-19T16...
Dimensions without coordinates: n_layer, n_face# Per-layer, per-face comparison statistics
layer_abs_diff = np.abs(sxda_volumes_ux.values - sxda_volumes.values)
layer_rel_diff = layer_abs_diff / np.maximum(np.abs(sxda_volumes.values), 1.0e-12)
print("Per-layer abs diff max:", layer_abs_diff.max())
print("Per-layer abs diff mean:", layer_abs_diff.mean())
print("Per-layer rel diff max:", layer_rel_diff.max())
print("Per-layer rel diff mean:", layer_rel_diff.mean())
Per-layer abs diff max: 37.250629562274526
Per-layer abs diff mean: 1.7644504003320332
Per-layer rel diff max: 0.029262378558841222
Per-layer rel diff mean: 0.0009774319691265771
# Compare total water-column volume per face (sum over layers)
sxda_volumes_total = sxda_volumes.sum(dim="n_layer")
sxda_volumes_ux_total = sxda_volumes_ux.sum(dim="n_layer")
# Signed difference: uxarray - planar, shape (time, n_face)
total_diff = sxda_volumes_ux_total.values - sxda_volumes_total.values
total_abs_diff = np.abs(total_diff)
eps = 1.0e-12
# 1) Pointwise relative error (denominator is each face at each timestep)
denom_pointwise = np.maximum(np.abs(sxda_volumes_total.values), eps)
total_rel_diff_pointwise = total_diff / denom_pointwise
# 2) Timestep-normalized relative error (denominator is max face volume at each timestep)
denom_timestep = np.maximum(
np.abs(sxda_volumes_total.values).max(axis=1, keepdims=True),
eps,
)
total_rel_diff_timestep = total_diff / denom_timestep
# Per-timestep summaries
mean_abs_rel_per_time = np.mean(np.abs(total_rel_diff_timestep), axis=1)
max_abs_rel_per_time = np.max(np.abs(total_rel_diff_timestep), axis=1)
print("Pointwise rel diff mean:", np.mean(total_rel_diff_pointwise))
print("Pointwise |rel diff| mean:", np.mean(np.abs(total_rel_diff_pointwise)))
print("Pointwise |rel diff| max:", np.max(np.abs(total_rel_diff_pointwise)))
print("Timestep-normalized |rel diff| mean:", np.mean(np.abs(total_rel_diff_timestep)))
print("Timestep-normalized |rel diff| max:", np.max(np.abs(total_rel_diff_timestep)))
print("Per-time mean |rel diff| (first 10):", mean_abs_rel_per_time[:10])
print("Per-time max |rel diff| (first 10):", max_abs_rel_per_time[:10])
Pointwise rel diff mean: -0.00022845575726045358
Pointwise |rel diff| mean: 0.0011783982481104157
Pointwise |rel diff| max: 0.023980858656040164
Timestep-normalized |rel diff| mean: 0.00020286449568462282
Timestep-normalized |rel diff| max: 0.0021381875189729455
Per-time mean |rel diff| (first 10): [0.00019809 0.00019846 0.00019906 0.00019991 0.00020094 0.00020207
0.00020312 0.00020424 0.00020518 0.00020605]
Per-time max |rel diff| (first 10): [0.00199495 0.00199951 0.00200727 0.00201832 0.00203294 0.00204866
0.00206348 0.00207965 0.00209335 0.00210563]
from bokeh.models import PrintfTickFormatter
# Visualize only #3: magnitude of relative difference (pointwise denominator)
time_i = 0
# Fixed color limit for relative magnitude map
rel_lim = 0.005
# Build a DataArray from |(uxarray - planar) / planar|
sxda_total_rel_abs_diff_pointwise = sx.SxDataArray(
xr.DataArray(
np.abs(total_rel_diff_pointwise),
dims=sxda_volumes_total.dims,
coords=sxda_volumes_total.coords,
name="column_volume_rel_abs_diff_pointwise",
),
sxgrid=sxda_subset.sxgrid,
)
# Plot relative difference magnitude map with fixed decimal formatting for
# both colorbar ticks and hover values (avoids scientific notation).
sxda_total_rel_abs_diff_pointwise.isel(time=time_i).plot(cmap="Reds").opts(
width=600,
height=350,
clim=(0.0, rel_lim),
colorbar_opts={"formatter": PrintfTickFormatter(format="%.4f")},
cformatter="%.4f",
)
Calculate mass
sxda_mass = sx.SxDataArray(
xr.DataArray(
_calculate_mass(sxda_subset),
dims=["time", "n_layer", "n_face"],
coords={"time": sxda_subset.time},
name="mass",
),
sxgrid=sxda_subset.sxgrid,
)
# Visualize total water column mass per face at first timestep
sxda_mass.sum(dim="n_layer").isel(time=0).plot()