roms-tools 2.2.1__py3-none-any.whl → 2.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.
Files changed (152) hide show
  1. ci/environment.yml +1 -0
  2. roms_tools/__init__.py +2 -0
  3. roms_tools/analysis/roms_output.py +590 -0
  4. roms_tools/{setup/download.py → download.py} +3 -0
  5. roms_tools/{setup/plot.py → plot.py} +34 -28
  6. roms_tools/setup/boundary_forcing.py +199 -203
  7. roms_tools/setup/datasets.py +60 -136
  8. roms_tools/setup/grid.py +40 -67
  9. roms_tools/setup/initial_conditions.py +249 -247
  10. roms_tools/setup/nesting.py +6 -27
  11. roms_tools/setup/river_forcing.py +41 -76
  12. roms_tools/setup/surface_forcing.py +125 -75
  13. roms_tools/setup/tides.py +31 -51
  14. roms_tools/setup/topography.py +1 -1
  15. roms_tools/setup/utils.py +44 -224
  16. roms_tools/tests/test_analysis/test_roms_output.py +269 -0
  17. roms_tools/tests/{test_setup/test_regrid.py → test_regrid.py} +1 -1
  18. roms_tools/tests/test_setup/test_boundary_forcing.py +221 -58
  19. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/.zattrs +5 -3
  20. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/.zmetadata +156 -121
  21. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/abs_time/.zarray +2 -2
  22. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/abs_time/.zattrs +2 -1
  23. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/abs_time/0 +0 -0
  24. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/bry_time/.zarray +2 -2
  25. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/bry_time/.zattrs +1 -1
  26. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/bry_time/0 +0 -0
  27. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/salt_east/.zarray +4 -4
  28. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/salt_east/0.0.0 +0 -0
  29. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/salt_north/.zarray +4 -4
  30. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/salt_north/0.0.0 +0 -0
  31. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/salt_south/.zarray +4 -4
  32. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/salt_south/0.0.0 +0 -0
  33. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/salt_west/.zarray +4 -4
  34. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/salt_west/0.0.0 +0 -0
  35. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/temp_east/.zarray +4 -4
  36. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/temp_east/0.0.0 +0 -0
  37. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/temp_north/.zarray +4 -4
  38. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/temp_north/0.0.0 +0 -0
  39. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/temp_south/.zarray +4 -4
  40. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/temp_south/0.0.0 +0 -0
  41. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/temp_west/.zarray +4 -4
  42. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/temp_west/0.0.0 +0 -0
  43. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/u_east/.zarray +4 -4
  44. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/u_east/0.0.0 +0 -0
  45. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/u_north/.zarray +4 -4
  46. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/u_north/0.0.0 +0 -0
  47. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/u_south/.zarray +4 -4
  48. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/u_south/0.0.0 +0 -0
  49. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/u_west/.zarray +4 -4
  50. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/u_west/0.0.0 +0 -0
  51. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/ubar_east/.zarray +4 -4
  52. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/ubar_east/0.0 +0 -0
  53. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/ubar_north/.zarray +4 -4
  54. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/ubar_north/0.0 +0 -0
  55. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/ubar_south/.zarray +4 -4
  56. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/ubar_south/0.0 +0 -0
  57. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/ubar_west/.zarray +4 -4
  58. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/ubar_west/0.0 +0 -0
  59. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/v_east/.zarray +4 -4
  60. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/v_east/0.0.0 +0 -0
  61. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/v_north/.zarray +4 -4
  62. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/v_north/0.0.0 +0 -0
  63. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/v_south/.zarray +4 -4
  64. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/v_south/0.0.0 +0 -0
  65. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/v_west/.zarray +4 -4
  66. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/v_west/0.0.0 +0 -0
  67. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/vbar_east/.zarray +4 -4
  68. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/vbar_east/0.0 +0 -0
  69. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/vbar_north/.zarray +4 -4
  70. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/vbar_north/0.0 +0 -0
  71. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/vbar_south/.zarray +4 -4
  72. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/vbar_south/0.0 +0 -0
  73. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/vbar_west/.zarray +4 -4
  74. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/vbar_west/0.0 +0 -0
  75. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_east/.zarray +4 -4
  76. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_east/.zattrs +8 -0
  77. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_east/0.0 +0 -0
  78. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_north/.zarray +4 -4
  79. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_north/.zattrs +8 -0
  80. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_north/0.0 +0 -0
  81. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_south/.zarray +4 -4
  82. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_south/.zattrs +8 -0
  83. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_south/0.0 +0 -0
  84. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_west/.zarray +4 -4
  85. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_west/.zattrs +8 -0
  86. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_west/0.0 +0 -0
  87. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/.zattrs +4 -4
  88. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/.zmetadata +4 -4
  89. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/angle/0.0 +0 -0
  90. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/angle_coarse/0.0 +0 -0
  91. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/f/0.0 +0 -0
  92. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/h/0.0 +0 -0
  93. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lat_coarse/0.0 +0 -0
  94. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lat_rho/0.0 +0 -0
  95. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lat_u/0.0 +0 -0
  96. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lat_v/0.0 +0 -0
  97. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lon_coarse/0.0 +0 -0
  98. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lon_rho/0.0 +0 -0
  99. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lon_u/0.0 +0 -0
  100. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lon_v/0.0 +0 -0
  101. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/mask_coarse/0.0 +0 -0
  102. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/mask_rho/0.0 +0 -0
  103. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/mask_u/0.0 +0 -0
  104. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/mask_v/0.0 +0 -0
  105. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/pm/0.0 +0 -0
  106. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/pn/0.0 +0 -0
  107. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/.zattrs +2 -1
  108. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/.zmetadata +6 -4
  109. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/Cs_r/.zattrs +1 -1
  110. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/Cs_w/.zattrs +1 -1
  111. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/NH4/0.0.0.0 +0 -0
  112. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/NO3/0.0.0.0 +0 -0
  113. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/PO4/0.0.0.0 +0 -0
  114. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/abs_time/.zattrs +1 -0
  115. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/diatSi/0.0.0.0 +0 -0
  116. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/ocean_time/.zattrs +1 -1
  117. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/spC/0.0.0.0 +0 -0
  118. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/spCaCO3/0.0.0.0 +0 -0
  119. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/spFe/0.0.0.0 +0 -0
  120. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/temp/0.0.0.0 +0 -0
  121. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/u/0.0.0.0 +0 -0
  122. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/ubar/0.0.0 +0 -0
  123. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/v/0.0.0.0 +0 -0
  124. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/vbar/0.0.0 +0 -0
  125. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/.zmetadata +30 -0
  126. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/river_location/.zarray +22 -0
  127. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/river_location/.zattrs +8 -0
  128. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/river_location/0.0 +0 -0
  129. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/.zmetadata +30 -0
  130. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/river_location/.zarray +22 -0
  131. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/river_location/.zattrs +8 -0
  132. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/river_location/0.0 +0 -0
  133. roms_tools/tests/test_setup/test_datasets.py +1 -1
  134. roms_tools/tests/test_setup/test_grid.py +1 -14
  135. roms_tools/tests/test_setup/test_initial_conditions.py +205 -67
  136. roms_tools/tests/test_setup/test_nesting.py +0 -16
  137. roms_tools/tests/test_setup/test_river_forcing.py +9 -37
  138. roms_tools/tests/test_setup/test_surface_forcing.py +103 -74
  139. roms_tools/tests/test_setup/test_tides.py +5 -17
  140. roms_tools/tests/test_setup/test_topography.py +1 -1
  141. roms_tools/tests/test_setup/test_utils.py +57 -1
  142. roms_tools/tests/{test_utils.py → test_tiling/test_partition.py} +1 -1
  143. roms_tools/tiling/partition.py +338 -0
  144. roms_tools/utils.py +310 -276
  145. roms_tools/vertical_coordinate.py +227 -0
  146. {roms_tools-2.2.1.dist-info → roms_tools-2.4.0.dist-info}/METADATA +1 -1
  147. {roms_tools-2.2.1.dist-info → roms_tools-2.4.0.dist-info}/RECORD +151 -142
  148. roms_tools/setup/vertical_coordinate.py +0 -109
  149. /roms_tools/{setup/regrid.py → regrid.py} +0 -0
  150. {roms_tools-2.2.1.dist-info → roms_tools-2.4.0.dist-info}/LICENSE +0 -0
  151. {roms_tools-2.2.1.dist-info → roms_tools-2.4.0.dist-info}/WHEEL +0 -0
  152. {roms_tools-2.2.1.dist-info → roms_tools-2.4.0.dist-info}/top_level.txt +0 -0
