"""SCOTIA overturning reader for AMOCatlas.
Reads the monthly overturning diagnostics of the Scotland-Canada overturning array
(SCOTIA; Fox et al. 2026), computed in neutral-density (gamma-n) space across the
subpolar North Atlantic. The upstream product has two files on a SAMS THREDDS server:
a small diagnostics time series (served here) and a large gridded T/S/velocity field
(~2 GB, not downloaded). Only the diagnostics file is read by default.
The diagnostics carry the overturning streamfunction ``psi`` and the transport-by-class
``transport`` on a neutral-density coordinate ``gamma_n_bin``, the streamfunction maximum
``moc`` and the density ``gamma_moc`` at which it occurs, plus northward heat (``hf``),
freshwater (``ff``) and density (``df``) fluxes. Raw variable names are kept so the
metadata ``variable_mapping`` can rename them downstream.
"""
from pathlib import Path
from typing import Union
import xarray as xr
from amocatlas import logger, utilities
from amocatlas.logger import log_error, log_info, log_warning, log_debug
from amocatlas.utilities import apply_defaults
from amocatlas.reader_utils import ReaderUtils
log = logger.log
DATASOURCE_ID = "scotia"
# Only the diagnostics file is served; the companion SCOTIA_gridded.nc is ~2 GB and is
# left on the THREDDS server (reachable via OPeNDAP subsetting) rather than downloaded.
SCOTIA_DEFAULT_FILES = ["SCOTIA_overturning_diagnostics.nc"]
SCOTIA_TRANSPORT_FILES = ["SCOTIA_overturning_diagnostics.nc"]
SCOTIA_DEFAULT_SOURCE = "https://thredds.sams.ac.uk/thredds/fileServer/Fox_et_al_2026/"
SCOTIA_METADATA = {
"project": "SCOTIA",
"weblink": "https://thredds.sams.ac.uk/thredds/catalog/Fox_et_al_2026/catalog.html",
"comment": "Dataset accessed and processed via http://github.com/AMOCcommunity/amocatlas",
}
SCOTIA_FILE_METADATA = {
"SCOTIA_overturning_diagnostics.nc": {
"data_product": "Monthly overturning diagnostics of the Scotland-Canada overturning array in neutral-density space",
}
}
# Bin-bound coordinates dropped in v1 (see _drop_gamma_bounds).
_GAMMA_BOUND_VARS = ("gamma_n_lower", "gamma_n_upper")
def _drop_gamma_bounds(ds: xr.Dataset, source_file: str = None) -> xr.Dataset:
"""Drop the neutral-density bin-bound arrays from the SCOTIA diagnostics.
The file carries ``gamma_n_lower``/``gamma_n_upper`` (the lower/upper edges of each
neutral-density bin) alongside the bin centres ``gamma_n_bin``. Rather than risk a
dangling CF ``bounds`` reference surviving the downstream rename, v1 serves only the
bin centres; the drop is logged, not silent, and the bounds can be reconstructed from
the source if needed.
Parameters
----------
ds : xr.Dataset
Raw SCOTIA diagnostics dataset.
source_file : str, optional
Source filename, for provenance in log messages.
Returns
-------
xr.Dataset
Dataset with the two bin-bound coordinates removed (if present).
"""
file_context = f" ({source_file})" if source_file else ""
present = [v for v in _GAMMA_BOUND_VARS if v in ds.variables]
if present:
ds = ds.drop_vars(present)
log_info(
"Dropped SCOTIA neutral-density bin-bound arrays %s%s; serving bin centres only",
present,
file_context,
)
else:
log_debug(f"No SCOTIA bin-bound arrays to drop{file_context}")
return ds
[docs]
@apply_defaults(SCOTIA_DEFAULT_SOURCE, SCOTIA_DEFAULT_FILES)
def read_scotia(
source: Union[str, Path, None],
file_list: Union[str, list[str]],
transport_only: bool = True,
data_dir: Union[str, Path, None] = None,
redownload: bool = False,
track_added_attrs: bool = False,
) -> list[xr.Dataset]:
"""Load the SCOTIA overturning diagnostics from a URL or local path into xarray Datasets.
Parameters
----------
source : str, optional
Local path or base URL to the data directory (remote source handled per-file).
file_list : str or list of str, optional
Filename or list of filenames to process. Defaults to SCOTIA_DEFAULT_FILES.
transport_only : bool, optional
If True, restrict to transport (diagnostics) files only.
data_dir : str, Path or None, optional
Optional local data directory.
redownload : bool, optional
If True, force redownload of the data.
track_added_attrs : bool, optional
If True, track which attributes were added during metadata enrichment.
Returns
-------
list of xr.Dataset
List with the single SCOTIA diagnostics dataset, metadata attached.
Raises
------
ValueError
If no source is provided for a file and no default URL mapping is found.
FileNotFoundError
If the file cannot be downloaded or does not exist locally.
"""
log.info("Starting to read SCOTIA dataset")
global_metadata, yaml_file_metadata = ReaderUtils.load_array_metadata_with_fallback(
DATASOURCE_ID, SCOTIA_METADATA
)
if file_list is None:
file_list = SCOTIA_DEFAULT_FILES
if transport_only:
file_list = SCOTIA_TRANSPORT_FILES
if isinstance(file_list, str):
file_list = [file_list]
local_data_dir = Path(data_dir) if data_dir else utilities.get_default_data_dir()
local_data_dir.mkdir(parents=True, exist_ok=True)
ReaderUtils.print_loading_info(file_list, DATASOURCE_ID, SCOTIA_FILE_METADATA)
datasets = []
added_attrs_per_dataset = [] if track_added_attrs else None
for file in file_list:
if not file.lower().endswith(".nc"):
log_warning("Skipping unsupported file type : %s", file)
continue
download_url = (
f"{source.rstrip('/')}/{file}" if utilities.is_valid_url(source) else None
)
file_path = utilities.resolve_file_path(
file_name=file,
source=source,
download_url=download_url,
local_data_dir=local_data_dir,
redownload=redownload,
)
ds = ReaderUtils.safe_load_dataset(file_path)
ds = _drop_gamma_bounds(ds, source_file=file)
if track_added_attrs:
ds, attr_changes = ReaderUtils.attach_metadata_with_tracking(
ds,
file,
file_path,
global_metadata,
yaml_file_metadata,
SCOTIA_FILE_METADATA,
DATASOURCE_ID,
track_added_attrs=True,
)
added_attrs_per_dataset.append(attr_changes)
else:
ds = ReaderUtils.attach_metadata_with_tracking(
ds,
file,
file_path,
global_metadata,
yaml_file_metadata,
SCOTIA_FILE_METADATA,
DATASOURCE_ID,
track_added_attrs=False,
)
datasets.append(ds)
if not datasets:
log_error("No valid SCOTIA files in %s", file_list)
raise FileNotFoundError(f"No valid data files found in {file_list}")
log_info("Successfully loaded %d SCOTIA dataset(s)", len(datasets))
if track_added_attrs:
return datasets, added_attrs_per_dataset
return datasets