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.

UXarray logo

Spatial Selection Operators

In this tutorial, you’ll learn:

  • Using UXarray to select specific regions from an unstructured grid

Prerequisites

ConceptsImportanceNotes
UXDataset & UxDataArray NotebookNecessary

Time to learn: 10 minutes


Overview

When working with unstructured grids, a region or zone rather than the entire global grid might be of interest for data analysis and visualization. Such spatial selection of the grid/data might help with not only analyzing/plotting a specific region/zone, but also reducing the data size and increasing the performance as well as allowing the entire plots to be effectively displayed on the screen.

This notebook showcases how to spatially subset a grid and take a cross-section of the grid at a constant latitude and longitude.

Subsetting

UXarray provides functionality for subsetting the grid into a bounding box, bounding circle, or K-nearest neighbors.

Before demonstrating them, let us load some global data first:

Load Data

# Import
import cartopy.crs as ccrs
import geoviews.feature as gf
import uxarray as ux

grid_path = "../../meshfiles/x1.655362.grid.nc"
data_path = "../../meshfiles/x1.655362.data.nc"

# Open dataset and grab a data variable of interest
uxds = ux.open_dataset(grid_path, data_path)
uxda = uxds["relhum_200hPa"][0]

Plot The Global Data

Note!

The visualizations throughout this tutorial are only for demonstrating the results of the subsetting and cross-sections versus the global grid. Since the details of plotting with UXarray will be covered in the next chapter, we will not go over any details of the plots here.
plot_opts = {"width": 700, "height": 350}

features = gf.coastline(
    projection=ccrs.PlateCarree(), line_width=0.4, scale="50m"
) * gf.states(projection=ccrs.PlateCarree(), line_width=0.4, scale="50m")

clim = (uxda.values.min(), uxda.values.max())

uxda.plot(
    rasterize=True, periodic_elements="exclude", title="Global Grid", **plot_opts
) * features
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[2], line 3
      1 plot_opts = {"width": 700, "height": 350}
      2 
----> 3 features = gf.coastline(
      4     projection=ccrs.PlateCarree(), line_width=0.4, scale="50m"
      5 ) * gf.states(projection=ccrs.PlateCarree(), line_width=0.4, scale="50m")
      6 

File ~/micromamba/envs/unstructured-grid-viz-cookbook-dev/lib/python3.14/site-packages/geoviews/element/geo.py:188, in Feature.__call__(self, *args, **kwargs)
    187 def __call__(self, *args, **kwargs):
--> 188     return self.clone().opts(*args, **kwargs)

File ~/micromamba/envs/unstructured-grid-viz-cookbook-dev/lib/python3.14/site-packages/holoviews/core/accessors.py:39, in AccessorPipelineMeta.pipelined.<locals>.pipelined_call(*args, **kwargs)
     35 inst = args[0]
     37 if not hasattr(inst._obj, "_pipeline"):
     38     # Wrapped object doesn't support the pipeline property
---> 39     return __call__(*args, **kwargs)
     41 inst_pipeline = copy.copy(inst._obj._pipeline)
     42 in_method = inst._obj._in_method

File ~/micromamba/envs/unstructured-grid-viz-cookbook-dev/lib/python3.14/site-packages/holoviews/core/accessors.py:651, in Opts.__call__(self, *args, **kwargs)
    643         msg = (
    644             "Calling the .opts method with options broken down by options "
    645             "group (i.e. separate plot, style and norm groups) has been removed. "
    646             "Use the .options method converting to the simplified format "
    647             "instead or use hv.opts.apply_groups for backward compatibility."
    648         )
    649         raise ValueError(msg)
--> 651 return self._dispatch_opts(*args, **kwargs)

File ~/micromamba/envs/unstructured-grid-viz-cookbook-dev/lib/python3.14/site-packages/holoviews/core/accessors.py:655, in Opts._dispatch_opts(self, *args, **kwargs)
    653 def _dispatch_opts(self, *args, **kwargs):
    654     if self._mode is None:
--> 655         return self._base_opts(*args, **kwargs)
    656     elif self._mode == "holomap":
    657         return self._holomap_opts(*args, **kwargs)

File ~/micromamba/envs/unstructured-grid-viz-cookbook-dev/lib/python3.14/site-packages/holoviews/core/accessors.py:740, in Opts._base_opts(self, *args, **kwargs)
    737     return opts.apply_groups(self._obj, **dict(kwargs, **new_kwargs))
    739 kwargs["clone"] = False if clone is None else clone
--> 740 return self._obj.options(*new_args, **kwargs)

File ~/micromamba/envs/unstructured-grid-viz-cookbook-dev/lib/python3.14/site-packages/holoviews/core/dimension.py:1478, in Dimensioned.options(self, clone, *args, **kwargs)
   1476     expanded_backends = opts._expand_by_backend(options, backend)
   1477 else:
