Reading ROMS Output#

[1]:
from roms_tools import Grid, ROMSOutput

For any type of visualization or analysis, we require information about the grid used in the model. We retrieve the grid data using the Grid.from_file method.

[2]:
grid = Grid(filename=
    "/anvil/projects/x-ees250129/Datasets/ROMSOutput/eastpac25km/epac25km_grd.nc"
)
2026-06-26 19:44:41 - WARNING - Vertical coordinates (Cs_r, Cs_w) not found in grid file and were not provided, using defaults.
2026-06-26 19:44:41 - INFO - === Preparing the vertical coordinate system using N = 100, theta_s = 5.0, theta_b = 2.0, hc = 300.0 ===
2026-06-26 19:44:41 - INFO - Total time: 0.004 seconds
2026-06-26 19:44:41 - INFO - ================================================================================================

The ROMSOutput class provides a flexible way to load ROMS output files using the path parameter, which can be specified in the following ways:

  1. Single file: If path is a single file, only that file will be loaded.

  2. List of files: If path is a list of file paths, the specified files will be loaded in the given order.

  3. Wildcards: If path contains wildcards (e.g., *rst*.nc), matching files are loaded in lexicographic order, assuming this reflects the correct temporal sequence (as is the case for standard ROMS output).

For cases 2 and 3, ROMS-Tools will attempt to concatenate the files along the time dimension. If this is not possible (e.g., due to inconsistent dimensions or metadata), an error may be thrown.

In the following example, we will read restart files generated during a ROMS simulation.

Reading a single file#

[3]:
%%time

roms_output_from_single_file = ROMSOutput(
    grid=grid,
    path="/anvil/projects/x-ees250129/Datasets/ROMSOutput/eastpac25km/eastpac25km_rst.19980106000000.nc",
    use_dask=True,
)
CPU times: user 188 ms, sys: 36.2 ms, total: 224 ms
Wall time: 305 ms

Note

In the cell above, we read our ROMS output files with use_dask = True. This enables Dask, a Python library designated to facilitate scalable, out-of-memory data processing by distributing computations across multiple threads or processes. Here you can learn more about using Dask with ROMS-Tools.

The .ds attribute contains an xarray.Dataset with the data that was read in. As you can see, the restart file contains two time stamps (10 minutes apart).

[4]:
roms_output_from_single_file.ds
[4]:
<xarray.Dataset> Size: 1GB
Dimensions:                (time: 2, auxil: 6, eta_rho: 162, xi_rho: 122,
                            xi_u: 121, eta_v: 161, s_rho: 100)
Coordinates:
  * time                   (time) datetime64[ns] 16B 1998-01-05T23:50:00 1998...
    lon_rho                (eta_rho, xi_rho) float64 158kB ...
    lat_rho                (eta_rho, xi_rho) float64 158kB ...
    lat_v                  (eta_v, xi_rho) float64 157kB 7.758 7.87 ... 52.18
    lon_v                  (eta_v, xi_rho) float64 157kB 231.8 232.0 ... 237.6
    lat_u                  (eta_rho, xi_u) float64 157kB 7.72 7.831 ... 52.23
    lon_u                  (eta_rho, xi_u) float64 157kB 231.9 232.1 ... 237.4
Dimensions without coordinates: auxil, eta_rho, xi_rho, xi_u, eta_v, s_rho
Data variables: (12/58)
    ocean_time             (time) float64 16B dask.array<chunksize=(1,), meta=np.ndarray>
    time_step              (time, auxil) int32 48B dask.array<chunksize=(1, 6), meta=np.ndarray>
    zeta                   (time, eta_rho, xi_rho) float64 316kB dask.array<chunksize=(1, 50, 50), meta=np.ndarray>
    ubar                   (time, eta_rho, xi_u) float64 314kB dask.array<chunksize=(1, 50, 50), meta=np.ndarray>
    vbar                   (time, eta_v, xi_rho) float64 314kB dask.array<chunksize=(1, 50, 50), meta=np.ndarray>
    MARBL_PH_3D            (time, s_rho, eta_rho, xi_rho) float64 32MB dask.array<chunksize=(1, 50, 50, 50), meta=np.ndarray>
    ...                     ...
    u_slow                 (time, s_rho, eta_rho, xi_u) float64 31MB dask.array<chunksize=(1, 50, 50, 50), meta=np.ndarray>
    v_slow                 (time, s_rho, eta_v, xi_rho) float64 31MB dask.array<chunksize=(1, 50, 50, 50), meta=np.ndarray>
    p_slow                 (time, s_rho, eta_rho, xi_rho) float64 32MB dask.array<chunksize=(1, 50, 50, 50), meta=np.ndarray>
    mask_rho               (eta_rho, xi_rho) float64 158kB ...
    mask_u                 (eta_rho, xi_u) int32 78kB 1 1 1 1 1 1 ... 0 0 0 0 0
    mask_v                 (eta_v, xi_rho) int32 79kB 1 1 1 1 1 1 ... 0 0 0 0 0
