Model Support Guide
Nereus provides specialized support for various atmospheric and ocean models with unstructured grids.
Unified Mesh System
All meshes in nereus are represented as xarray Datasets with standardized variable names. This provides a consistent interface across all supported models.
Key design principles:
Mesh IS an xr.Dataset: No special classes, just standard xarray
Standardized names:
lon,lat,area,triangles,depthStandalone spatial functions:
nr.find_nearest(lon, lat, ...)Auto dask detection: Large meshes (>1M points) automatically use dask arrays
Standard Variable Names
All mesh datasets contain these standardized variables:
Variable |
Dimension |
Description |
|---|---|---|
|
|
Longitude, normalized to [-180, 180] degrees |
|
|
Latitude in degrees |
|
|
Cell/node area in m² |
Optional variables (model-dependent):
Variable |
Dimension |
Description |
|---|---|---|
|
|
0-indexed triangle connectivity |
|
|
Element center coordinates |
|
|
Layer center depths (positive down) |
|
|
Layer interfaces |
|
|
Layer thickness in meters |
|
|
3D node area with NaN below ocean bottom (for masking/volume computations) |
Supported Models
Module |
Model |
Status |
|---|---|---|
|
FESOM2 |
Fully implemented |
|
HEALPix grids |
Fully implemented |
|
NEMO |
Fully implemented |
|
IFS TCO |
Fully implemented |
|
MITgcm |
Fully implemented |
|
MPAS-Ocean |
Fully implemented |
|
ICON-Ocean |
Planned |
|
ICON-Atmosphere |
Planned |
|
IFS (ECMWF) |
Planned |
FESOM2
The Finite Element Sea ice-Ocean Model version 2 uses an unstructured triangular mesh.
Loading a FESOM2 Mesh
import nereus as nr
# Load mesh from directory (returns xr.Dataset)
mesh = nr.fesom.load_mesh("/path/to/mesh/")
# Check what we got
print(type(mesh)) # <class 'xarray.core.dataset.Dataset'>
print(mesh.sizes) # {'npoints': 126858, 'nelem': 244659, 'nz': 47, ...}
The mesh directory should contain:
nod2d.out: 2D node coordinateselem2d.out: Element (triangle) connectivitymesh.diag.ncorfesom.mesh.diag.nc: Areas and depth infoaux3d.out(optional): Vertical level information
Accessing Mesh Data
Use standard xarray syntax to access mesh variables:
mesh = nr.fesom.load_mesh("/path/to/mesh/")
# As xr.DataArray
lon = mesh["lon"]
lat = mesh["lat"]
area = mesh["area"]
# As numpy arrays (for use with other functions)
lon_np = mesh["lon"].values
lat_np = mesh["lat"].values
area_np = mesh["area"].values
# Check mesh attributes
print(mesh.attrs["nereus_mesh_type"]) # 'fesom'
print(mesh.attrs["nereus_dask_backend"]) # False (or True for large meshes)
Triangles and Element Centers
# Triangle connectivity (0-indexed)
triangles = mesh["triangles"].values # Shape: (nelem, 3)
# Element center coordinates (pre-computed)
lon_tri = mesh["lon_tri"].values
lat_tri = mesh["lat_tri"].values
Vertical Structure
# Layer center depths
depth = mesh["depth"].values # Shape: (nz,)
# Layer bounds
depth_bounds = mesh["depth_bounds"].values # Shape: (nz, 2)
# Layer thickness
thickness = mesh["layer_thickness"].values # Shape: (nz,)
Opening FESOM2 Data
# Open data file with mesh coordinates
ds = nr.fesom.open_dataset("fesom_output.nc", mesh=mesh)
# The dataset now has lon/lat as coordinates
print(ds)
# Plot directly
fig, ax, _ = nr.plot(ds.temp.isel(time=0, nz1=0), mesh["lon"].values, mesh["lat"].values)
Spatial Queries
Use standalone functions with coordinate arrays:
# Find nearest mesh point to a location
idx = nr.find_nearest(mesh["lon"].values, mesh["lat"].values, -30, 45)
print(f"Nearest node index: {idx}")
# With distance
idx, dist = nr.find_nearest(mesh["lon"].values, mesh["lat"].values, -30, 45, return_distance=True)
print(f"Distance: {dist/1000:.1f} km")
# Find k nearest neighbors
indices = nr.find_nearest(mesh["lon"].values, mesh["lat"].values, -30, 45, k=10)
print(f"10 nearest nodes: {indices}")
# Query multiple locations at once
query_lons = np.array([-30, -40, -50])
query_lats = np.array([45, 50, 55])
indices = nr.find_nearest(mesh["lon"].values, mesh["lat"].values, query_lons, query_lats)
Geographic Subsetting
# Get mask for bounding box
mask = nr.subset_by_bbox(
mesh["lon"].values, mesh["lat"].values,
lon_min=-80, lon_max=0,
lat_min=0, lat_max=65
)
# Apply mask to data
atlantic_temp = temp.values[mask]
atlantic_area = mesh["area"].values[mask]
# Use mask in diagnostics
atlantic_ice = nr.ice_area(conc, mesh["area"], mask=mask)
FESOM-Specific Functions
# Interpolate node data to element centers
temp_elements = nr.fesom.node_to_element(temp_nodes, mesh)
# Interpolate element data back to nodes
temp_nodes = nr.fesom.element_to_node(temp_elements, mesh)
# Compute element centers (if not already present)
mesh = nr.fesom.compute_element_centers(mesh)
Depth Masking
FESOM output files may contain invalid values (zeros or near-zeros) for grid cells
below the ocean bottom. The mesh provides nod_area_nans derived from the nod_area
variable, with zeros replaced by NaN to indicate cells below the seafloor.
When loading from a NetCDF mesh file (fesom.mesh.diag.nc), nereus creates:
nod_area_nans: 3D node area array (shape:nz, npoints) with NaN below bottom
This can be used directly in volume computations or with mask_by_depth() to mask data:
import nereus as nr
# Load mesh
mesh = nr.fesom.load_mesh("fesom.mesh.diag.nc")
# Check if nod_area_nans is available
print("nod_area_nans" in mesh) # True for NetCDF meshes with 3D nod_area
# Mask a 3D temperature field
temp_masked = nr.fesom.mask_by_depth(temp_3d, mesh.nod_area_nans)
# Result: numpy array with NaN where nod_area_nans is NaN
# Mask a single depth level
temp_lev10_masked = nr.fesom.mask_by_depth(
temp_3d[10, :],
mesh.nod_area_nans[10, :]
)
# Use nod_area_nans directly in volume computations
# (NaN values are automatically excluded by nansum/nanmean)
volume = np.nansum(mesh.nod_area_nans.values * mesh.layer_thickness.values[:, None])
The function returns a numpy array (not xarray) to avoid dimension name alignment issues that can occur with inconsistent dimension names across FESOM output files.
Use cases:
Plotting transects without bottom artifacts
Computing volume integrals excluding invalid cells
Creating Hovmöller diagrams with proper bottom masking
Note
nod_area_nans is only available when loading from NetCDF mesh files that contain
3D nod_area variable. ASCII-only meshes do not have this information.
HEALPix
HEALPix (Hierarchical Equal Area isoLatitude Pixelization) grids are used by ICON and other models.
Creating a HEALPix Mesh
import nereus as nr
# Create mesh from number of points
mesh = nr.healpix.load_mesh(3145728) # nside=512
# Or use helper function
npoints = nr.healpix.nside_to_npoints(512) # 3145728
mesh = nr.healpix.load_mesh(npoints)
# Find appropriate nside for desired resolution
nside = nr.healpix.resolution_to_nside(1.0) # ~1 degree -> nside=64
HEALPix Properties
# All pixels have equal area
pixel_area = mesh["area"].values[0] # Same for all pixels
print(f"Pixel area: {pixel_area/1e6:.1f} km²")
# Access coordinates
lon = mesh["lon"].values
lat = mesh["lat"].values
# Mesh attributes
print(mesh.attrs["nside"]) # 512
print(mesh.attrs["ordering"]) # 'NESTED' or 'RING'
NEMO
NEMO (Nucleus for European Modelling of the Ocean) uses structured grids that are flattened for compatibility.
Loading a NEMO Mesh
import nereus as nr
# Load from mesh_mask.nc file
mesh = nr.nemo.load_mesh("/path/to/mesh_mask.nc")
# Coordinates are flattened from 2D to 1D
print(mesh.sizes["npoints"]) # Total ocean points
# Original 2D shape is stored in attributes
print(f"Original shape: {mesh.attrs['nlon']} x {mesh.attrs['nlat']}")
MITgcm
MITgcm (MIT General Circulation Model) uses a custom binary format (MDS: .meta + .data file pairs).
Nereus reads these natively without requiring the xmitgcm package.
Loading a MITgcm Mesh
import nereus as nr
# Load mesh from directory containing grid files (XC, YC, RAC, etc.)
mesh = nr.mitgcm.load_mesh("/path/to/run/")
# Coordinates are flattened from 2D to 1D
print(mesh.sizes["npoints"]) # Total grid points
# Original 2D shape is stored in attributes
print(f"Original shape: {mesh.attrs['ny']} x {mesh.attrs['nx']}")
# Depth levels and layer thickness (if RC.data/DRF.data exist)
print(mesh["depth"].values)
print(mesh["layer_thickness"].values)
# Use corner-point grid instead of cell centers
mesh_g = nr.mitgcm.load_mesh("/path/to/run/", grid_type="G")
Loading MITgcm Diagnostic Data
# Load 2D diagnostics with time coordinate
ds = nr.mitgcm.open_dataset(
"/path/to/run/",
prefix="diags2D",
delta_t=3600, # model timestep in seconds
ref_date="1710-1-1", # reference date for time axis
mesh=mesh, # attach lon/lat/depth coordinates
)
# Load multiple diagnostic prefixes at once
ds = nr.mitgcm.open_dataset(
"/path/to/run/",
prefix=["diags2D", "diags3D"],
delta_t=3600,
ref_date="1710-1-1",
mesh=mesh,
)
# Load specific iterations only
ds = nr.mitgcm.open_dataset(
"/path/to/run/",
prefix="diags3D",
iters=[8760, 17520],
delta_t=3600,
)
Land Masking
By default, MITgcm land points are filled with zeros. Use mask_land=True
to replace them with NaN. The mask is derived from hFacC.data (3D per-level,
preferred) or Depth.data > 0 (2D fallback when hFacC is absent).
# Load mesh with land mask (reads hFacC.data)
mesh = nr.mitgcm.load_mesh("/path/to/run/", mask_land=True)
# mesh now contains:
# - land_mask: boolean (npoints,), True = land
# - hFacC: float (depth_level, npoints), 0=land, 1=ocean, partial cells in between
print(f"Ocean points: {(~mesh['land_mask'].values).sum()}")
# Load data with automatic land masking (zeros → NaN)
ds = nr.mitgcm.open_dataset(
"/path/to/run/",
prefix="diags3D",
delta_t=3600,
mesh=mesh,
mask_land=True, # uses hFacC for 3D per-level masking
)
# Or apply the mask manually to any variable
ds["THETA"].where(~mesh["land_mask"]) # 2D surface mask
ds["THETA"].where(mesh["hFacC"] > 0) # 3D per-level mask
MPAS-Ocean
MPAS-Ocean (Model for Prediction Across Scales) uses an unstructured Voronoi mesh. All data and mesh information are stored in standard NetCDF files — mesh variables (coordinates, areas, connectivity) are embedded directly in every output file.
Loading an MPAS-Ocean Mesh
import nereus as nr
# Load mesh from any MPAS-Ocean NetCDF file
mesh = nr.mpas.load_mesh("/path/to/mpas_output.nc")
# Mesh info
print(mesh.sizes["npoints"]) # Number of cells
print(mesh["depth"].values) # Layer center depths
print(mesh["layer_thickness"].values) # Layer thicknesses
print(mesh["bathymetry"].values) # Bottom depth per cell
print(mesh["maxLevelCell"].values) # Deepest active level per cell
Since MPAS embeds mesh information in every output file, any MPAS-Ocean NetCDF file can serve as the mesh source. Coordinates are automatically converted from radians to degrees and normalized to [-180, 180].
Opening MPAS-Ocean Data
MPAS data is standard NetCDF, so open_dataset simply opens the file
and attaches mesh coordinates:
# Open with a pre-loaded mesh
ds = nr.mpas.open_dataset("/path/to/output.nc", mesh=mesh)
# Or let it load the mesh from the data file itself
ds = nr.mpas.open_dataset("/path/to/output.nc")
# Access data with coordinates
sst = ds["temperature"].isel(Time=0, nVertLevels=0)
print(ds.coords) # lon, lat, depth attached
IFS TCO
IFS TCO meshes use separate grid and area files with A* variables for
longitude, latitude, and cell surface area.
Loading an IFS TCO Mesh
import nereus as nr
mesh = nr.ifs_tco.load_mesh(
"/path/to/tco_grid.nc",
"/path/to/tco_areas.nc",
)
print(mesh.sizes["npoints"])
print(mesh["lon"].values[:5])
Universal Mesh Loader
The nr.load_mesh() function auto-detects mesh type:
# Auto-detect FESOM mesh
mesh = nr.load_mesh("/path/to/fesom/mesh/")
# Auto-detect NEMO mesh
mesh = nr.load_mesh("/path/to/mesh_mask.nc")
# Auto-detect MITgcm mesh
mesh = nr.load_mesh("/path/to/mitgcm/run/")
# Auto-detect MPAS mesh
mesh = nr.load_mesh("/path/to/mpas_output.nc")
# Explicit type
mesh = nr.load_mesh("/path/to/mesh/", mesh_type="fesom")
# HEALPix from npoints
mesh = nr.load_mesh(3145728, mesh_type="healpix")
Creating Regular Lon-Lat Grids
Create regular grids as mesh datasets for comparison or regridding targets:
# 1-degree global grid
mesh_1deg = nr.create_lonlat_mesh(1.0)
# Different resolution in lon/lat
mesh = nr.create_lonlat_mesh((0.5, 0.25)) # 0.5° lon, 0.25° lat
# Regional grid
mesh_regional = nr.create_lonlat_mesh(
0.25,
lon_bounds=(-80, 0),
lat_bounds=(20, 70)
)
Creating Meshes from Arrays
Create mesh datasets from existing coordinate arrays:
import numpy as np
# From 1D arrays
lon = np.array([...])
lat = np.array([...])
area = np.array([...]) # Optional
mesh = nr.mesh_from_arrays(lon, lat, area=area)
# 2D arrays are automatically flattened
lon_2d = np.array([[...]])
lat_2d = np.array([[...]])
mesh = nr.mesh_from_arrays(lon_2d, lat_2d)
# 1D regular grid coordinates (different sizes) are expanded via meshgrid
# e.g., lon(360) and lat(173) from a regular grid dataset
ds = xr.open_dataset("regular_grid_data.nc")
mesh = nr.mesh_from_arrays(ds.lon, ds.lat) # creates 360*173 = 62,280 points
Using Meshes with Diagnostics
Mesh area integrates directly with diagnostic functions:
# Sea ice area
ice_area = nr.ice_area(sic, mesh["area"], mask=mesh["lat"].values > 0)
# Ice extent
ice_extent = nr.ice_extent(sic, mesh["area"], threshold=0.15)
# Surface mean
sst_mean = nr.surface_mean(sst, mesh["area"])
# Volume mean (3D data)
temp_mean = nr.volume_mean(
temp_3d,
mesh["area"],
mesh["layer_thickness"],
mesh["depth"],
depth_max=500
)
Mesh Metadata
All nereus meshes include metadata attributes:
mesh.attrs["nereus_mesh_type"] # 'fesom', 'healpix', 'nemo', 'lonlat'
mesh.attrs["nereus_mesh_version"] # '1.0'
mesh.attrs["nereus_dask_backend"] # True/False
mesh.attrs["nereus_source_path"] # Original file path (if applicable)
Check if a dataset is a nereus mesh:
from nereus.core.mesh import is_nereus_mesh, get_mesh_type
if is_nereus_mesh(ds):
print(f"Mesh type: {get_mesh_type(ds)}")
Dask Support
Large meshes (>1M points) automatically use dask arrays:
# Auto-detect (>1M points uses dask)
mesh = nr.fesom.load_mesh("/path/to/large/mesh/")
print(mesh.attrs["nereus_dask_backend"]) # True
# Force dask for smaller meshes
mesh = nr.fesom.load_mesh("/path/to/mesh/", use_dask=True)
# Disable dask for large meshes
mesh = nr.fesom.load_mesh("/path/to/mesh/", use_dask=False)
# Check threshold
from nereus.core.mesh import DASK_THRESHOLD_POINTS
print(f"Dask threshold: {DASK_THRESHOLD_POINTS:,} points") # 1,000,000