Demo: mooring-level processing (Step 1)
This notebook demonstrates the first of the mooring-level processing steps, time_gridding.
Step 1: Time Gridding and Optional Filtering Demo
This notebook demonstrates the Step 1 processing workflow for mooring data:
Loading multiple instrument datasets
Optional time-domain filtering (applied BEFORE interpolation)
Interpolating onto a common time grid
Combining into a unified mooring dataset
Key Point: Filtering is applied to individual instrument records on their native time grids BEFORE interpolation to preserve data integrity.
[1]:
import numpy as np
import pandas as pd
import xarray as xr
import matplotlib.pyplot as plt
from pathlib import Path
import yaml
# Import the time gridding module
from oceanarray.time_gridding import time_gridding_mooring
# Set up plotting
plt.style.use("default")
Configuration
First, let’s set up our data paths and examine the mooring configuration.
[2]:
# Set your data paths here
basedir = "../data"
mooring_name = "dsE_1_2018"
# Construct paths
proc_dir = Path(basedir) / "moor" / "proc" / mooring_name
config_file = proc_dir / f"{mooring_name}.mooring.yaml"
print(f"Processing directory: {proc_dir}")
print(f"Configuration file: {config_file}")
print(f"Config exists: {config_file.exists()}")
Processing directory: ../data/moor/proc/dsE_1_2018
Configuration file: ../data/moor/proc/dsE_1_2018/dsE_1_2018.mooring.yaml
Config exists: False
[3]:
# Load and examine the mooring configuration
if config_file.exists():
with open(config_file, "r") as f:
config = yaml.safe_load(f)
print("Mooring Configuration:")
print(f"Name: {config['name']}")
print(f"Water depth: {config.get('waterdepth', 'unknown')} m")
print(
f"Location: {config.get('latitude', 'unknown')}°N, {config.get('longitude', 'unknown')}°E"
)
print(f"\nInstruments ({len(config.get('instruments', []))}):")
for i, inst in enumerate(config.get("instruments", [])):
print(
f" {i + 1}. {inst.get('instrument', 'unknown')} "
f"(serial: {inst.get('serial num.', 'unknown')}) at {inst.get('depth', 'unknown')} m"
)
else:
print("Configuration file not found!")
print("Please check your data path and mooring name.")
Configuration file not found!
Please check your data path and mooring name.
Examine individual instrument files
Let’s look at the individual instrument files before processing to understand the different sampling rates and data characteristics.
[4]:
# Find and examine individual instrument files
file_suffix = "_use"
instrument_files = []
instrument_datasets = []
rows = []
if config_file.exists():
for inst_config in config.get("instruments", []):
instrument_type = inst_config.get("instrument", "unknown")
serial = inst_config.get("serial", 0)
depth = inst_config.get("depth", 0)
# Look for the file
filename = f"{mooring_name}_{serial}{file_suffix}.nc"
filepath = proc_dir / instrument_type / filename
if filepath.exists():
ds = xr.open_dataset(filepath)
instrument_files.append(filepath)
instrument_datasets.append(ds)
# Time coverage
t0, t1 = ds.time.values[0], ds.time.values[-1]
npoints = len(ds.time)
# Median sampling interval
time_diff = np.diff(ds.time.values) / np.timedelta64(1, "m") # in minutes
median_interval = np.nanmedian(time_diff)
if median_interval > 1:
sampling = f"{median_interval:.1f} min"
else:
sampling = f"{median_interval * 60:.1f} sec"
# Collect a row for the table
rows.append(
{
"Instrument": instrument_type,
"Serial": serial,
"Depth [m]": depth,
"File": filepath.name,
"Start": str(t0)[:19],
"End": str(t1)[:19],
"Points": npoints,
"Sampling": sampling,
"Variables": ", ".join(list(ds.data_vars)),
}
)
else:
rows.append(
{
"Instrument": instrument_type,
"Serial": serial,
"Depth [m]": depth,
"File": "MISSING",
"Start": "",
"End": "",
"Points": 0,
"Sampling": "",
"Variables": "",
}
)
# Make a DataFrame summary
summary = pd.DataFrame(rows)
pd.set_option("display.max_colwidth", 80) # allow long var lists
print(summary.to_markdown(index=False))
print(f"\nFound {len(instrument_datasets)} instrument datasets")
Process with time gridding (no filtering)
[5]:
# Process without filtering
print("Processing mooring with time gridding only (no filtering)...")
print("=" * 60)
result = time_gridding_mooring(mooring_name, basedir, file_suffix="_use")
print(f"\nProcessing result: {'SUCCESS' if result else 'FAILED'}")
Processing mooring with time gridding only (no filtering)...
============================================================
ERROR: Processing directory not found: ../data/moor/proc/dsE_1_2018
Processing result: FAILED
[6]:
# Load and examine the combined dataset
output_file = proc_dir / f"{mooring_name}_mooring_use.nc"
if output_file.exists():
print(f"Output file exists: {output_file}")
# Load the combined dataset
combined_ds = xr.open_dataset(output_file)
else:
print("Output file not found - processing may have failed")
Output file not found - processing may have failed
Visualize Combined Dataset
Let’s plot the combined dataset to see how the different instruments look on the common time grid.
[7]:
import numpy as np
import matplotlib.pyplot as plt
def plot_combined_timeseries(
combined_ds,
variables=("temperature", "salinity", "pressure"),
cmap_name="viridis",
line_alpha=0.8,
line_width=1.2,
percentile_limits=(1, 99),
):
"""Plot selected variables from a combined mooring dataset as stacked time series.
Parameters
----------
combined_ds : xarray.Dataset
Must have dims: time, N_LEVELS. Optional coords: nominal_depth, serial_number.
variables : iterable[str]
Variable names to try to plot (if present in dataset).
cmap_name : str
Matplotlib colormap name for coloring by instrument level.
line_alpha : float
Line transparency.
line_width : float
Line width.
percentile_limits : (low, high)
Percentiles to use for automatic y-limits (e.g., (1, 99)).
"""
if combined_ds is None:
print("Combined dataset not available.")
return None, None
n_levels = combined_ds.sizes.get("N_LEVELS")
if n_levels is None:
raise ValueError("Dataset must contain dimension 'N_LEVELS'.")
available = [v for v in variables if v in combined_ds.data_vars]
if not available:
print("No requested variables found to plot.")
return None, None
# Colors by level
cmap = plt.get_cmap(cmap_name)
colors = cmap(np.linspace(0, 1, n_levels))
fig, axes = plt.subplots(
len(available),
1,
figsize=(14, 3.6 * len(available)),
sharex=True,
constrained_layout=True,
)
if len(available) == 1:
axes = [axes]
depth_arr = combined_ds.get("nominal_depth")
serial_arr = combined_ds.get("serial_number")
first_axis = True
for ax, var in zip(axes, available):
values_for_limits = []
for level in range(n_levels):
depth = None if depth_arr is None else depth_arr.values[level]
serial = None if serial_arr is None else serial_arr.values[level]
label = None
if first_axis:
if depth is not None and np.isfinite(depth):
label = (
f"Serial {serial} ({int(depth)} m)"
if serial is not None
else f"({int(depth)} m)"
)
elif serial is not None:
label = f"Serial {serial}"
da = combined_ds[var].isel(N_LEVELS=level)
da = da.where(np.isfinite(da), drop=True)
if da.size == 0:
continue
values_for_limits.append(da.values)
ax.plot(
da["time"].values,
da.values,
color=colors[level],
alpha=line_alpha,
linewidth=line_width,
label=label,
)
# Set labels and grid
ax.set_ylabel(var.replace("_", " ").title())
ax.grid(True, alpha=0.3)
ax.set_title(f"{var.replace('_', ' ').title()} — Combined Time Grid")
# Legend only once
if first_axis:
ax.legend(ncol=3, fontsize=8, loc="upper right", frameon=False)
first_axis = False
# Auto y-limits based on percentiles
if values_for_limits:
flat = np.concatenate(values_for_limits)
low, high = np.nanpercentile(flat, percentile_limits)
ax.set_ylim(low, high)
axes[-1].set_xlabel("Time")
return fig, axes
# Usage:
if "combined_ds" in locals():
plot_combined_timeseries(combined_ds)