ci/environment.yml CHANGED
@@ -8,6 +8,7 @@ dependencies:
8
8
  - zarr
9
9
  - pytest
10
10
  - pytest-xdist
11
+ - h5py
11
12
  - flake8
12
13
  - black
13
14
  - pre-commit==3.8.0
roms_tools/__init__.py CHANGED
@@ -15,6 +15,8 @@ from roms_tools.setup.initial_conditions import InitialConditions # noqa: F401
15
15
  from roms_tools.setup.boundary_forcing import BoundaryForcing # noqa: F401
16
16
  from roms_tools.setup.river_forcing import RiverForcing # noqa: F401
17
17
  from roms_tools.setup.nesting import Nesting # noqa: F401
18
+ from roms_tools.tiling.partition import partition_netcdf # noqa: F401
19
+ from roms_tools.analysis.roms_output import ROMSOutput # noqa: F401
18
20
 
19
21
  # Configure logging when the package is imported
20
22
  logging.basicConfig(level=logging.INFO, format="%(levelname)s - %(message)s")
@@ -0,0 +1,590 @@
1
+ import xarray as xr
2
+ import numpy as np
3
+ import matplotlib.pyplot as plt
4
+ from roms_tools.utils import _load_data
5
+ from dataclasses import dataclass, field
6
+ from typing import Union, Optional
7
+ from pathlib import Path
8
+ import os
9
+ import re
10
+ import logging
11
+ from datetime import datetime, timedelta
12
+ from roms_tools import Grid
13
+ from roms_tools.plot import _plot, _section_plot, _profile_plot, _line_plot
14
+ from roms_tools.vertical_coordinate import (
15
+ compute_depth_coordinates,
16
+ )
17
+
18
+
19
+ @dataclass(frozen=True, kw_only=True)
20
+ class ROMSOutput:
21
+ """Represents ROMS model output.
22
+
23
+ Parameters
24
+ ----------
25
+ grid : Grid
26
+ Object representing the grid information.
27
+ path : Union[str, Path, List[Union[str, Path]]]
28
+ Directory, filename, or list of filenames with model output.
29
+ type : str
30
+ Specifies the type of model output. Options are:
31
+
32
+ - "restart": for restart files.
33
+ - "average": for time-averaged files.
34
+ - "snapshot": for snapshot files.
35
+
36
+ model_reference_date : datetime, optional
37
+ If not specified, this is inferred from metadata of the model output
38
+ If specified and does not coincide with metadata, a warning is raised.
39
+ use_dask: bool, optional
40
+ Indicates whether to use dask for processing. If True, data is processed with dask; if False, data is processed eagerly. Defaults to False.
41
+ """
42
+
43
+ grid: Grid
44
+ path: Union[str, Path]
45
+ type: Union[str, Path]
46
+ use_dask: bool = False
47
+ model_reference_date: Optional[datetime] = None
48
+ ds: xr.Dataset = field(init=False, repr=False)
49
+
50
+ def __post_init__(self):
51
+ # Validate `type`
52
+ if self.type not in {"restart", "average", "snapshot"}:
53
+ raise ValueError(
54
+ f"Invalid type '{self.type}'. Must be one of 'restart', 'average', or 'snapshot'."
55
+ )
56
+
57
+ ds = self._load_model_output()
58
+ self._infer_model_reference_date_from_metadata(ds)
59
+ self._check_vertical_coordinate(ds)
60
+ ds = self._add_absolute_time(ds)
61
+ ds = self._add_lat_lon_coords(ds)
62
+ object.__setattr__(self, "ds", ds)
63
+
64
+ def plot(
65
+ self,
66
+ var_name,
67
+ time=0,
68
+ s=None,
69
+ eta=None,
70
+ xi=None,
71
+ depth_contours=False,
72
+ layer_contours=False,
73
+ ax=None,
74
+ ) -> None:
75
+ """Plot a ROMS output field for a given vertical (s_rho) or horizontal (eta, xi)
76
+ slice.
77
+
78
+ Parameters
79
+ ----------
80
+ var_name : str
81
+ Name of the variable to plot. Supported options include:
82
+
83
+ - Oceanographic fields: "temp", "salt", "zeta", "u", "v", "w", etc.
84
+ - Biogeochemical tracers: "PO4", "NO3", "O2", "DIC", "ALK", etc.
85
+
86
+ time : int, optional
87
+ Index of the time dimension to plot. Default is 0.
88
+ s : int, optional
89
+ The index of the vertical layer (`s_rho`) to plot. If not specified, the plot
90
+ will represent a horizontal slice (eta- or xi- plane). Default is None.
91
+ eta : int, optional
92
+ The eta-index to plot. Used for vertical sections or horizontal slices.
93
+ Default is None.
94
+ xi : int, optional
95
+ The xi-index to plot. Used for vertical sections or horizontal slices.
96
+ Default is None.
97
+ depth_contours : bool, optional
98
+ If True, depth contours will be overlaid on the plot, showing lines of constant
99
+ depth. This is typically used for plots that show a single vertical layer.
100
+ Default is False.
101
+ layer_contours : bool, optional
102
+ If True, contour lines representing the boundaries between vertical layers will
103
+ be added to the plot. This is particularly useful in vertical sections to
104
+ visualize the layering of the water column. For clarity, the number of layer
105
+ contours displayed is limited to a maximum of 10. Default is False.
106
+ ax : matplotlib.axes.Axes, optional
107
+ The axes to plot on. If None, a new figure is created. Note that this argument does not work for horizontal plots that display the eta- and xi-dimensions at the same time.
108
+
109
+ Returns
110
+ -------
111
+ None
112
+ This method does not return any value. It generates and displays a plot.
113
+
114
+ Raises
115
+ ------
116
+ ValueError
117
+ If the specified `var_name` is not one of the valid options.
118
+ If the field specified by `var_name` is 3D and none of `s`, `eta`, or `xi` are specified.
119
+ If the field specified by `var_name` is 2D and both `eta` and `xi` are specified.
120
+ """
121
+
122
+ # Input checks
123
+ if var_name not in self.ds:
124
+ raise ValueError(f"Variable '{var_name}' is not found in dataset.")
125
+
126
+ if "time" in self.ds[var_name].dims:
127
+ if time >= len(self.ds[var_name].time):
128
+ raise ValueError(
129
+ f"Invalid time index: The specified time index ({time}) exceeds the maximum index "
130
+ f"({len(self.ds[var_name].time) - 1}) for the 'time' dimension in variable '{var_name}'."
131
+ )
132
+ field = self.ds[var_name].isel(time=time)
133
+ else:
134
+ if time > 0:
135
+ raise ValueError(
136
+ f"Invalid input: The variable '{var_name}' does not have a 'time' dimension, "
137
+ f"but a time index ({time}) greater than 0 was provided."
138
+ )
139
+ field = self.ds[var_name]
140
+
141
+ if len(field.dims) == 3:
142
+ if not any([s is not None, eta is not None, xi is not None]):
143
+ raise ValueError(
144
+ "Invalid input: For 3D fields, you must specify at least one of the dimensions 's', 'eta', or 'xi'."
145
+ )
146
+ if all([s is not None, eta is not None, xi is not None]):
147
+ raise ValueError(
148
+ "Ambiguous input: For 3D fields, specify at most two of 's', 'eta', or 'xi'. Specifying all three is not allowed."
149
+ )
150
+
151
+ if len(field.dims) == 2 and all([eta is not None, xi is not None]):
152
+ raise ValueError(
153
+ "Conflicting input: For 2D fields, specify only one dimension, either 'eta' or 'xi', not both."
154
+ )
155
+
156
+ # Load the data
157
+ if self.use_dask:
158
+ from dask.diagnostics import ProgressBar
159
+
160
+ with ProgressBar():
161
+ field.load()
162
+
163
+ # Get correct mask and spatial coordinates
164
+ if all(dim in field.dims for dim in ["eta_rho", "xi_rho"]):
165
+ loc = "rho"
166
+ elif all(dim in field.dims for dim in ["eta_rho", "xi_u"]):
167
+ loc = "u"
168
+ elif all(dim in field.dims for dim in ["eta_v", "xi_rho"]):
169
+ loc = "v"
170
+ else:
171
+ ValueError("provided field does not have two horizontal dimension")
172
+
173
+ mask = self.grid.ds[f"mask_{loc}"]
174
+ lat_deg = self.grid.ds[f"lat_{loc}"]
175
+ lon_deg = self.grid.ds[f"lon_{loc}"]
176
+
177
+ if self.grid.straddle:
178
+ lon_deg = xr.where(lon_deg > 180, lon_deg - 360, lon_deg)
179
+
180
+ field = field.assign_coords({"lon": lon_deg, "lat": lat_deg})
181
+
182
+ # Retrieve depth coordinates
183
+ compute_layer_depth = (depth_contours or s is None) and len(field.dims) > 2
184
+ compute_interface_depth = layer_contours and s is None
185
+
186
+ if compute_layer_depth:
187
+ layer_depth = compute_depth_coordinates(
188
+ self.grid.ds,
189
+ self.ds.zeta.isel(time=time),
190
+ depth_type="layer",
191
+ location=loc,
192
+ eta=eta,
193
+ xi=xi,
194
+ )
195
+ if s is not None:
196
+ layer_depth = layer_depth.isel(s_rho=s)
197
+ if compute_interface_depth:
198
+ interface_depth = compute_depth_coordinates(
199
+ self.grid.ds,
200
+ self.ds.zeta.isel(time=time),
201
+ depth_type="interface",
202
+ location=loc,
203
+ eta=eta,
204
+ xi=xi,
205
+ )
206
+ if s is not None:
207
+ interface_depth = interface_depth.isel(s_w=s)
208
+
209
+ # Slice the field as desired
210
+ title = field.long_name
211
+ if s is not None:
212
+ title = title + f", s_rho = {field.s_rho[s].item()}"
213
+ field = field.isel(s_rho=s)
214
+ else:
215
+ depth_contours = False
216
+
217
+ def _process_dimension(field, mask, dim_name, dim_values, idx, title):
218
+ if dim_name in field.dims:
219
+ title = title + f", {dim_name} = {dim_values[idx].item()}"
220
+ field = field.isel(**{dim_name: idx})
221
+ mask = mask.isel(**{dim_name: idx})
222
+ else:
223
+ raise ValueError(
224
+ f"None of the expected dimensions ({dim_name}) found in field."
225
+ )
226
+ return field, mask, title
227
+
228
+ if eta is not None:
229
+ field, mask, title = _process_dimension(
230
+ field,
231
+ mask,
232
+ "eta_rho" if "eta_rho" in field.dims else "eta_v",
233
+ field.eta_rho if "eta_rho" in field.dims else field.eta_v,
234
+ eta,
235
+ title,
236
+ )
237
+
238
+ if xi is not None:
239
+ field, mask, title = _process_dimension(
240
+ field,
241
+ mask,
242
+ "xi_rho" if "xi_rho" in field.dims else "xi_u",
243
+ field.xi_rho if "xi_rho" in field.dims else field.xi_u,
244
+ xi,
245
+ title,
246
+ )
247
+
248
+ # Format to exclude seconds
249
+ formatted_time = np.datetime_as_string(field.abs_time.values, unit="m")
250
+ title = title + f", time: {formatted_time}"
251
+
252
+ if compute_layer_depth:
253
+ field = field.assign_coords({"layer_depth": layer_depth})
254
+
255
+ # Choose colorbar
256
+ if var_name in ["u", "v", "w", "ubar", "vbar", "zeta"]:
257
+ vmax = max(field.where(mask).max().values, -field.where(mask).min().values)
258
+ vmin = -vmax
259
+ cmap = plt.colormaps.get_cmap("RdBu_r")
260
+ else:
261
+ vmax = field.where(mask).max().values
262
+ vmin = field.where(mask).min().values
263
+ if var_name in ["temp", "salt"]:
264
+ cmap = plt.colormaps.get_cmap("YlOrRd")
265
+ else:
266
+ cmap = plt.colormaps.get_cmap("YlGn")
267
+ cmap.set_bad(color="gray")
268
+ kwargs = {"vmax": vmax, "vmin": vmin, "cmap": cmap}
269
+
270
+ # Plotting
271
+ if eta is None and xi is None:
272
+ _plot(
273
+ field=field.where(mask),
274
+ depth_contours=depth_contours,
275
+ title=title,
276
+ kwargs=kwargs,
277
+ c="g",
278
+ )
279
+ else:
280
+ if len(field.dims) == 2:
281
+ if not layer_contours:
282
+ interface_depth = None
283
+ else:
284
+ # restrict number of layer_contours to 10 for the sake of plot clearity
285
+ nr_layers = len(interface_depth["s_w"])
286
+ selected_layers = np.linspace(
287
+ 0, nr_layers - 1, min(nr_layers, 10), dtype=int
288
+ )
289
+ interface_depth = interface_depth.isel(s_w=selected_layers)
290
+ _section_plot(
291
+ field.where(mask),
292
+ interface_depth=interface_depth,
293
+ title=title,
294
+ kwargs=kwargs,
295
+ ax=ax,
296
+ )
297
+ else:
298
+ if "s_rho" in field.dims:
299
+ _profile_plot(field.where(mask), title=title, ax=ax)
300
+ else:
301
+ _line_plot(field.where(mask), title=title, ax=ax)
302
+
303
+ def compute_depth_coordinates(self, depth_type="layer", locations=["rho"]):
304
+ """Compute and update vertical depth coordinates.
305
+
306
+ Calculates vertical depth coordinates (layer or interface) for specified locations (e.g., rho, u, v points)
307
+ and updates them in the dataset (`self.ds`).
308
+
309
+ Parameters
310
+ ----------
311
+ depth_type : str
312
+ The type of depth coordinate to compute. Valid options:
313
+ - "layer": Compute layer depth coordinates.
314
+ - "interface": Compute interface depth coordinates.
315
+ locations : list[str], optional
316
+ Locations for which to compute depth coordinates. Default is ["rho", "u", "v"].
317
+ Valid options include:
318
+ - "rho": Depth coordinates at rho points.
319
+ - "u": Depth coordinates at u points.
320
+ - "v": Depth coordinates at v points.
321
+
322
+ Updates
323
+ -------
324
+ self.ds : xarray.Dataset
325
+ The dataset (`self.ds`) is updated with the following depth coordinate variables:
326
+ - f"{depth_type}_depth_rho": Depth coordinates at rho points.
327
+ - f"{depth_type}_depth_u": Depth coordinates at u points (if included in `locations`).
328
+ - f"{depth_type}_depth_v": Depth coordinates at v points (if included in `locations`).
329
+
330
+ Notes
331
+ -----
332
+ This method uses the `compute_and_update_depth_coordinates` function to perform calculations and updates.
333
+ """
334
+
335
+ for location in locations:
336
+ self.ds[f"{depth_type}_depth_{location}"] = compute_depth_coordinates(
337
+ self.grid.ds, self.ds.zeta, depth_type, location
338
+ )
339
+
340
+ def _load_model_output(self) -> xr.Dataset:
341
+ """Load the model output based on the type."""
342
+ if isinstance(self.path, list):
343
+ filetype = "list"
344
+ force_combine_nested = True
345
+ # Check if all items in the list are files
346
+ if not all(Path(item).is_file() for item in self.path):
347
+ raise FileNotFoundError(
348
+ "All items in the provided list must be valid files."
349
+ )
350
+ elif Path(self.path).is_file():
351
+ filetype = "file"
352
+ force_combine_nested = False
353
+ elif Path(self.path).is_dir():
354
+ filetype = "dir"
355
+ force_combine_nested = True
356
+ else:
357
+ raise FileNotFoundError(
358
+ f"The specified path '{self.path}' is neither a file, nor a list of files, nor a directory."
359
+ )
360
+
361
+ time_chunking = True
362
+ if self.type == "restart":
363
+ time_chunking = False
364
+ filename = _validate_and_set_filenames(self.path, filetype, "rst")
365
+ elif self.type == "average":
366
+ filename = _validate_and_set_filenames(self.path, filetype, "avg")
367
+ elif self.type == "snapshot":
368
+ filename = _validate_and_set_filenames(self.path, filetype, "his")
369
+ else:
370
+ raise ValueError(f"Unsupported type '{self.type}'.")
371
+
372
+ # Load the dataset
373
+ ds = _load_data(
374
+ filename,
375
+ dim_names={"time": "time"},
376
+ use_dask=self.use_dask,
377
+ time_chunking=time_chunking,
378
+ force_combine_nested=force_combine_nested,
379
+ )
380
+
381
+ return ds
382
+
383
+ def _infer_model_reference_date_from_metadata(self, ds: xr.Dataset) -> None:
384
+ """Infer and validate the model reference date from `ocean_time` metadata.
385
+
386
+ Parameters
387
+ ----------
388
+ ds : xr.Dataset
389
+ Dataset with an `ocean_time` variable and a `long_name` attribute
390
+ in the format `Time since YYYY/MM/DD`.
391
+
392
+ Raises
393
+ ------
394
+ ValueError
395
+ If `self.model_reference_date` is not set and the reference date cannot
396
+ be inferred, or if the inferred date does not match `self.model_reference_date`.
397
+
398
+ Warns
399
+ -----
400
+ UserWarning
401
+ If `self.model_reference_date` is set but the reference date cannot be inferred.
402
+ """
403
+ # Check if 'long_name' exists in the attributes of 'ocean_time'
404
+ if "long_name" in ds.ocean_time.attrs:
405
+ input_string = ds.ocean_time.attrs["long_name"]
406
+ match = re.search(r"(\d{4})/(\d{2})/(\d{2})", input_string)
407
+
408
+ if match:
409
+ # If a match is found, extract year, month, day and create the inferred date
410
+ year, month, day = map(int, match.groups())
411
+ inferred_date = datetime(year, month, day)
412
+
413
+ if hasattr(self, "model_reference_date") and self.model_reference_date:
414
+ # Check if the inferred date matches the provided model reference date
415
+ if self.model_reference_date != inferred_date:
416
+ raise ValueError(
417
+ f"Mismatch between `self.model_reference_date` ({self.model_reference_date}) "
418
+ f"and inferred reference date ({inferred_date})."
419
+ )
420
+ else:
421
+ # Set the model reference date if not already set
422
+ object.__setattr__(self, "model_reference_date", inferred_date)
423
+ else:
424
+ # Handle case where no match is found
425
+ if hasattr(self, "model_reference_date") and self.model_reference_date:
426
+ logging.warning(
427
+ "Could not infer the model reference date from the metadata. "
428
+ "`self.model_reference_date` will be used.",
429
+ )
430
+ else:
431
+ raise ValueError(
432
+ "Model reference date could not be inferred from the metadata, "
433
+ "and `self.model_reference_date` is not set."
434
+ )
435
+ else:
436
+ # Handle case where 'long_name' attribute doesn't exist
437
+ if hasattr(self, "model_reference_date") and self.model_reference_date:
438
+ logging.warning(
439
+ "`long_name` attribute not found in ocean_time. "
440
+ "`self.model_reference_date` will be used instead.",
441
+ )
442
+ else:
443
+ raise ValueError(
444
+ "Model reference date could not be inferred from the metadata, "
445
+ "and `self.model_reference_date` is not set."
446
+ )
447
+
448
+ def _check_vertical_coordinate(self, ds: xr.Dataset) -> None:
449
+ """Check that the vertical coordinate parameters in the dataset are consistent
450
+ with the model grid.
451
+
452
+ This method compares the vertical coordinate parameters (`theta_s`, `theta_b`, `hc`, `Cs_r`, `Cs_w`) in
453
+ the provided dataset (`ds`) with those in the model grid (`self.grid`). The first three parameters are
454
+ checked for exact equality, while the last two are checked for numerical closeness.
455
+
456
+ Parameters
457
+ ----------
458
+ ds : xarray.Dataset
459
+ The dataset containing vertical coordinate parameters in its attributes, such as `theta_s`, `theta_b`,
460
+ `hc`, `Cs_r`, and `Cs_w`.
461
+
462
+ Raises
463
+ ------
464
+ ValueError
465
+ If the vertical coordinate parameters do not match the expected values (based on exact or approximate equality).
466
+
467
+ Notes
468
+ -----
469
+ - `theta_s`, `theta_b`, and `hc` are checked for exact equality using `np.array_equal`.
470
+ - `Cs_r` and `Cs_w` are checked for numerical closeness using `np.allclose`.
471
+ """
472
+
473
+ # Check exact equality for theta_s, theta_b, and hc
474
+ if not np.array_equal(self.grid.theta_s, ds.attrs["theta_s"]):
475
+ raise ValueError(
476
+ f"theta_s from grid ({self.grid.theta_s}) does not match dataset ({ds.attrs['theta_s']})."
477
+ )
478
+
479
+ if not np.array_equal(self.grid.theta_b, ds.attrs["theta_b"]):
480
+ raise ValueError(
481
+ f"theta_b from grid ({self.grid.theta_b}) does not match dataset ({ds.attrs['theta_b']})."
482
+ )
483
+
484
+ if not np.array_equal(self.grid.hc, ds.attrs["hc"]):
485
+ raise ValueError(
486
+ f"hc from grid ({self.grid.hc}) does not match dataset ({ds.attrs['hc']})."
487
+ )
488
+
489
+ # Check numerical closeness for Cs_r and Cs_w
490
+ if not np.allclose(self.grid.ds.Cs_r, ds.attrs["Cs_r"]):
491
+ raise ValueError(
492
+ f"Cs_r from grid ({self.grid.ds.Cs_r}) is not close to dataset ({ds.attrs['Cs_r']})."
493
+ )
494
+
495
+ if not np.allclose(self.grid.ds.Cs_w, ds.attrs["Cs_w"]):
496
+ raise ValueError(
497
+ f"Cs_w from grid ({self.grid.ds.Cs_w}) is not close to dataset ({ds.attrs['Cs_w']})."
498
+ )
499
+
500
+ def _add_absolute_time(self, ds: xr.Dataset) -> xr.Dataset:
501
+ """Add absolute time as a coordinate to the dataset.
502
+
503
+ Computes "abs_time" based on "ocean_time" and a reference date,
504
+ and adds it as a coordinate.
505
+
506
+ Parameters
507
+ ----------
508
+ ds : xarray.Dataset
509
+ Dataset containing "ocean_time" in seconds since the model reference date.
510
+
511
+ Returns
512
+ -------
513
+ xarray.Dataset
514
+ Dataset with "abs_time" added and "time" removed.
515
+ """
516
+ ocean_time_seconds = ds["ocean_time"].values
517
+
518
+ abs_time = np.array(
519
+ [
520
+ self.model_reference_date + timedelta(seconds=seconds)
521
+ for seconds in ocean_time_seconds
522
+ ]
523
+ )
524
+
525
+ abs_time = xr.DataArray(
526
+ abs_time, dims=["time"], coords={"time": ds["ocean_time"]}
527
+ )
528
+ abs_time.attrs["long_name"] = "absolute time"
529
+ ds = ds.assign_coords({"abs_time": abs_time})
530
+ ds = ds.drop_vars("time")
531
+
532
+ return ds
533
+
534
+ def _add_lat_lon_coords(self, ds: xr.Dataset) -> xr.Dataset:
535
+ """Add latitude and longitude coordinates to the dataset.
536
+
537
+ Adds "lat_rho" and "lon_rho" from the grid object to the dataset.
538
+
539
+ Parameters
540
+ ----------
541
+ ds : xarray.Dataset
542
+ Dataset to update.
543
+
544
+ Returns
545
+ -------
546
+ xarray.Dataset
547
+ Dataset with "lat_rho" and "lon_rho" coordinates added.
548
+ """
549
+ ds = ds.assign_coords(
550
+ {"lat_rho": self.grid.ds["lat_rho"], "lon_rho": self.grid.ds["lon_rho"]}
551
+ )
552
+
553
+ return ds
554
+
555
+
556
+ def _validate_and_set_filenames(
557
+ filenames: Union[str, list], filetype: str, string: str
558
+ ) -> Union[str, list]:
559
+ """Validates and adjusts the filename or list of filenames based on the specified
560
+ type and checks for the presence of a string in the filename.
561
+
562
+ Parameters
563
+ ----------
564
+ filenames : Union[str, list]
565
+ A single filename (str), a list of filenames, or a directory path.
566
+ filetype : str
567
+ The type of input: 'file' for a single file, 'list' for a list of files, or 'dir' for a directory.
568
+ string : str
569
+ The string that should be present in each filename.
570
+
571
+ Returns
572
+ -------
573
+ Union[str, list]
574
+ The validated filename(s). If a directory is provided, the function returns the adjusted file pattern.
575
+ """
576
+ if filetype == "file":
577
+ if string not in os.path.basename(filenames):
578
+ logging.warning(
579
+ f"The file '{filenames}' does not appear to contain '{string}' in the name."
580
+ )
581
+ elif filetype == "list":
582
+ for file in filenames:
583
+ if string not in os.path.basename(file):
584
+ logging.warning(
585
+ f"The file '{file}' does not appear to contain '{string}' in the name."
586
+ )
587
+ elif filetype == "dir":
588
+ filenames = os.path.join(filenames, f"*{string}.*.nc")
589
+
590
+ return filenames
@@ -59,6 +59,9 @@ pup_test_data = pooch.create(
59
59
  "grid_created_with_matlab.nc": "fd537ef8159fabb18e38495ec8d44e2fa1b7fb615fcb1417dd4c0e1bb5f4e41d",
60
60
  "etopo5_coarsened_and_shifted.nc": "9a5cb4b38c779d22ddb0ad069b298b9722db34ca85a89273eccca691e89e6f96",
61
61
  "srtm15_coarsened.nc": "48bc8f4beecfdca9c192b13f4cbeef1455f49d8261a82563aaec5757e100dff9",
62
+ "eastpac25km_rst.19980106000000.nc": "8f56d72bd8daf72eb736cc6705f93f478f4ad0ae4a95e98c4c9393a38e032f4c",
63
+ "eastpac25km_rst.19980126000000.nc": "20ad9007c980d211d1e108c50589183120c42a2d96811264cf570875107269e4",
64
+ "epac25km_grd.nc": "ec26c69cda4c4e96abde5b7756c955a7e1074931ab5a0641f598b099778fb617",
62
65
  },
63
66
  )
64
67