Attributes: (12/35)
    title:                 eastpac25km , 25km resolution
    grid_file:             /pscratch/sd/e/eay/EASTPAC25KM/INPUT_FIXED_TOPO/ep...
    init_file:             /pscratch/sd/e/eay/EASTPAC25KM_spinup/output/eastp...
    ntimes:                4610
    ndtfast:               45
    dt:                    600.0
    ...                    ...
    SRCS:                  SRCS $(shell ls *$(UPF77_ext)) SRCS : $(filter-out...
    CPPS:                  <cppdefs.opt> PACIFIC_PD SOLVE3D UV_ADV UV_COR ADV...
    surf_forcing_strings:
    bc_options:             OBC_WEST, OBC_NORTH, OBC_SOUTH, OBC_M3ORLANSKI, O...
    git_version:
    type:                  ROMS restart file

Reading multiple files#

[5]:
%%time

roms_output_from_two_files = ROMSOutput(
    grid=grid,
    path=[
        "/anvil/projects/x-ees250129/Datasets/ROMSOutput/eastpac25km/eastpac25km_rst.19980106000000.nc",
        "/anvil/projects/x-ees250129/Datasets/ROMSOutput/eastpac25km/eastpac25km_rst.19990201000000.nc",
    ],
    use_dask=True,
)
CPU times: user 115 ms, sys: 17 ms, total: 132 ms
Wall time: 180 ms

The two specified restart files were concatenated into a single xarray.Dataset, which now contains 4 time stamps.

[6]:
roms_output_from_two_files.ds
[6]:
<xarray.Dataset> Size: 3GB
Dimensions:                (time: 4, auxil: 6, eta_rho: 162, xi_rho: 122,
                            xi_u: 121, eta_v: 161, s_rho: 100)
Coordinates:
  * time                   (time) datetime64[ns] 32B 1998-01-05T23:50:00 ... ...
    lon_rho                (eta_rho, xi_rho) float64 158kB ...
    lat_rho                (eta_rho, xi_rho) float64 158kB ...
    lat_v                  (eta_v, xi_rho) float64 157kB 7.758 7.87 ... 52.18
    lon_v                  (eta_v, xi_rho) float64 157kB 231.8 232.0 ... 237.6
    lat_u                  (eta_rho, xi_u) float64 157kB 7.72 7.831 ... 52.23
    lon_u                  (eta_rho, xi_u) float64 157kB 231.9 232.1 ... 237.4
Dimensions without coordinates: auxil, eta_rho, xi_rho, xi_u, eta_v, s_rho
Data variables: (12/58)
    ocean_time             (time) float64 32B dask.array<chunksize=(1,), meta=np.ndarray>
    time_step              (time, auxil) int32 96B dask.array<chunksize=(1, 6), meta=np.ndarray>
    zeta                   (time, eta_rho, xi_rho) float64 632kB dask.array<chunksize=(1, 50, 50), meta=np.ndarray>
    ubar                   (time, eta_rho, xi_u) float64 627kB dask.array<chunksize=(1, 50, 50), meta=np.ndarray>
    vbar                   (time, eta_v, xi_rho) float64 629kB dask.array<chunksize=(1, 50, 50), meta=np.ndarray>
    MARBL_PH_3D            (time, s_rho, eta_rho, xi_rho) float64 63MB dask.array<chunksize=(1, 50, 50, 50), meta=np.ndarray>
    ...                     ...
    u_slow                 (time, s_rho, eta_rho, xi_u) float64 63MB dask.array<chunksize=(1, 50, 50, 50), meta=np.ndarray>
    v_slow                 (time, s_rho, eta_v, xi_rho) float64 63MB dask.array<chunksize=(1, 50, 50, 50), meta=np.ndarray>
    p_slow                 (time, s_rho, eta_rho, xi_rho) float64 63MB dask.array<chunksize=(1, 50, 50, 50), meta=np.ndarray>
    mask_rho               (eta_rho, xi_rho) float64 158kB ...
    mask_u                 (eta_rho, xi_u) int32 78kB 1 1 1 1 1 1 ... 0 0 0 0 0
    mask_v                 (eta_v, xi_rho) int32 79kB 1 1 1 1 1 1 ... 0 0 0 0 0
Attributes: (12/35)
    title:                 eastpac25km , 25km resolution
    grid_file:             /pscratch/sd/e/eay/EASTPAC25KM/INPUT_FIXED_TOPO/ep...
    init_file:             /pscratch/sd/e/eay/EASTPAC25KM_spinup/output/eastp...
    ntimes:                4610
    ndtfast:               45
    dt:                    600.0
    ...                    ...
    SRCS:                  SRCS $(shell ls *$(UPF77_ext)) SRCS : $(filter-out...
    CPPS:                  <cppdefs.opt> PACIFIC_PD SOLVE3D UV_ADV UV_COR ADV...
    surf_forcing_strings:
    bc_options:             OBC_WEST, OBC_NORTH, OBC_SOUTH, OBC_M3ORLANSKI, O...
    git_version:
    type:                  ROMS restart file

Reading files with wildcards#

[7]:
%%time

roms_output = ROMSOutput(
    grid=grid,
    path="/anvil/projects/x-ees250129/Datasets/ROMSOutput/eastpac25km/*rst*.nc",
    use_dask=True,
)
CPU times: user 119 ms, sys: 11 ms, total: 130 ms
Wall time: 134 ms

The specified directory contains 222 restart files, all of which were concatenated into one dataset. (The concatenation takes some time, even though we used use_dask = True.)

[8]:
roms_output.ds
[8]:
<xarray.Dataset> Size: 3GB
Dimensions:                (time: 4, auxil: 6, eta_rho: 162, xi_rho: 122,
                            xi_u: 121, eta_v: 161, s_rho: 100)
Coordinates:
  * time                   (time) datetime64[ns] 32B 1998-01-05T23:50:00 ... ...
    lon_rho                (eta_rho, xi_rho) float64 158kB ...
    lat_rho                (eta_rho, xi_rho) float64 158kB ...
    lat_v                  (eta_v, xi_rho) float64 157kB 7.758 7.87 ... 52.18
    lon_v                  (eta_v, xi_rho) float64 157kB 231.8 232.0 ... 237.6
    lat_u                  (eta_rho, xi_u) float64 157kB 7.72 7.831 ... 52.23
    lon_u                  (eta_rho, xi_u) float64 157kB 231.9 232.1 ... 237.4
Dimensions without coordinates: auxil, eta_rho, xi_rho, xi_u, eta_v, s_rho
Data variables: (12/58)
    ocean_time             (time) float64 32B dask.array<chunksize=(1,), meta=np.ndarray>
    time_step              (time, auxil) int32 96B dask.array<chunksize=(1, 6), meta=np.ndarray>
    zeta                   (time, eta_rho, xi_rho) float64 632kB dask.array<chunksize=(1, 50, 50), meta=np.ndarray>
    ubar                   (time, eta_rho, xi_u) float64 627kB dask.array<chunksize=(1, 50, 50), meta=np.ndarray>
    vbar                   (time, eta_v, xi_rho) float64 629kB dask.array<chunksize=(1, 50, 50), meta=np.ndarray>
    MARBL_PH_3D            (time, s_rho, eta_rho, xi_rho) float64 63MB dask.array<chunksize=(1, 50, 50, 50), meta=np.ndarray>
    ...                     ...
    u_slow                 (time, s_rho, eta_rho, xi_u) float64 63MB dask.array<chunksize=(1, 50, 50, 50), meta=np.ndarray>
    v_slow                 (time, s_rho, eta_v, xi_rho) float64 63MB dask.array<chunksize=(1, 50, 50, 50), meta=np.ndarray>
    p_slow                 (time, s_rho, eta_rho, xi_rho) float64 63MB dask.array<chunksize=(1, 50, 50, 50), meta=np.ndarray>
    mask_rho               (eta_rho, xi_rho) float64 158kB ...
    mask_u                 (eta_rho, xi_u) int32 78kB 1 1 1 1 1 1 ... 0 0 0 0 0
    mask_v                 (eta_v, xi_rho) int32 79kB 1 1 1 1 1 1 ... 0 0 0 0 0
Attributes: (12/35)
    title:                 eastpac25km , 25km resolution
    grid_file:             /pscratch/sd/e/eay/EASTPAC25KM/INPUT_FIXED_TOPO/ep...
    init_file:             /pscratch/sd/e/eay/EASTPAC25KM_spinup/output/eastp...
    ntimes:                4610
    ndtfast:               45
    dt:                    600.0
    ...                    ...
    SRCS:                  SRCS $(shell ls *$(UPF77_ext)) SRCS : $(filter-out...
    CPPS:                  <cppdefs.opt> PACIFIC_PD SOLVE3D UV_ADV UV_COR ADV...
    surf_forcing_strings:
    bc_options:             OBC_WEST, OBC_NORTH, OBC_SOUTH, OBC_M3ORLANSKI, O...
    git_version:
    type:                  ROMS restart file

Let’s verify that the time dimension has been concatenated in a linear sequence.

[9]:
roms_output.ds.time.plot()
[9]:
[<matplotlib.lines.Line2D at 0x1518165b8910>]
_images/reading_roms_output_19_1.png

Adjusting the depth for sea surface height#

ROMS uses a terrain-following vertical coordinate system. To visualize or regrid ROMS output data on a depth coordinate in later notebooks, ROMS-Tools internally computes the corresponding depth values. The treatment of sea surface height (SSH) is controlled by the adjust_depth_for_sea_surface_height parameter:

  • If adjust_depth_for_sea_surface_height = False (default), a constant sea surface height is assumed: \(\zeta(x,y,t) = 0\), which corresponds to measuring depth relative to the surface.

  • If adjust_depth_for_sea_surface_height = True, depth calculations account for spatial and temporal variations in SSH, making the depths time-dependent. This approach corresponds to measuring depth as elevation above the bottom.

For simplicity, the default setting is adjust_depth_for_sea_surface_height = False, which has been used so far in this notebook.

Next, we will enable adjust_depth_for_sea_surface_height = True.

[10]:
%%time

roms_output_adjusted_for_ssh = ROMSOutput(
    grid=grid,
    path="/anvil/projects/x-ees250129/Datasets/ROMSOutput/eastpac25km/*rst*.nc",
    adjust_depth_for_sea_surface_height=True,
    use_dask=True,
)
CPU times: user 118 ms, sys: 11 ms, total: 129 ms
Wall time: 129 ms

The adjust_depth_for_sea_surface_height parameter will affect:

  1. Plots of fields with a vertical dimension, such as those in this notebook.

  2. Regridding onto a lat-lon-z grid, affecting the regridded 3D variables.

Let’s explore the second point further by performing two regridding operations: one with adjust_depth_for_sea_surface_height = True and one with adjust_depth_for_sea_surface_height = False. For more details on regridding, see this notebook.

[11]:
ds_regridded = roms_output.regrid()
[12]:
ds_regridded
[12]:
<xarray.Dataset> Size: 6GB
Dimensions:                (time: 4, lat: 185, lon: 197, depth: 100,
                            eta_v: 161, xi_u: 121, auxil: 6)
Coordinates:
  * time                   (time) datetime64[ns] 32B 1998-01-05T23:50:00 ... ...
  * lat                    (lat) float32 740B 7.0 7.25 7.5 ... 52.5 52.75 53.0
  * lon                    (lon) float32 788B 208.0 208.2 208.5 ... 256.8 257.0
  * depth                  (depth) float32 400B 1.46 4.45 ... 5.528e+03
Dimensions without coordinates: eta_v, xi_u, auxil
Data variables: (12/58)
    NH4                    (time, lat, lon, depth) float64 117MB dask.array<chunksize=(1, 185, 197, 100), meta=np.ndarray>
    DOCr                   (time, lat, lon, depth) float64 117MB dask.array<chunksize=(1, 185, 197, 100), meta=np.ndarray>
    zooC                   (time, lat, lon, depth) float64 117MB dask.array<chunksize=(1, 185, 197, 100), meta=np.ndarray>
    diatFe                 (time, lat, lon, depth) float64 117MB dask.array<chunksize=(1, 185, 197, 100), meta=np.ndarray>
    DV_avg2                (time, eta_v, lat, lon) float64 188MB dask.array<chunksize=(1, 161, 185, 197), meta=np.ndarray>
    mask_u                 (xi_u, lat, lon) float64 35MB dask.array<chunksize=(121, 185, 197), meta=np.ndarray>
    ...                     ...
    salt                   (time, lat, lon, depth) float64 117MB dask.array<chunksize=(1, 185, 197, 100), meta=np.ndarray>
    diatC                  (time, lat, lon, depth) float64 117MB dask.array<chunksize=(1, 185, 197, 100), meta=np.ndarray>
    diatChl                (time, lat, lon, depth) float64 117MB dask.array<chunksize=(1, 185, 197, 100), meta=np.ndarray>
    NO3                    (time, lat, lon, depth) float64 117MB dask.array<chunksize=(1, 185, 197, 100), meta=np.ndarray>
    diatP                  (time, lat, lon, depth) float64 117MB dask.array<chunksize=(1, 185, 197, 100), meta=np.ndarray>
    MARBL_PH_SURF_ALT_CO2  (time, lat, lon) float64 1MB dask.array<chunksize=(1, 185, 197), meta=np.ndarray>
Attributes: (12/36)
    title:                 eastpac25km , 25km resolution
    grid_file:             /pscratch/sd/e/eay/EASTPAC25KM/INPUT_FIXED_TOPO/ep...
    init_file:             /pscratch/sd/e/eay/EASTPAC25KM_spinup/output/eastp...
    ntimes:                4610
    ndtfast:               45
    dt:                    600.0
    ...                    ...
    CPPS:                  <cppdefs.opt> PACIFIC_PD SOLVE3D UV_ADV UV_COR ADV...
    surf_forcing_strings:
    bc_options:             OBC_WEST, OBC_NORTH, OBC_SOUTH, OBC_M3ORLANSKI, O...
    git_version:
    type:                  ROMS restart file
    regrid_method:         bilinear
[13]:
ds_regridded_adjusted_for_ssh = roms_output_adjusted_for_ssh.regrid()
/home/x-kthyng/.conda/envs/romstools-test/lib/python3.13/site-packages/xarray/computation/apply_ufunc.py:312: PerformanceWarning: Regridding is increasing the number of chunks by a factor of 16.0, you might want to specify sizes in `output_chunks` in the regridder call. Default behaviour is to preserve the chunk sizes from the input (50, 50).
  result_var = func(*data_vars)
/home/x-kthyng/.conda/envs/romstools-test/lib/python3.13/site-packages/dask/array/routines.py:331: PerformanceWarning: Increasing number of chunks by factor of 11
  intermediate = blockwise(
[14]:
ds_regridded_adjusted_for_ssh
[14]:
<xarray.Dataset> Size: 6GB
Dimensions:                (time: 4, lat: 185, lon: 197, depth: 100,
                            eta_v: 161, xi_u: 121, auxil: 6)
Coordinates:
  * time                   (time) datetime64[ns] 32B 1998-01-05T23:50:00 ... ...
  * lat                    (lat) float32 740B 7.0 7.25 7.5 ... 52.5 52.75 53.0
  * lon                    (lon) float32 788B 208.0 208.2 208.5 ... 256.8 257.0
  * depth                  (depth) float32 400B 1.46 4.45 ... 5.528e+03
Dimensions without coordinates: eta_v, xi_u, auxil
Data variables: (12/58)
    NH4                    (time, lat, lon, depth) float64 117MB dask.array<chunksize=(1, 50, 50, 100), meta=np.ndarray>
    DOCr                   (time, lat, lon, depth) float64 117MB dask.array<chunksize=(1, 50, 50, 100), meta=np.ndarray>
    zooC                   (time, lat, lon, depth) float64 117MB dask.array<chunksize=(1, 50, 50, 100), meta=np.ndarray>
    diatFe                 (time, lat, lon, depth) float64 117MB dask.array<chunksize=(1, 50, 50, 100), meta=np.ndarray>
    DV_avg2                (time, eta_v, lat, lon) float64 188MB dask.array<chunksize=(1, 161, 185, 197), meta=np.ndarray>
    mask_u                 (xi_u, lat, lon) float64 35MB dask.array<chunksize=(121, 185, 197), meta=np.ndarray>
    ...                     ...
    salt                   (time, lat, lon, depth) float64 117MB dask.array<chunksize=(1, 50, 50, 100), meta=np.ndarray>
    diatC                  (time, lat, lon, depth) float64 117MB dask.array<chunksize=(1, 50, 50, 100), meta=np.ndarray>
    diatChl                (time, lat, lon, depth) float64 117MB dask.array<chunksize=(1, 50, 50, 100), meta=np.ndarray>
    NO3                    (time, lat, lon, depth) float64 117MB dask.array<chunksize=(1, 50, 50, 100), meta=np.ndarray>
    diatP                  (time, lat, lon, depth) float64 117MB dask.array<chunksize=(1, 50, 50, 100), meta=np.ndarray>
    MARBL_PH_SURF_ALT_CO2  (time, lat, lon) float64 1MB dask.array<chunksize=(1, 185, 197), meta=np.ndarray>
Attributes: (12/36)
    title:                 eastpac25km , 25km resolution
    grid_file:             /pscratch/sd/e/eay/EASTPAC25KM/INPUT_FIXED_TOPO/ep...
    init_file:             /pscratch/sd/e/eay/EASTPAC25KM_spinup/output/eastp...
    ntimes:                4610
    ndtfast:               45
    dt:                    600.0
    ...                    ...
    CPPS:                  <cppdefs.opt> PACIFIC_PD SOLVE3D UV_ADV UV_COR ADV...
    surf_forcing_strings:
    bc_options:             OBC_WEST, OBC_NORTH, OBC_SOUTH, OBC_M3ORLANSKI, O...
    git_version:
    type:                  ROMS restart file
    regrid_method:         bilinear
[15]:
import matplotlib.pyplot as plt
[16]:
indexers = {"time": 0, "depth": 0}
cbar_kwargs = {"label": r"meq/m$^3$"}
fig, axs = plt.subplots(1, 3, figsize=(17, 4))

ds_regridded["ALK"].isel(**indexers).plot(ax=axs[0], cbar_kwargs=cbar_kwargs)
axs[0].set_title("no adjustment for SSH")
ds_regridded_adjusted_for_ssh["ALK"].isel(**indexers).plot(
    ax=axs[1], cbar_kwargs=cbar_kwargs
)
axs[1].set_title("with adjustment for SSH")
(ds_regridded["ALK"] - ds_regridded_adjusted_for_ssh["ALK"]).isel(**indexers).plot(
    ax=axs[2], cbar_kwargs=cbar_kwargs
)
axs[2].set_title("Difference")

fig.suptitle("Surface Alkalinity", y=1.05)
[16]:
Text(0.5, 1.05, 'Surface Alkalinity')
_images/reading_roms_output_28_1.png

The plot above shows a difference between adjusting for sea surface height (SSH) and not, albeit a small one.

Computing the mixed layer depth#

There is a public-facing function available for calculating the mixed layer depth for post-processing. Here we demo the use.

[21]:
from roms_tools.setup.utils import compute_potential_density, compute_mld

sigma0 = compute_potential_density(roms_output.ds["temp"], roms_output.ds["salt"])
mld = compute_mld(sigma0, roms_output.ds_depth_coords["layer_depth_rho"], "s_rho")
mld.isel(time=0).plot(x="lon_rho", y="lat_rho")
[21]:
<matplotlib.collections.QuadMesh at 0x1517c6716210>
_images/reading_roms_output_31_1.png