-> 1478     expanded_backends = [(backend, opts._expand_options(options, backend))]
   1480 obj = self
   1481 for backend, expanded in expanded_backends:

File ~/micromamba/envs/unstructured-grid-viz-cookbook-dev/lib/python3.14/site-packages/holoviews/util/__init__.py:353, in opts._expand_options(cls, options, backend)
    350 current_backend = Store.current_backend
    352 if not Store.renderers:
--> 353     raise ValueError(
    354         "No plotting extension is currently loaded. "
    355         "Ensure you load an plotting extension with "
    356         "hv.extension or import it explicitly from "
    357         "holoviews.plotting before applying any "
    358         "options."
    359     )
    360 elif current_backend not in Store.renderers:
    361     raise ValueError(
    362         f"Currently selected plotting extension {current_backend!r} "
    363         "has not been loaded, ensure you load it "
    364         f"with hv.extension({current_backend!r}) before setting "
    365         "options"
    366     )

ValueError: No plotting extension is currently loaded. Ensure you load an plotting extension with hv.extension or import it explicitly from holoviews.plotting before applying any options.

Generate Various Subsets

# Bounding box around Boulder, CO
ref_lon = -105.2705
ref_lat = 40.0150
ref_offset = 1

lon_bounds = (ref_lon - ref_offset, ref_lon + ref_offset)
lat_bounds = (ref_lat - ref_offset, ref_lat + ref_offset)

# Subset the global data variable and its grid using the bounding box
uxda_bbox = uxda.subset.bounding_box(lon_bounds, lat_bounds)
# Now use a bounding circle subsetting
ref_center = [ref_lon, ref_lat]

uxda_bcircle = uxda.subset.bounding_circle(ref_center, ref_offset)
# Now use a K-nearest neighbor subsetting
k_nn = 60

uxda_nn = uxda.subset.nearest_neighbor(ref_center, k=k_nn, element="nodes")

Plot The Subsets

plot_bbox = (
    uxda_bbox.plot(
        rasterize=True,
        periodic_elements="exclude",
        clim=clim,
        title="Bounding Box Subset around Boulder, CO (Corner Node Query)",
        **plot_opts,
    )
    * features
)

plot_bcircle = (
    uxda_bcircle.plot(
        rasterize=True,
        periodic_elements="exclude",
        clim=clim,
        title="Bounding Circle Subset around Boulder, CO (Corner Node Query)",
        **plot_opts,
    )
    * features
)

plot_nn = (
    uxda_nn.plot(
        rasterize=True,
        periodic_elements="exclude",
        clim=clim,
        title="K-Nearest Neighbor Subset around Boulder, CO (Corner Node Query)",
        **plot_opts,
    )
    * features
)

(plot_bbox + plot_bcircle + plot_nn).cols(1)

Cross-sections

Similarly, UXarray provides functionality for taking cross-sections of a grid/data at constant latitudes and longitudes.

Load Data

# Data paths
grid_path = "../../meshfiles/outCSne30.grid.ug"
data_path = "../../meshfiles/outCSne30.data.nc"

# Open dataset and grab a data variable of interest
uxds = ux.open_dataset(grid_path, data_path)
uxda = uxds["psi"]

Plot The Global Data

projection = ccrs.Robinson()

features_2 = gf.coastline(
    projection=projection, line_width=0.4, scale="50m"
) * gf.states(projection=projection, line_width=0.4, scale="50m")

uxda.plot(
    cmap="inferno",
    periodic_elements="split",
    projection=projection,
    title="Global Plot",
) * features_2

Generate Cross-Sections

# Cross-section at the latitude of Boulder, CO
cross_lat = 40.0150

uxda_cross_lat = uxda.cross_section.constant_latitude(cross_lat)
# Now cross-section at the longitude of Boulder, CO
cross_lon = -105.2705

uxda_cross_lon = uxda.cross_section.constant_longitude(cross_lon)

Plot The Cross-Sections

plot_cross_lat = (
    uxda_cross_lat.plot(
        rasterize=False,
        backend="bokeh",
        cmap="inferno",
        projection=projection,
        global_extent=True,
        coastline=True,
        title=f"Cross-section at ({cross_lat}) degrees latitude around Boulder, CO",
    )
    * features_2
)

plot_cross_lon = (
    uxda_cross_lon.plot(
        rasterize=False,
        backend="bokeh",
        cmap="inferno",
        projection=projection,
        global_extent=True,
        coastline=True,
        title=f"Cross-section at ({cross_lon}) degrees longitude around Boulder, CO",
    )
    * features_2
)

(plot_cross_lat + plot_cross_lon).cols(1)

What is next?

With this section, we have wrapped up this chapter, move on to the next chapter!