From 1e9419b09b0404d891238e1d19d262e51740d6e2 Mon Sep 17 00:00:00 2001 From: Isabelle Schmitz Date: Thu, 7 May 2026 15:59:38 +0200 Subject: [PATCH 1/2] [FEAT] New NAC reader This commit features a new reader for North Atlantic current (NAC) transport estimates ba M. Lankhorst (2025). The new reader is set up with `nac.py`, which loads the data, `nac.yml` which holds the metadata and variable mapping and `test_nac.py` which tests the new reader. Also, other files were changed to account for the new reader: `__init__.py`, `read.py`, `readers.py`, `standardise.py`. In addition to that the `demo.ipynb` was extended to load and show both transport estimates: `TRANS_NAC` which is derived from satellite and float observations and `TRANS_FBC_PROXY` which is derived just from satellite altimetry. Changes were also made in `plotters.py`. Now data products which have a coarser resolution than 1 month can be plotted in a desired color as well, when `resample_monthly` is set to `False`. The change in `utilities.py` accounts for an error which was given, when a loaded dataset has a `TIME` coordinate stored as `datetime64`, but also carries `valid_min` and/or `valid_max` metadata that is numeric. Therefore the masking code from function `utilities.mask_invalid_values()` now skips datetime variables. --- amocatlas/data_sources/__init__.py | 2 + amocatlas/data_sources/nac.py | 188 +++++++++++++++++++++++++++++ amocatlas/metadata/nac.yml | 39 ++++++ amocatlas/plotters.py | 8 +- amocatlas/read.py | 7 ++ amocatlas/readers.py | 3 + amocatlas/standardise.py | 10 ++ amocatlas/utilities.py | 4 + data/_2_1.nc | Bin 0 -> 84774 bytes notebooks/demo.ipynb | 43 +++++++ tests/test_nac.py | 104 ++++++++++++++++ 11 files changed, 404 insertions(+), 4 deletions(-) create mode 100644 amocatlas/data_sources/nac.py create mode 100644 amocatlas/metadata/nac.yml create mode 100644 data/_2_1.nc create mode 100644 tests/test_nac.py diff --git a/amocatlas/data_sources/__init__.py b/amocatlas/data_sources/__init__.py index ad8eb018..a63d1f93 100644 --- a/amocatlas/data_sources/__init__.py +++ b/amocatlas/data_sources/__init__.py @@ -27,6 +27,7 @@ from .zheng2024 import read_zheng2024 from .wh41n import read_41n from .noac47n import read_47n +from .nac import read_nac __all__ = [ "read_rapid", @@ -43,4 +44,5 @@ "read_zheng2024", "read_41n", "read_47n", + "read_nac", ] diff --git a/amocatlas/data_sources/nac.py b/amocatlas/data_sources/nac.py new file mode 100644 index 00000000..159f42b7 --- /dev/null +++ b/amocatlas/data_sources/nac.py @@ -0,0 +1,188 @@ +"""North Atlantic Current (NAC) data reader for AMOCatlas. + +This module provides functions to read and process the North Atlantic current time series from satellite and float observations. +The NAC is a key component of the Atlantic Meridional Overturning Circulation, transporting warm, saline water from the tropics to the high northern latitudes. + +The dataset includes NAC transport estimates from satellite and float observations and an NAC estimation from satellite altimetry alone. + +Key functions: +- read_nac(): Main data loading interface for North Atlantic Current data + +Data source: Satellite and float observations +Location: Between tip of Greenland and northern Spain +""" + +from pathlib import Path +from typing import Union + +import xarray as xr + +# Import the modules used +from amocatlas import logger, utilities +from amocatlas.logger import log_error, log_info, log_warning +from amocatlas.utilities import apply_defaults +from amocatlas.reader_utils import ReaderUtils + +log = logger.log # Use the global logger + +# Datasource identifier for automatic standardization +DATASOURCE_ID = "nac" + +# Default list of NAC data files +NAC_DEFAULT_FILES = ["_2_1.nc"] +NAC_TRANSPORT_FILES = ["_2_1.nc"] +NAC_DEFAULT_SOURCE = "https://library.ucsd.edu/dc/object/bb6635909m" + +NAC_METADATA = { + "project": "North Atlantic Current Time Series from Satellite and Float Observations (1993-2025)", + "weblink": "https://library.ucsd.edu/dc/object/bb6635909m", + "comment": "Dataset accessed and processed via http://github.com/AMOCcommunity/amocatlas", +} + +NAC_FILE_METADATA = { + "bb6635909m_2_1.nc": { + "data_product": "6-monthly estimates of NAC transport from satellite altimetryand float observations", + }, +} + + +@apply_defaults(NAC_DEFAULT_SOURCE, NAC_DEFAULT_FILES) +def read_nac( + ## source: str, + 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 NAC (North Atlantic Current) transport datasets from a URL or local file path into xarray Datasets. + + Parameters + ---------- + source : str, optional + Local path to the data directory (remote source is handled per-file). + + file_list : str or list of str, optional + Filename or list of filenames to process. + Defaults to NAC_DEFAULT_FILES. + + transport_only : bool, optional + If True, restrict to transport 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 of loaded xarray datasets with basic inline and file-specific metadata. + + 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 NAC dataset") + + # Load YAML metadata with fallback + global_metadata, yaml_file_metadata = ReaderUtils.load_array_metadata_with_fallback( + DATASOURCE_ID, NAC_METADATA + ) + + # Ensure file_list has a default + if file_list is None: + file_list = NAC_DEFAULT_FILES + if transport_only: + file_list = NAC_TRANSPORT_FILES + if isinstance(file_list, str): + file_list = [file_list] + # Determine the local storage path + local_data_dir = Path(data_dir) if data_dir else utilities.get_default_data_dir() + local_data_dir.mkdir(parents=True, exist_ok=True) + + # Print information about files being loaded + ReaderUtils.print_loading_info(file_list, DATASOURCE_ID, NAC_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, + ) + + # Open dataset + + if file.lower().endswith(".nc"): + # Use ReaderUtils for consistent dataset loading + + ds = ReaderUtils.safe_load_dataset(file_path) + # Attach metadata + # Attach metadata with optional tracking + + if track_added_attrs: + + ds, attr_changes = ReaderUtils.attach_metadata_with_tracking( + ds, + file, + file_path, + global_metadata, + yaml_file_metadata, + NAC_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, + NAC_FILE_METADATA, + DATASOURCE_ID, + track_added_attrs=False, + ) + else: + raise ValueError( + f"Unsupported file type for {file}. Only .nc files are supported." + ) + + datasets.append(ds) + + if not datasets: + log_error("No valid NAC files in %s", file_list) + raise FileNotFoundError(f"No valid data files found in {file_list}") + + log_info("Successfully loaded %d NAC dataset(s)", len(datasets)) + + if track_added_attrs: + return datasets, added_attrs_per_dataset + else: + return datasets diff --git a/amocatlas/metadata/nac.yml b/amocatlas/metadata/nac.yml new file mode 100644 index 00000000..290af35b --- /dev/null +++ b/amocatlas/metadata/nac.yml @@ -0,0 +1,39 @@ +metadata: + program: "NAC" + description: "North Atlantic Current Time Series from Satellite and Float Observations (1993-2025)" + project: "Lankhorst, Matthias (2025). North Atlantic Current Time Series from Satellite and Float Observations (1993-2025). " + weblink: https://library.ucsd.edu/dc/object/bb6635909m + comment: Dataset accessed and processed via http://github.com/AMOCcommunity/amocatlas + acknowledgment: > + Earlier versions of this dataset were created with support from the European Commission through awards EVK2-CT-2000-00087 and EVR1-CT-2001-40014 (projects 'GYROSCOPE' and 'ANIMATE'). Updated versions were partially supported through award NA15OAR4320071 from U.S. NOAA OOMD. + citation: > + Lankhorst, Matthias (2025). North Atlantic Current Time Series from Satellite and Float Observations (1993-2025). In North Atlantic Current Time Series from Satellite and Float Observations. UC San Diego Library Digital Collections. https://doi.org/10.6075/J0D79CCG + license: + featureType: timeSeries + time_coverage_start: '1993-01-01' + time_coverage_end: '2025-07-02' + +files: + _2_1.nc: + source_url: https://library.ucsd.edu/dc/object/bb6635909m/ + data_product: "6-monthly mean NAC transport time series (1993-2025) estimated from satellite and float observations" + variable_mapping: + "NAC": TRANS_NAC + "NAC_UNCERTAINTY": TRANS_NAC_UNCERTAINTY + "NAC_PROXY": TRANS_NAC_PROXY + original_variable_metadata: + NAC: + long_name: "NAC Transport" + description: "North Atlantic Current transport time series from satellite and float observations" + units: Sverdrup + standard_name: ocean_volume_transport_across_line + NAC_UNCERTAINTY: + long_name: "Uncertainty of values in NAC variable" + description: "Uncertainty of North Atlantic Current transport time series" + units: Sverdrup + standard_name: ocean_volume_transport_across_line_uncertainty + NAC_PROXY: + long_name: "NAC Transport Proxy" + description: "Proxy for North Atlantic Current transport time series from satellite altimetry" + units: Sverdrup + standard_name: ocean_volume_transport_across_line \ No newline at end of file diff --git a/amocatlas/plotters.py b/amocatlas/plotters.py index 8d1f7d2f..ea623d00 100644 --- a/amocatlas/plotters.py +++ b/amocatlas/plotters.py @@ -585,14 +585,14 @@ def plot_amoc_timeseries( # Raw plot if plot_raw: - # Use black if no monthly resampling, grey otherwise - raw_color = "black" if not resample_monthly else "grey" + # Use grey if monthly resampling is enabled, otherwise the specified color + raw_color = "grey" if resample_monthly==True else color ax.plot( da[time_key], da, color=raw_color, - alpha=0.7 if not resample_monthly else 0.5, - linewidth=0.5, + alpha=0.85 if not resample_monthly else 0.5, + linewidth=1 if not resample_monthly else 0.5, label=f"{label} (raw)", ) diff --git a/amocatlas/read.py b/amocatlas/read.py index a8c174c0..bc31264f 100644 --- a/amocatlas/read.py +++ b/amocatlas/read.py @@ -50,6 +50,7 @@ read_47n, read_fbc, read_arcticgateway, + read_nac, ) # Import file constants for list_files() functionality @@ -66,6 +67,7 @@ from .data_sources.noac47n import NOAC47N_DEFAULT_FILES from .data_sources.fbc import FBC_DEFAULT_FILES from .data_sources.arcticgateway import ARCTIC_DEFAULT_FILES +from .data_sources.nac import NAC_DEFAULT_FILES # Import standardization functions from . import standardise @@ -85,6 +87,7 @@ "fbc", "calafat2025", "zheng2024", + "nac", } @@ -428,6 +431,9 @@ def list_files() -> List[str]: zheng2024 = _create_array_function( read_zheng2024, "Zheng et al. 2024", available_files=ZHENG2024_DEFAULT_FILES ) +nac = _create_array_function( + read_nac, "North Atlantic Current", available_files=NAC_DEFAULT_FILES +) # Define __all__ to control what's exported @@ -445,4 +451,5 @@ def list_files() -> List[str]: "fbc", "calafat2025", "zheng2024", + "nac" ] diff --git a/amocatlas/readers.py b/amocatlas/readers.py index e6c0c1b0..a81e6274 100644 --- a/amocatlas/readers.py +++ b/amocatlas/readers.py @@ -41,6 +41,7 @@ read_47n, read_fbc, read_arcticgateway, + read_nac, ) log = logger.log @@ -83,6 +84,7 @@ def _get_reader(array_name: str) -> Callable[..., List[xr.Dataset]]: "47n": read_47n, "fbc": read_fbc, "arcticgateway": read_arcticgateway, + "nac": read_nac, } try: return readers[array_name.lower()] @@ -175,6 +177,7 @@ def load_dataset( - '47n' : 47N array - 'fbc' : Faroe Bank Channel overflow array - 'arcticgateway' : ARCTIC Gateway array + - 'nac' : North Atlantic Current array source : str, optional URL or local path to the data source. If None, the reader-specific default source will be used. diff --git a/amocatlas/standardise.py b/amocatlas/standardise.py index 51b5a330..9671fb21 100644 --- a/amocatlas/standardise.py +++ b/amocatlas/standardise.py @@ -1167,6 +1167,16 @@ def standardise_arcticgateway(ds: xr.Dataset, file_name: str) -> xr.Dataset: return standardise_array(ds, file_name) +def standardise_nac(ds: xr.Dataset, file_name: str) -> xr.Dataset: + """Standardise NAC array dataset to consistent format.""" + warnings.warn( + "standardise_nac() is deprecated and will be removed in a future version. " + "Use standardise_data() instead.", + DeprecationWarning, + stacklevel=2, + ) + + return standardise_array(ds, file_name) def standardise_data(ds: xr.Dataset, file_name: str) -> xr.Dataset: """Standardise a dataset using YAML-based metadata. diff --git a/amocatlas/utilities.py b/amocatlas/utilities.py index 3da8178f..51826ab8 100644 --- a/amocatlas/utilities.py +++ b/amocatlas/utilities.py @@ -20,6 +20,7 @@ import pandas as pd import requests import xarray as xr +import numpy as np from amocatlas import logger from amocatlas.logger import log_debug @@ -879,6 +880,9 @@ def mask_invalid_values(ds: xr.Dataset) -> xr.Dataset: # Use xarray operations to preserve lazy evaluation invalid_mask = xr.zeros_like(var, dtype=bool) + if np.issubdtype(var.dtype, np.datetime64): + continue + if valid_min is not None: invalid_mask = invalid_mask | (var < valid_min) diff --git a/data/_2_1.nc b/data/_2_1.nc new file mode 100644 index 0000000000000000000000000000000000000000..4e8e01c2bb7390fe5b9f39f571a45964dd09dc1d GIT binary patch literal 84774 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
YsuaN@w9FOd`#6dJ~OqMj7H+U{e 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 0 + assert "_2_1.nc" in files + + transport_files = nac.NAC_TRANSPORT_FILES + assert len(transport_files) > 0 + assert "_2_1.nc" in transport_files + + # Files should be NetCDF format for NAC data + for file in files: + assert file.endswith(".nc") + + def test_function_exists_and_callable(self): + """Test that main function exists and is callable.""" + assert hasattr(nac, "read_nac") + assert callable(nac.read_nac) + + # Check function has documentation + func = nac.read_nac + assert func.__doc__ is not None + assert "North Atlantic Current" in func.__doc__ or "NAC" in func.__doc__ + + def test_module_docstring_informative(self): + """Test that module has informative docstring.""" + assert nac.__doc__ is not None + assert "North Atlantic Current" in nac.__doc__ + assert "NAC" in nac.__doc__ + + def test_module_imports_successfully(self): + """Test that the module imports without errors.""" + # This test passes if the module loaded successfully + assert nac is not None + # Check required dependencies are accessible + assert hasattr(nac, "xr") # xarray + + def test_nac_metadata_structure(self): + """Test that NAC metadata has expected structure.""" + metadata = nac.NAC_METADATA + + # Should have expected keys + assert "project" in metadata + assert "weblink" in metadata + assert "comment" in metadata + + # Values should be non-empty strings + assert isinstance(metadata["project"], str) + assert len(metadata["project"]) > 0 + assert isinstance(metadata["weblink"], str) + assert len(metadata["weblink"]) > 0 + + def test_nac_file_metadata_structure(self): + """Test that NAC file metadata has expected structure.""" + file_metadata = nac.NAC_FILE_METADATA + + # Should have metadata for _2_1.nc file + assert "bb6635909m_2_1.nc" in file_metadata + + # File metadata should have expected keys + file_meta = file_metadata["bb6635909m_2_1.nc"] + assert "data_product" in file_meta + assert isinstance(file_meta["data_product"], str) + assert len(file_meta["data_product"]) > 0 + + def test_default_source_url(self): + """Test that default source URL is properly configured.""" + source = nac.NAC_DEFAULT_SOURCE + assert isinstance(source, str) + assert len(source) > 0 + # Should be a valid URL-like string + assert "http" in source or "library" in source or "/" in source From 382de38d7bd77c6000d222083f10e5d6e7497d1c Mon Sep 17 00:00:00 2001 From: Isabelle Schmitz Date: Thu, 7 May 2026 17:30:03 +0200 Subject: [PATCH 2/2] Resolving comments Minor changes to `standardise.py`, `plotters.py` regarding formatting issues. Adjustments in `nac.py` to account for new data file name, which results from the url, when loading the dataset off of the website. Changed the `test_nac.py` accrodingly. Implemented an extension of the test in `test_utilities.py`, called `test_mask_invalid_values()`, to account for the changes commited earlier regarding datetime64 objects. --- amocatlas/data_sources/nac.py | 6 +++--- amocatlas/plotters.py | 2 +- amocatlas/standardise.py | 2 ++ tests/test_nac.py | 8 +++----- tests/test_utilities.py | 10 ++++++++++ 5 files changed, 19 insertions(+), 9 deletions(-) diff --git a/amocatlas/data_sources/nac.py b/amocatlas/data_sources/nac.py index 159f42b7..bd561bee 100644 --- a/amocatlas/data_sources/nac.py +++ b/amocatlas/data_sources/nac.py @@ -40,15 +40,14 @@ } NAC_FILE_METADATA = { - "bb6635909m_2_1.nc": { - "data_product": "6-monthly estimates of NAC transport from satellite altimetryand float observations", + "_2_1.nc": { + "data_product": "6-monthly estimates of NAC transport from satellite altimetry and float observations", }, } @apply_defaults(NAC_DEFAULT_SOURCE, NAC_DEFAULT_FILES) def read_nac( - ## source: str, source: Union[str, Path, None], file_list: Union[str, list[str]], transport_only: bool = True, @@ -82,6 +81,7 @@ def read_nac( ------- list of xr.Dataset List of loaded xarray datasets with basic inline and file-specific metadata. + And if track_added_attrs is True, also returns a list of dictionaries with the attributes that were added to each dataset during metadata enrichment. Raises ------ diff --git a/amocatlas/plotters.py b/amocatlas/plotters.py index ea623d00..f56e66e3 100644 --- a/amocatlas/plotters.py +++ b/amocatlas/plotters.py @@ -586,7 +586,7 @@ def plot_amoc_timeseries( # Raw plot if plot_raw: # Use grey if monthly resampling is enabled, otherwise the specified color - raw_color = "grey" if resample_monthly==True else color + raw_color = "grey" if resample_monthly else color ax.plot( da[time_key], da, diff --git a/amocatlas/standardise.py b/amocatlas/standardise.py index 9671fb21..943d603f 100644 --- a/amocatlas/standardise.py +++ b/amocatlas/standardise.py @@ -1167,6 +1167,7 @@ def standardise_arcticgateway(ds: xr.Dataset, file_name: str) -> xr.Dataset: return standardise_array(ds, file_name) + def standardise_nac(ds: xr.Dataset, file_name: str) -> xr.Dataset: """Standardise NAC array dataset to consistent format.""" warnings.warn( @@ -1178,6 +1179,7 @@ def standardise_nac(ds: xr.Dataset, file_name: str) -> xr.Dataset: return standardise_array(ds, file_name) + def standardise_data(ds: xr.Dataset, file_name: str) -> xr.Dataset: """Standardise a dataset using YAML-based metadata. diff --git a/tests/test_nac.py b/tests/test_nac.py index afc345c9..fe7cfecf 100644 --- a/tests/test_nac.py +++ b/tests/test_nac.py @@ -64,8 +64,6 @@ def test_module_imports_successfully(self): """Test that the module imports without errors.""" # This test passes if the module loaded successfully assert nac is not None - # Check required dependencies are accessible - assert hasattr(nac, "xr") # xarray def test_nac_metadata_structure(self): """Test that NAC metadata has expected structure.""" @@ -86,11 +84,11 @@ def test_nac_file_metadata_structure(self): """Test that NAC file metadata has expected structure.""" file_metadata = nac.NAC_FILE_METADATA - # Should have metadata for _2_1.nc file - assert "bb6635909m_2_1.nc" in file_metadata + # Should have metadata for the default _2_1.nc file + assert "_2_1.nc" in file_metadata # File metadata should have expected keys - file_meta = file_metadata["bb6635909m_2_1.nc"] + file_meta = file_metadata["_2_1.nc"] assert "data_product" in file_meta assert isinstance(file_meta["data_product"], str) assert len(file_meta["data_product"]) > 0 diff --git a/tests/test_utilities.py b/tests/test_utilities.py index 1959dbe1..ff2b4620 100644 --- a/tests/test_utilities.py +++ b/tests/test_utilities.py @@ -317,6 +317,11 @@ def test_mask_invalid_values(self): [35.0, -99.0, 36.0, 37.0, 100.0], # -99 and 100 are invalid {"valid_min": 30.0, "valid_max": 40.0}, ), + "datetime_marker": ( + ["time"], + pd.date_range("2020-01-01", periods=5), + {"valid_min": 0.0, "valid_max": 50.0}, + ), "no_limits": (["time"], [1, 2, 3, 4, 5]), # No valid_min/max }, coords={"time": pd.date_range("2020-01-01", periods=5)}, @@ -340,6 +345,11 @@ def test_mask_invalid_values(self): # Variable without limits should be unchanged assert result["no_limits"].equals(ds["no_limits"]) + # Datetime-valued data with numeric valid_min/valid_max should be left alone + assert result["datetime_marker"].equals(ds["datetime_marker"]) + assert result["datetime_marker"].attrs["valid_min"] == 0.0 + assert result["datetime_marker"].attrs["valid_max"] == 50.0 + # Attributes should be preserved assert result["temperature"].attrs["valid_min"] == 0.0 assert result["temperature"].attrs["valid_max"] == 50.0