roms-tools 3.3.0__py3-none-any.whl → 3.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- roms_tools/__init__.py +1 -1
- roms_tools/analysis/cdr_ensemble.py +10 -13
- roms_tools/analysis/roms_output.py +5 -304
- roms_tools/{download.py → datasets/download.py} +1 -0
- roms_tools/{setup → datasets}/lat_lon_datasets.py +76 -64
- roms_tools/{setup → datasets}/river_datasets.py +9 -4
- roms_tools/datasets/roms_dataset.py +767 -0
- roms_tools/datasets/utils.py +475 -0
- roms_tools/{setup/fill.py → fill.py} +110 -13
- roms_tools/plot.py +4 -4
- roms_tools/setup/boundary_forcing.py +51 -43
- roms_tools/setup/cdr_release.py +2 -4
- roms_tools/setup/grid.py +29 -12
- roms_tools/setup/initial_conditions.py +19 -19
- roms_tools/setup/nesting.py +8 -4
- roms_tools/setup/river_forcing.py +4 -4
- roms_tools/setup/surface_forcing.py +14 -9
- roms_tools/setup/tides.py +1 -1
- roms_tools/setup/topography.py +10 -2
- roms_tools/setup/utils.py +72 -524
- roms_tools/tests/test_analysis/test_cdr_ensemble.py +4 -6
- roms_tools/tests/test_analysis/test_roms_output.py +1 -220
- roms_tools/tests/{test_setup → test_datasets}/test_lat_lon_datasets.py +4 -4
- roms_tools/tests/{test_setup → test_datasets}/test_river_datasets.py +1 -1
- roms_tools/tests/test_datasets/test_roms_dataset.py +539 -0
- roms_tools/tests/test_datasets/test_utils.py +527 -0
- roms_tools/tests/{test_setup/test_fill.py → test_fill.py} +72 -9
- roms_tools/tests/test_setup/test_boundary_forcing.py +57 -138
- roms_tools/tests/test_setup/test_cdr_release.py +4 -5
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/zarr.json +293 -2021
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/zarr.json +294 -2022
- roms_tools/tests/test_setup/test_grid.py +42 -1
- roms_tools/tests/test_setup/test_initial_conditions.py +3 -94
- roms_tools/tests/test_setup/test_nesting.py +2 -1
- roms_tools/tests/test_setup/test_surface_forcing.py +1 -1
- roms_tools/tests/test_setup/test_tides.py +1 -1
- roms_tools/tests/test_setup/test_utils.py +100 -15
- roms_tools/tests/test_tiling/test_partition.py +63 -15
- roms_tools/tests/test_utils.py +78 -0
- roms_tools/tiling/partition.py +81 -211
- roms_tools/utils.py +193 -0
- {roms_tools-3.3.0.dist-info → roms_tools-3.4.0.dist-info}/METADATA +1 -1
- {roms_tools-3.3.0.dist-info → roms_tools-3.4.0.dist-info}/RECORD +46 -170
- {roms_tools-3.3.0.dist-info → roms_tools-3.4.0.dist-info}/WHEEL +1 -1
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/ALK_ALT_CO2_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/ALK_ALT_CO2_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/ALK_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/ALK_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DIC_ALT_CO2_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DIC_ALT_CO2_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DIC_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DIC_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DOC_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DOC_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DOCr_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DOCr_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DON_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DON_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DONr_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DONr_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DOP_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DOP_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DOPr_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DOPr_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/Fe_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/Fe_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/Lig_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/Lig_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/NH4_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/NH4_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/NO3_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/NO3_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/O2_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/O2_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/PO4_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/PO4_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/SiO3_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/SiO3_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatC_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatC_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatChl_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatChl_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatFe_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatFe_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatP_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatP_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatSi_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatSi_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazC_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazC_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazChl_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazChl_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazFe_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazFe_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazP_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazP_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spC_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spC_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spCaCO3_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spCaCO3_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spChl_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spChl_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spFe_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spFe_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spP_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spP_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/zooC_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/zooC_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/ALK_ALT_CO2_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/ALK_ALT_CO2_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/ALK_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/ALK_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/DIC_ALT_CO2_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/DIC_ALT_CO2_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/DIC_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/DIC_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/DOC_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/DOC_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/DOCr_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/DOCr_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/DON_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/DON_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/DONr_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/DONr_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/DOP_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/DOP_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/DOPr_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/DOPr_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/Fe_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/Fe_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/Lig_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/Lig_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/NH4_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/NH4_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/NO3_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/NO3_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/O2_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/O2_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/PO4_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/PO4_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/SiO3_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/SiO3_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/diatC_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/diatC_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/diatChl_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/diatChl_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/diatFe_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/diatFe_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/diatP_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/diatP_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/diatSi_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/diatSi_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/diazC_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/diazC_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/diazChl_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/diazChl_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/diazFe_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/diazFe_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/diazP_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/diazP_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/spC_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/spC_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/spCaCO3_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/spCaCO3_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/spChl_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/spChl_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/spFe_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/spFe_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/spP_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/spP_west/zarr.json +0 -54
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/zooC_west/c/0/0/0 +0 -0
- roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/zooC_west/zarr.json +0 -54
- {roms_tools-3.3.0.dist-info → roms_tools-3.4.0.dist-info}/licenses/LICENSE +0 -0
- {roms_tools-3.3.0.dist-info → roms_tools-3.4.0.dist-info}/top_level.txt +0 -0
roms_tools/setup/utils.py
CHANGED
|
@@ -4,12 +4,11 @@ import time
|
|
|
4
4
|
import typing
|
|
5
5
|
from collections.abc import Sequence
|
|
6
6
|
from dataclasses import asdict, fields, is_dataclass
|
|
7
|
-
from datetime import datetime
|
|
7
|
+
from datetime import datetime
|
|
8
8
|
from enum import StrEnum
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from typing import Any, Literal, TypeAlias
|
|
11
11
|
|
|
12
|
-
import cftime
|
|
13
12
|
import numba as nb
|
|
14
13
|
import numpy as np
|
|
15
14
|
import pandas as pd
|
|
@@ -128,32 +127,6 @@ def substitute_nans_by_fillvalue(field, fill_value=0.0) -> xr.DataArray:
|
|
|
128
127
|
return field.fillna(fill_value)
|
|
129
128
|
|
|
130
129
|
|
|
131
|
-
def one_dim_fill(da: xr.DataArray, dim: str, direction="forward") -> xr.DataArray:
|
|
132
|
-
"""Fill NaN values in a DataArray along a specified dimension.
|
|
133
|
-
|
|
134
|
-
Parameters
|
|
135
|
-
----------
|
|
136
|
-
da : xr.DataArray
|
|
137
|
-
The input DataArray with NaN values to be filled, which must include the specified dimension.
|
|
138
|
-
dim : str
|
|
139
|
-
The name of the dimension along which to fill NaN values (e.g., 'depth' or 'time').
|
|
140
|
-
direction : str, optional
|
|
141
|
-
The filling direction; either "forward" to propagate non-NaN values downward or "backward" to propagate them upward.
|
|
142
|
-
Defaults to "forward".
|
|
143
|
-
|
|
144
|
-
Returns
|
|
145
|
-
-------
|
|
146
|
-
xr.DataArray
|
|
147
|
-
A new DataArray with NaN values filled in the specified direction, leaving the original data unchanged.
|
|
148
|
-
"""
|
|
149
|
-
if dim in da.dims:
|
|
150
|
-
if direction == "forward":
|
|
151
|
-
return da.ffill(dim=dim)
|
|
152
|
-
elif direction == "backward":
|
|
153
|
-
return da.bfill(dim=dim)
|
|
154
|
-
return da
|
|
155
|
-
|
|
156
|
-
|
|
157
130
|
def assign_dates_to_climatology(ds: xr.Dataset, time_dim: str) -> xr.Dataset:
|
|
158
131
|
"""Assigns climatology dates to the dataset's time dimension.
|
|
159
132
|
|
|
@@ -182,240 +155,6 @@ def assign_dates_to_climatology(ds: xr.Dataset, time_dim: str) -> xr.Dataset:
|
|
|
182
155
|
return ds
|
|
183
156
|
|
|
184
157
|
|
|
185
|
-
def interpolate_cyclic_time(
|
|
186
|
-
data_array: xr.DataArray,
|
|
187
|
-
time_dim_name: str,
|
|
188
|
-
day_of_year: int | float | np.ndarray | xr.DataArray | Sequence[int | float],
|
|
189
|
-
) -> xr.DataArray:
|
|
190
|
-
"""Interpolates a DataArray cyclically across the start and end of the year.
|
|
191
|
-
|
|
192
|
-
This function extends the data cyclically by appending the last time step
|
|
193
|
-
(shifted back by one year) at the beginning and the first time step
|
|
194
|
-
(shifted forward by one year) at the end. It then performs linear interpolation
|
|
195
|
-
to match the specified `day_of_year` values.
|
|
196
|
-
|
|
197
|
-
Parameters
|
|
198
|
-
----------
|
|
199
|
-
data_array : xr.DataArray
|
|
200
|
-
The input data array containing a time-like dimension.
|
|
201
|
-
time_dim_name : str
|
|
202
|
-
The name of the time dimension in the dataset.
|
|
203
|
-
day_of_year : Union[int, float, np.ndarray, xr.DataArray, Sequence[Union[int, float]]]
|
|
204
|
-
The target day(s) of the year for interpolation. This can be:
|
|
205
|
-
- A single integer or float representing the day of the year.
|
|
206
|
-
- A NumPy array or xarray DataArray containing multiple days.
|
|
207
|
-
- A list or tuple of integers or floats for multiple target days.
|
|
208
|
-
|
|
209
|
-
Returns
|
|
210
|
-
-------
|
|
211
|
-
xr.DataArray
|
|
212
|
-
The interpolated DataArray, ensuring cyclic continuity across year boundaries.
|
|
213
|
-
|
|
214
|
-
Notes
|
|
215
|
-
-----
|
|
216
|
-
- This function is useful for interpolating climatological data, where the time axis
|
|
217
|
-
represents a repeating annual cycle.
|
|
218
|
-
- The `day_of_year` values should be within the range [1, 365] or [1, 366] for leap years.
|
|
219
|
-
"""
|
|
220
|
-
# Concatenate across the beginning and end of the year
|
|
221
|
-
time_concat = xr.concat(
|
|
222
|
-
[
|
|
223
|
-
data_array[time_dim_name][-1] - 365.25, # Shift last time backward
|
|
224
|
-
data_array[time_dim_name],
|
|
225
|
-
data_array[time_dim_name][0] + 365.25, # Shift first time forward
|
|
226
|
-
],
|
|
227
|
-
dim=time_dim_name,
|
|
228
|
-
)
|
|
229
|
-
|
|
230
|
-
data_array_concat = xr.concat(
|
|
231
|
-
[
|
|
232
|
-
data_array.isel(
|
|
233
|
-
**{time_dim_name: -1}
|
|
234
|
-
), # Append last value at the beginning
|
|
235
|
-
data_array,
|
|
236
|
-
data_array.isel(**{time_dim_name: 0}), # Append first value at the end
|
|
237
|
-
],
|
|
238
|
-
dim=time_dim_name,
|
|
239
|
-
)
|
|
240
|
-
data_array_concat[time_dim_name] = time_concat
|
|
241
|
-
|
|
242
|
-
# Interpolate to specified times
|
|
243
|
-
data_array_interpolated = data_array_concat.interp(
|
|
244
|
-
**{time_dim_name: day_of_year}, method="linear"
|
|
245
|
-
)
|
|
246
|
-
|
|
247
|
-
return data_array_interpolated
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
def interpolate_from_climatology(
|
|
251
|
-
field: xr.DataArray | xr.Dataset,
|
|
252
|
-
time_dim_name: str,
|
|
253
|
-
time: xr.DataArray | pd.DatetimeIndex,
|
|
254
|
-
) -> xr.DataArray | xr.Dataset:
|
|
255
|
-
"""Interpolates a climatological field to specified time points.
|
|
256
|
-
|
|
257
|
-
This function interpolates the input `field` based on `day_of_year` values
|
|
258
|
-
extracted from the provided `time` points. If `field` is an `xarray.Dataset`,
|
|
259
|
-
interpolation is applied to all its data variables individually.
|
|
260
|
-
|
|
261
|
-
Parameters
|
|
262
|
-
----------
|
|
263
|
-
field : xarray.DataArray or xarray.Dataset
|
|
264
|
-
The input field to be interpolated.
|
|
265
|
-
- If `field` is an `xarray.DataArray`, it must have a time dimension identified by `time_dim_name`.
|
|
266
|
-
- If `field` is an `xarray.Dataset`, all variables within the dataset are interpolated along `time_dim_name`.
|
|
267
|
-
The time dimension is assumed to represent `day_of_year` for climatological purposes.
|
|
268
|
-
time_dim_name : str
|
|
269
|
-
The name of the time dimension in `field`. This dimension is used for interpolation.
|
|
270
|
-
time : xarray.DataArray or pandas.DatetimeIndex
|
|
271
|
-
The target time points for interpolation. These are internally converted to `day_of_year`
|
|
272
|
-
before performing interpolation.
|
|
273
|
-
|
|
274
|
-
Returns
|
|
275
|
-
-------
|
|
276
|
-
xarray.DataArray or xarray.Dataset
|
|
277
|
-
The interpolated field, maintaining the same type (`xarray.DataArray` or `xarray.Dataset`)
|
|
278
|
-
but aligned to the specified `time` values.
|
|
279
|
-
|
|
280
|
-
Notes
|
|
281
|
-
-----
|
|
282
|
-
- This function assumes that `field` represents a climatological dataset, where time is expressed as `day_of_year` (1-365).
|
|
283
|
-
- The `time` input is automatically converted to `day_of_year`, so manual conversion is not required before calling this function.
|
|
284
|
-
"""
|
|
285
|
-
|
|
286
|
-
def interpolate_single_field(data_array: xr.DataArray) -> xr.DataArray:
|
|
287
|
-
if isinstance(time, xr.DataArray):
|
|
288
|
-
# Extract day of year from xarray.DataArray
|
|
289
|
-
day_of_year = time.dt.dayofyear
|
|
290
|
-
else:
|
|
291
|
-
if np.size(time) == 1:
|
|
292
|
-
# Convert single datetime64 object to pandas.Timestamp
|
|
293
|
-
date = pd.Timestamp(time)
|
|
294
|
-
day_of_year = (
|
|
295
|
-
date.dayofyear
|
|
296
|
-
+ (date.hour / 24)
|
|
297
|
-
+ (date.minute / 1440)
|
|
298
|
-
+ (date.second / 86400)
|
|
299
|
-
)
|
|
300
|
-
else:
|
|
301
|
-
# Convert each datetime64 object in the array to pandas.Timestamp and compute fractional day of year
|
|
302
|
-
day_of_year = np.array(
|
|
303
|
-
[
|
|
304
|
-
pd.Timestamp(t).dayofyear
|
|
305
|
-
+ (pd.Timestamp(t).hour / 24)
|
|
306
|
-
+ (pd.Timestamp(t).minute / 1440)
|
|
307
|
-
+ (pd.Timestamp(t).second / 86400)
|
|
308
|
-
for t in time
|
|
309
|
-
]
|
|
310
|
-
)
|
|
311
|
-
|
|
312
|
-
data_array_interpolated = interpolate_cyclic_time(
|
|
313
|
-
data_array, time_dim_name, day_of_year
|
|
314
|
-
)
|
|
315
|
-
|
|
316
|
-
if np.size(time) == 1:
|
|
317
|
-
data_array_interpolated = data_array_interpolated.expand_dims(
|
|
318
|
-
{time_dim_name: 1}
|
|
319
|
-
)
|
|
320
|
-
return data_array_interpolated
|
|
321
|
-
|
|
322
|
-
if isinstance(field, xr.DataArray):
|
|
323
|
-
return interpolate_single_field(field)
|
|
324
|
-
elif isinstance(field, xr.Dataset):
|
|
325
|
-
interpolated_data_vars = {
|
|
326
|
-
var: interpolate_single_field(data_array)
|
|
327
|
-
for var, data_array in field.data_vars.items()
|
|
328
|
-
}
|
|
329
|
-
return xr.Dataset(interpolated_data_vars, attrs=field.attrs)
|
|
330
|
-
|
|
331
|
-
else:
|
|
332
|
-
raise TypeError("Input 'field' must be an xarray.DataArray or xarray.Dataset.")
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
def get_time_type(data_array: xr.DataArray) -> str:
|
|
336
|
-
"""Determines the type of time values in the xarray DataArray.
|
|
337
|
-
|
|
338
|
-
Parameters
|
|
339
|
-
----------
|
|
340
|
-
data_array : xr.DataArray
|
|
341
|
-
The xarray DataArray to be checked for time data types.
|
|
342
|
-
|
|
343
|
-
Returns
|
|
344
|
-
-------
|
|
345
|
-
str
|
|
346
|
-
A string indicating the type of the time data: 'cftime', 'datetime', or 'int'.
|
|
347
|
-
|
|
348
|
-
Raises
|
|
349
|
-
------
|
|
350
|
-
TypeError
|
|
351
|
-
If the values in the DataArray are not of type numpy.ndarray or list.
|
|
352
|
-
"""
|
|
353
|
-
# List of cftime datetime types
|
|
354
|
-
cftime_types = (
|
|
355
|
-
cftime.DatetimeNoLeap,
|
|
356
|
-
cftime.DatetimeJulian,
|
|
357
|
-
cftime.DatetimeGregorian,
|
|
358
|
-
cftime.Datetime360Day,
|
|
359
|
-
cftime.DatetimeProlepticGregorian,
|
|
360
|
-
)
|
|
361
|
-
|
|
362
|
-
# Check if any of the coordinate values are of cftime, datetime, or integer type
|
|
363
|
-
if isinstance(data_array.values, np.ndarray | list):
|
|
364
|
-
# Check if the data type is numpy datetime64, indicating standard datetime objects
|
|
365
|
-
if data_array.values.dtype == "datetime64[ns]":
|
|
366
|
-
return "datetime"
|
|
367
|
-
|
|
368
|
-
# Check if any values in the array are instances of cftime types
|
|
369
|
-
if any(isinstance(value, cftime_types) for value in data_array.values):
|
|
370
|
-
return "cftime"
|
|
371
|
-
|
|
372
|
-
# Check if all values are of integer type (e.g., for indices or time steps)
|
|
373
|
-
if np.issubdtype(data_array.values.dtype, np.integer):
|
|
374
|
-
return "int"
|
|
375
|
-
|
|
376
|
-
# If none of the above conditions are met, raise a ValueError
|
|
377
|
-
raise ValueError("Unsupported data type for time values in input dataset.")
|
|
378
|
-
|
|
379
|
-
# Handle unexpected types
|
|
380
|
-
raise TypeError("DataArray values must be of type numpy.ndarray or list.")
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
def convert_cftime_to_datetime(data_array: np.ndarray) -> np.ndarray:
|
|
384
|
-
"""Converts cftime datetime objects to numpy datetime64 objects in a numpy ndarray.
|
|
385
|
-
|
|
386
|
-
Parameters
|
|
387
|
-
----------
|
|
388
|
-
data_array : np.ndarray
|
|
389
|
-
The numpy ndarray containing cftime datetime objects to be converted.
|
|
390
|
-
|
|
391
|
-
Returns
|
|
392
|
-
-------
|
|
393
|
-
np.ndarray
|
|
394
|
-
The ndarray with cftime datetimes converted to numpy datetime64 objects.
|
|
395
|
-
|
|
396
|
-
Notes
|
|
397
|
-
-----
|
|
398
|
-
This function is intended to be used with numpy ndarrays. If you need to convert
|
|
399
|
-
cftime datetime objects in an xarray.DataArray, please use the appropriate function
|
|
400
|
-
to handle xarray.DataArray conversions.
|
|
401
|
-
"""
|
|
402
|
-
# List of cftime datetime types
|
|
403
|
-
cftime_types = (
|
|
404
|
-
cftime.DatetimeNoLeap,
|
|
405
|
-
cftime.DatetimeJulian,
|
|
406
|
-
cftime.DatetimeGregorian,
|
|
407
|
-
)
|
|
408
|
-
|
|
409
|
-
# Define a conversion function for cftime to numpy datetime64
|
|
410
|
-
def convert_datetime(dt):
|
|
411
|
-
if isinstance(dt, cftime_types):
|
|
412
|
-
# Convert to ISO format and then to nanosecond precision
|
|
413
|
-
return np.datetime64(dt.isoformat(), "ns")
|
|
414
|
-
return np.datetime64(dt, "ns")
|
|
415
|
-
|
|
416
|
-
return np.vectorize(convert_datetime)(data_array)
|
|
417
|
-
|
|
418
|
-
|
|
419
158
|
def get_variable_metadata():
|
|
420
159
|
"""Retrieves metadata for commonly used variables in the dataset.
|
|
421
160
|
|
|
@@ -1861,38 +1600,6 @@ def get_boundary_coords():
|
|
|
1861
1600
|
return bdry_coords
|
|
1862
1601
|
|
|
1863
1602
|
|
|
1864
|
-
def wrap_longitudes(grid_ds, straddle):
|
|
1865
|
-
"""Adjusts longitude values in a dataset to handle dateline crossing.
|
|
1866
|
-
|
|
1867
|
-
Parameters
|
|
1868
|
-
----------
|
|
1869
|
-
grid_ds : xr.Dataset
|
|
1870
|
-
The dataset containing longitude variables to adjust.
|
|
1871
|
-
straddle : bool
|
|
1872
|
-
If True, adjusts longitudes to the range [-180, 180] for datasets
|
|
1873
|
-
that straddle the dateline. If False, adjusts longitudes to the
|
|
1874
|
-
range [0, 360].
|
|
1875
|
-
|
|
1876
|
-
Returns
|
|
1877
|
-
-------
|
|
1878
|
-
xr.Dataset
|
|
1879
|
-
The dataset with adjusted longitude values.
|
|
1880
|
-
"""
|
|
1881
|
-
for lon_dim in ["lon_rho", "lon_u", "lon_v"]:
|
|
1882
|
-
if straddle:
|
|
1883
|
-
grid_ds[lon_dim] = xr.where(
|
|
1884
|
-
grid_ds[lon_dim] > 180,
|
|
1885
|
-
grid_ds[lon_dim] - 360,
|
|
1886
|
-
grid_ds[lon_dim],
|
|
1887
|
-
)
|
|
1888
|
-
else:
|
|
1889
|
-
grid_ds[lon_dim] = xr.where(
|
|
1890
|
-
grid_ds[lon_dim] < 0, grid_ds[lon_dim] + 360, grid_ds[lon_dim]
|
|
1891
|
-
)
|
|
1892
|
-
|
|
1893
|
-
return grid_ds
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
1603
|
def to_float(val):
|
|
1897
1604
|
"""Convert a value or list of values to float.
|
|
1898
1605
|
|
|
@@ -1967,260 +1674,101 @@ def validate_names(
|
|
|
1967
1674
|
return names
|
|
1968
1675
|
|
|
1969
1676
|
|
|
1970
|
-
def
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
"""Check if the dataset contains the specified variables and dimensions.
|
|
1677
|
+
def check_and_set_boundaries(
|
|
1678
|
+
boundaries: dict[str, bool] | None,
|
|
1679
|
+
mask: xr.DataArray,
|
|
1680
|
+
) -> dict[str, bool]:
|
|
1681
|
+
"""
|
|
1682
|
+
Validate and finalize the `boundaries` dictionary.
|
|
1977
1683
|
|
|
1978
1684
|
Parameters
|
|
1979
1685
|
----------
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
Dictionary of variable names that are required in the dataset.
|
|
1986
|
-
opt_var_names : Optional[Dict[str, str]], optional
|
|
1987
|
-
Dictionary of optional variable names.
|
|
1988
|
-
These variables are not strictly required, and the function will not raise an error if they are missing.
|
|
1989
|
-
Default is None, meaning no optional variables are considered.
|
|
1686
|
+
boundaries : dict[str, bool] or None
|
|
1687
|
+
User-supplied dictionary controlling which boundaries are active.
|
|
1688
|
+
Keys may include any subset of {"south", "east", "north", "west"}.
|
|
1689
|
+
Missing keys will be filled from mask-based defaults.
|
|
1690
|
+
If None, all boundaries are inferred from the land mask.
|
|
1990
1691
|
|
|
1692
|
+
mask : xr.DataArray
|
|
1693
|
+
2D land/sea mask on rho-points. Used to determine which boundaries
|
|
1694
|
+
contain at least one ocean point.
|
|
1991
1695
|
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1696
|
+
Returns
|
|
1697
|
+
-------
|
|
1698
|
+
dict[str, bool]
|
|
1699
|
+
Completed and validated boundary configuration.
|
|
1996
1700
|
"""
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
1701
|
+
valid_keys = {"south", "east", "north", "west"}
|
|
1702
|
+
|
|
1703
|
+
# --------------------------------------------
|
|
1704
|
+
# Case 1: boundaries not provided → infer them
|
|
1705
|
+
# --------------------------------------------
|
|
1706
|
+
if boundaries is None:
|
|
1707
|
+
inferred = _infer_valid_boundaries_from_mask(mask)
|
|
1708
|
+
logging.info(f"No `boundaries` provided. Using mask-based defaults: {inferred}")
|
|
1709
|
+
return inferred
|
|
1710
|
+
|
|
1711
|
+
# --------------------------------------------
|
|
1712
|
+
# Case 2: boundaries provided → validate
|
|
1713
|
+
# --------------------------------------------
|
|
1714
|
+
if not isinstance(boundaries, dict):
|
|
1715
|
+
raise TypeError(
|
|
1716
|
+
"`boundaries` must be a dict mapping boundary names to booleans."
|
|
2001
1717
|
)
|
|
2002
1718
|
|
|
2003
|
-
|
|
2004
|
-
|
|
1719
|
+
# Unknown keys?
|
|
1720
|
+
unknown_keys = set(boundaries) - valid_keys
|
|
1721
|
+
if unknown_keys:
|
|
2005
1722
|
raise ValueError(
|
|
2006
|
-
f"
|
|
1723
|
+
f"`boundaries` contains invalid keys: {unknown_keys}. "
|
|
1724
|
+
"Allowed keys are: 'south', 'east', 'north', 'west'."
|
|
2007
1725
|
)
|
|
2008
1726
|
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
1727
|
+
# Type-check provided values
|
|
1728
|
+
for key, val in boundaries.items():
|
|
1729
|
+
if not isinstance(val, bool):
|
|
1730
|
+
raise TypeError(f"Boundary '{key}' must be a boolean.")
|
|
1731
|
+
|
|
1732
|
+
# Fill missing boundaries using defaults
|
|
1733
|
+
inferred_defaults = _infer_valid_boundaries_from_mask(mask)
|
|
1734
|
+
completed = boundaries.copy()
|
|
1735
|
+
|
|
1736
|
+
for key in valid_keys:
|
|
1737
|
+
if key not in completed:
|
|
1738
|
+
completed[key] = inferred_defaults[key]
|
|
1739
|
+
logging.info(
|
|
1740
|
+
f"`boundaries[{key!r}]` not provided — defaulting to "
|
|
1741
|
+
f"{inferred_defaults[key]}"
|
|
2016
1742
|
)
|
|
2017
1743
|
|
|
1744
|
+
logging.info(f"Using boundary configuration: {completed}")
|
|
1745
|
+
return completed
|
|
2018
1746
|
|
|
2019
|
-
def select_relevant_times(
|
|
2020
|
-
ds: xr.Dataset,
|
|
2021
|
-
time_dim: str,
|
|
2022
|
-
start_time: datetime,
|
|
2023
|
-
end_time: datetime | None = None,
|
|
2024
|
-
climatology: bool = False,
|
|
2025
|
-
allow_flex_time: bool = False,
|
|
2026
|
-
) -> xr.Dataset:
|
|
2027
|
-
"""
|
|
2028
|
-
Select a subset of the dataset based on time constraints.
|
|
2029
|
-
|
|
2030
|
-
This function supports two main use cases:
|
|
2031
|
-
|
|
2032
|
-
1. **Time range selection (start_time + end_time provided):**
|
|
2033
|
-
- Returns all records strictly between `start_time` and `end_time`.
|
|
2034
|
-
- Ensures at least one record at or before `start_time` and one record at or
|
|
2035
|
-
after `end_time` are included, even if they fall outside the strict range.
|
|
2036
|
-
|
|
2037
|
-
2. **Initial condition selection (start_time provided, end_time=None):**
|
|
2038
|
-
- Delegates to `_select_initial_time`, which reduces the dataset to exactly one
|
|
2039
|
-
time entry.
|
|
2040
|
-
- If `allow_flex_time=True`, a +24-hour buffer around `start_time` is allowed,
|
|
2041
|
-
and the closest timestamp is chosen.
|
|
2042
|
-
- If `allow_flex_time=False`, requires an exact timestamp match.
|
|
2043
|
-
|
|
2044
|
-
Additional behavior:
|
|
2045
|
-
- If `climatology=True`, the dataset must contain exactly 12 time steps. If valid,
|
|
2046
|
-
the climatology dataset is returned without further filtering.
|
|
2047
|
-
- If the dataset uses `cftime` datetime objects, these are converted to
|
|
2048
|
-
`np.datetime64` before filtering.
|
|
2049
|
-
|
|
2050
|
-
Parameters
|
|
2051
|
-
----------
|
|
2052
|
-
ds : xr.Dataset
|
|
2053
|
-
The dataset to filter. Must contain a valid time dimension.
|
|
2054
|
-
time_dim : str
|
|
2055
|
-
Name of the time dimension in `ds`.
|
|
2056
|
-
start_time : datetime
|
|
2057
|
-
Start time for filtering.
|
|
2058
|
-
end_time : datetime or None
|
|
2059
|
-
End time for filtering. If `None`, the function assumes an initial condition
|
|
2060
|
-
use case and selects exactly one timestamp.
|
|
2061
|
-
climatology : bool, optional
|
|
2062
|
-
If True, requires exactly 12 time steps and bypasses normal filtering.
|
|
2063
|
-
Defaults to False.
|
|
2064
|
-
allow_flex_time : bool, optional
|
|
2065
|
-
Whether to allow a +24h search window after `start_time` when `end_time`
|
|
2066
|
-
is None. If False (default), requires an exact match.
|
|
2067
1747
|
|
|
2068
|
-
|
|
2069
|
-
-------
|
|
2070
|
-
xr.Dataset
|
|
2071
|
-
A filtered dataset containing only the selected time entries.
|
|
2072
|
-
|
|
2073
|
-
Raises
|
|
2074
|
-
------
|
|
2075
|
-
ValueError
|
|
2076
|
-
- If `climatology=True` but the dataset does not contain exactly 12 time steps.
|
|
2077
|
-
- If `climatology=False` and the dataset contains integer time values.
|
|
2078
|
-
- If no valid records are found within the requested range or window.
|
|
2079
|
-
|
|
2080
|
-
Warns
|
|
2081
|
-
-----
|
|
2082
|
-
UserWarning
|
|
2083
|
-
- If no records exist at or before `start_time` or at or after `end_time`.
|
|
2084
|
-
- If the specified time dimension does not exist in the dataset.
|
|
2085
|
-
|
|
2086
|
-
Notes
|
|
2087
|
-
-----
|
|
2088
|
-
- For initial conditions (end_time=None), see `_select_initial_time` for details
|
|
2089
|
-
on strict vs. flexible selection behavior.
|
|
2090
|
-
- Logs warnings instead of failing hard when boundary records are missing, and
|
|
2091
|
-
defaults to using the earliest or latest available time in such cases.
|
|
1748
|
+
def _infer_valid_boundaries_from_mask(mask: xr.DataArray) -> dict[str, bool]:
|
|
2092
1749
|
"""
|
|
2093
|
-
|
|
2094
|
-
logging.warning(
|
|
2095
|
-
f"Dataset does not contain time dimension '{time_dim}'. "
|
|
2096
|
-
"Please check variable naming or dataset structure."
|
|
2097
|
-
)
|
|
2098
|
-
return ds
|
|
2099
|
-
|
|
2100
|
-
time_type = get_time_type(ds[time_dim])
|
|
2101
|
-
|
|
2102
|
-
if climatology:
|
|
2103
|
-
if len(ds[time_dim]) != 12:
|
|
2104
|
-
raise ValueError(
|
|
2105
|
-
f"The dataset contains {len(ds[time_dim])} time steps, but the climatology flag is set to True, which requires exactly 12 time steps."
|
|
2106
|
-
)
|
|
2107
|
-
else:
|
|
2108
|
-
if time_type == "int":
|
|
2109
|
-
raise ValueError(
|
|
2110
|
-
"The dataset contains integer time values, which are only supported when the climatology flag is set to True. However, your climatology flag is set to False."
|
|
2111
|
-
)
|
|
2112
|
-
if time_type == "cftime":
|
|
2113
|
-
ds = ds.assign_coords({time_dim: convert_cftime_to_datetime(ds[time_dim])})
|
|
2114
|
-
|
|
2115
|
-
if not end_time:
|
|
2116
|
-
# Assume we are looking for exactly one time record for initial conditions
|
|
2117
|
-
return _select_initial_time(
|
|
2118
|
-
ds, time_dim, start_time, climatology, allow_flex_time
|
|
2119
|
-
)
|
|
2120
|
-
|
|
2121
|
-
if climatology:
|
|
2122
|
-
return ds
|
|
2123
|
-
|
|
2124
|
-
# Identify records before or at start_time
|
|
2125
|
-
before_start = ds[time_dim] <= np.datetime64(start_time)
|
|
2126
|
-
if before_start.any():
|
|
2127
|
-
closest_before_start = ds[time_dim].where(before_start, drop=True)[-1]
|
|
2128
|
-
else:
|
|
2129
|
-
logging.warning(f"No records found at or before the start_time: {start_time}.")
|
|
2130
|
-
closest_before_start = ds[time_dim][0]
|
|
2131
|
-
|
|
2132
|
-
# Identify records after or at end_time
|
|
2133
|
-
after_end = ds[time_dim] >= np.datetime64(end_time)
|
|
2134
|
-
if after_end.any():
|
|
2135
|
-
closest_after_end = ds[time_dim].where(after_end, drop=True).min()
|
|
2136
|
-
else:
|
|
2137
|
-
logging.warning(f"No records found at or after the end_time: {end_time}.")
|
|
2138
|
-
closest_after_end = ds[time_dim].max()
|
|
2139
|
-
|
|
2140
|
-
# Select records within the time range and add the closest before/after
|
|
2141
|
-
within_range = (ds[time_dim] > np.datetime64(start_time)) & (
|
|
2142
|
-
ds[time_dim] < np.datetime64(end_time)
|
|
2143
|
-
)
|
|
2144
|
-
selected_times = ds[time_dim].where(
|
|
2145
|
-
within_range
|
|
2146
|
-
| (ds[time_dim] == closest_before_start)
|
|
2147
|
-
| (ds[time_dim] == closest_after_end),
|
|
2148
|
-
drop=True,
|
|
2149
|
-
)
|
|
2150
|
-
ds = ds.sel({time_dim: selected_times})
|
|
2151
|
-
|
|
2152
|
-
return ds
|
|
1750
|
+
Determine which grid boundaries contain at least one ocean point.
|
|
2153
1751
|
|
|
2154
|
-
|
|
2155
|
-
def _select_initial_time(
|
|
2156
|
-
ds: xr.Dataset,
|
|
2157
|
-
time_dim: str,
|
|
2158
|
-
ini_time: datetime,
|
|
2159
|
-
climatology: bool,
|
|
2160
|
-
allow_flex_time: bool = False,
|
|
2161
|
-
) -> xr.Dataset:
|
|
2162
|
-
"""Select exactly one initial time from dataset.
|
|
1752
|
+
Any boundary consisting entirely of land is considered inactive.
|
|
2163
1753
|
|
|
2164
1754
|
Parameters
|
|
2165
1755
|
----------
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
time_dim : str
|
|
2169
|
-
Name of the time dimension.
|
|
2170
|
-
ini_time : datetime
|
|
2171
|
-
The desired initial time.
|
|
2172
|
-
allow_flex_time : bool
|
|
2173
|
-
- If True: allow a +24h window and pick the closest available timestamp.
|
|
2174
|
-
- If False (default): require an exact match, otherwise raise ValueError.
|
|
1756
|
+
mask : xr.DataArray
|
|
1757
|
+
2D mask array on rho-points where 1 = ocean, 0 = land.
|
|
2175
1758
|
|
|
2176
1759
|
Returns
|
|
2177
1760
|
-------
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
Raises
|
|
2182
|
-
------
|
|
2183
|
-
ValueError
|
|
2184
|
-
If no matching time is found (when `allow_flex_time=False`), or no entries are
|
|
2185
|
-
available within the +24h window (when `allow_flex_time=True`).
|
|
1761
|
+
dict[str, bool]
|
|
1762
|
+
Boolean availability for {south, east, north, west}.
|
|
2186
1763
|
"""
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
ds["time"] = ds["time"] / np.timedelta64(1, "D")
|
|
2190
|
-
# Interpolate from climatology for initial conditions
|
|
2191
|
-
return interpolate_from_climatology(ds, time_dim, ini_time)
|
|
2192
|
-
|
|
2193
|
-
if allow_flex_time:
|
|
2194
|
-
# Look in time range [ini_time, ini_time + 24h)
|
|
2195
|
-
end_time = ini_time + timedelta(days=1)
|
|
2196
|
-
times = (np.datetime64(ini_time) <= ds[time_dim]) & (
|
|
2197
|
-
ds[time_dim] < np.datetime64(end_time)
|
|
2198
|
-
)
|
|
2199
|
-
|
|
2200
|
-
if np.all(~times):
|
|
2201
|
-
raise ValueError(
|
|
2202
|
-
f"No time entries found between {ini_time} and {end_time}."
|
|
2203
|
-
)
|
|
1764
|
+
bdry_coords = get_boundary_coords()
|
|
1765
|
+
boundaries = {}
|
|
2204
1766
|
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
ds = ds.isel({time_dim: 0})
|
|
1767
|
+
for direction in ["south", "east", "north", "west"]:
|
|
1768
|
+
coords = bdry_coords["rho"][direction]
|
|
1769
|
+
bdry_mask = mask.isel(**coords)
|
|
2209
1770
|
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
)
|
|
1771
|
+
# Boundary is valid if ANY ocean point exists
|
|
1772
|
+
boundaries[direction] = bool(bdry_mask.values.any())
|
|
2213
1773
|
|
|
2214
|
-
|
|
2215
|
-
# Strict match required
|
|
2216
|
-
if not (ds[time_dim].values == np.datetime64(ini_time)).any():
|
|
2217
|
-
raise ValueError(
|
|
2218
|
-
f"No exact match found for initial time {ini_time}. Consider setting allow_flex_time to True."
|
|
2219
|
-
)
|
|
2220
|
-
|
|
2221
|
-
ds = ds.sel({time_dim: np.datetime64(ini_time)})
|
|
2222
|
-
|
|
2223
|
-
if time_dim not in ds.dims:
|
|
2224
|
-
ds = ds.expand_dims(time_dim)
|
|
2225
|
-
|
|
2226
|
-
return ds
|
|
1774
|
+
return boundaries
|
|
@@ -86,18 +86,16 @@ def test_extract_efficiency(create_member_ds: xr.Dataset) -> None:
|
|
|
86
86
|
assert eff_rel.time.attrs.get("long_name") == "time since release start"
|
|
87
87
|
|
|
88
88
|
|
|
89
|
-
def
|
|
90
|
-
"""Test that _extract_efficiency raises an error if '
|
|
89
|
+
def test_extract_efficiency_missing_time() -> None:
|
|
90
|
+
"""Test that _extract_efficiency raises an error if 'time' is missing."""
|
|
91
91
|
times = np.array(["2000-01-01", "2000-01-02"], dtype="datetime64[ns]")
|
|
92
92
|
ds = xr.Dataset(
|
|
93
93
|
{"cdr_efficiency": ("time", [0.1, 0.2])},
|
|
94
|
-
coords={"
|
|
94
|
+
coords={"abs_time": times}, # Note: no 'time' coordinate
|
|
95
95
|
)
|
|
96
96
|
|
|
97
97
|
ens = Ensemble.__new__(Ensemble)
|
|
98
|
-
with pytest.raises(
|
|
99
|
-
ValueError, match="Dataset must contain an 'abs_time' coordinate."
|
|
100
|
-
):
|
|
98
|
+
with pytest.raises(ValueError, match="Dataset must contain a 'time' coordinate."):
|
|
101
99
|
ens._extract_efficiency(ds)
|
|
102
100
|
|
|
103
101
|
|