roms-tools 2.3.0__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 (143) hide show
  1. ci/environment.yml +1 -0
  2. roms_tools/__init__.py +1 -0
  3. roms_tools/analysis/roms_output.py +10 -6
  4. roms_tools/setup/boundary_forcing.py +178 -193
  5. roms_tools/setup/datasets.py +58 -1
  6. roms_tools/setup/grid.py +31 -97
  7. roms_tools/setup/initial_conditions.py +172 -126
  8. roms_tools/setup/nesting.py +2 -23
  9. roms_tools/setup/river_forcing.py +34 -67
  10. roms_tools/setup/surface_forcing.py +111 -61
  11. roms_tools/setup/tides.py +7 -30
  12. roms_tools/setup/utils.py +24 -70
  13. roms_tools/tests/test_setup/test_boundary_forcing.py +220 -57
  14. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/.zattrs +5 -3
  15. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/.zmetadata +156 -121
  16. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/abs_time/.zarray +2 -2
  17. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/abs_time/.zattrs +2 -1
  18. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/abs_time/0 +0 -0
  19. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/bry_time/.zarray +2 -2
  20. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/bry_time/.zattrs +1 -1
  21. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/bry_time/0 +0 -0
  22. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/salt_east/.zarray +4 -4
  23. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/salt_east/0.0.0 +0 -0
  24. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/salt_north/.zarray +4 -4
  25. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/salt_north/0.0.0 +0 -0
  26. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/salt_south/.zarray +4 -4
  27. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/salt_south/0.0.0 +0 -0
  28. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/salt_west/.zarray +4 -4
  29. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/salt_west/0.0.0 +0 -0
  30. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/temp_east/.zarray +4 -4
  31. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/temp_east/0.0.0 +0 -0
  32. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/temp_north/.zarray +4 -4
  33. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/temp_north/0.0.0 +0 -0
  34. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/temp_south/.zarray +4 -4
  35. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/temp_south/0.0.0 +0 -0
  36. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/temp_west/.zarray +4 -4
  37. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/temp_west/0.0.0 +0 -0
  38. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/u_east/.zarray +4 -4
  39. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/u_east/0.0.0 +0 -0
  40. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/u_north/.zarray +4 -4
  41. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/u_north/0.0.0 +0 -0
  42. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/u_south/.zarray +4 -4
  43. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/u_south/0.0.0 +0 -0
  44. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/u_west/.zarray +4 -4
  45. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/u_west/0.0.0 +0 -0
  46. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/ubar_east/.zarray +4 -4
  47. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/ubar_east/0.0 +0 -0
  48. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/ubar_north/.zarray +4 -4
  49. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/ubar_north/0.0 +0 -0
  50. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/ubar_south/.zarray +4 -4
  51. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/ubar_south/0.0 +0 -0
  52. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/ubar_west/.zarray +4 -4
  53. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/ubar_west/0.0 +0 -0
  54. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/v_east/.zarray +4 -4
  55. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/v_east/0.0.0 +0 -0
  56. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/v_north/.zarray +4 -4
  57. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/v_north/0.0.0 +0 -0
  58. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/v_south/.zarray +4 -4
  59. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/v_south/0.0.0 +0 -0
  60. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/v_west/.zarray +4 -4
  61. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/v_west/0.0.0 +0 -0
  62. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/vbar_east/.zarray +4 -4
  63. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/vbar_east/0.0 +0 -0
  64. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/vbar_north/.zarray +4 -4
  65. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/vbar_north/0.0 +0 -0
  66. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/vbar_south/.zarray +4 -4
  67. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/vbar_south/0.0 +0 -0
  68. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/vbar_west/.zarray +4 -4
  69. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/vbar_west/0.0 +0 -0
  70. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_east/.zarray +4 -4
  71. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_east/.zattrs +8 -0
  72. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_east/0.0 +0 -0
  73. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_north/.zarray +4 -4
  74. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_north/.zattrs +8 -0
  75. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_north/0.0 +0 -0
  76. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_south/.zarray +4 -4
  77. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_south/.zattrs +8 -0
  78. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_south/0.0 +0 -0
  79. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_west/.zarray +4 -4
  80. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_west/.zattrs +8 -0
  81. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zeta_west/0.0 +0 -0
  82. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/.zattrs +4 -4
  83. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/.zmetadata +4 -4
  84. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/angle/0.0 +0 -0
  85. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/angle_coarse/0.0 +0 -0
  86. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/f/0.0 +0 -0
  87. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/h/0.0 +0 -0
  88. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lat_coarse/0.0 +0 -0
  89. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lat_rho/0.0 +0 -0
  90. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lat_u/0.0 +0 -0
  91. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lat_v/0.0 +0 -0
  92. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lon_coarse/0.0 +0 -0
  93. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lon_rho/0.0 +0 -0
  94. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lon_u/0.0 +0 -0
  95. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lon_v/0.0 +0 -0
  96. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/mask_coarse/0.0 +0 -0
  97. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/mask_rho/0.0 +0 -0
  98. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/mask_u/0.0 +0 -0
  99. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/mask_v/0.0 +0 -0
  100. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/pm/0.0 +0 -0
  101. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/pn/0.0 +0 -0
  102. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/.zattrs +2 -1
  103. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/.zmetadata +6 -4
  104. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/Cs_r/.zattrs +1 -1
  105. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/Cs_w/.zattrs +1 -1
  106. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/NH4/0.0.0.0 +0 -0
  107. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/NO3/0.0.0.0 +0 -0
  108. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/PO4/0.0.0.0 +0 -0
  109. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/abs_time/.zattrs +1 -0
  110. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/diatSi/0.0.0.0 +0 -0
  111. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/ocean_time/.zattrs +1 -1
  112. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/spC/0.0.0.0 +0 -0
  113. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/spCaCO3/0.0.0.0 +0 -0
  114. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/spFe/0.0.0.0 +0 -0
  115. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/temp/0.0.0.0 +0 -0
  116. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/u/0.0.0.0 +0 -0
  117. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/ubar/0.0.0 +0 -0
  118. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/v/0.0.0.0 +0 -0
  119. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/vbar/0.0.0 +0 -0
  120. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/.zmetadata +30 -0
  121. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/river_location/.zarray +22 -0
  122. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/river_location/.zattrs +8 -0
  123. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/river_location/0.0 +0 -0
  124. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/.zmetadata +30 -0
  125. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/river_location/.zarray +22 -0
  126. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/river_location/.zattrs +8 -0
  127. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/river_location/0.0 +0 -0
  128. roms_tools/tests/test_setup/test_grid.py +0 -13
  129. roms_tools/tests/test_setup/test_initial_conditions.py +204 -66
  130. roms_tools/tests/test_setup/test_nesting.py +0 -16
  131. roms_tools/tests/test_setup/test_river_forcing.py +8 -36
  132. roms_tools/tests/test_setup/test_surface_forcing.py +102 -73
  133. roms_tools/tests/test_setup/test_tides.py +4 -16
  134. roms_tools/tests/test_setup/test_utils.py +1 -0
  135. roms_tools/tests/{test_utils.py → test_tiling/test_partition.py} +1 -1
  136. roms_tools/tiling/partition.py +338 -0
  137. roms_tools/utils.py +66 -333
  138. roms_tools/vertical_coordinate.py +54 -133
  139. {roms_tools-2.3.0.dist-info → roms_tools-2.4.0.dist-info}/METADATA +1 -1
  140. {roms_tools-2.3.0.dist-info → roms_tools-2.4.0.dist-info}/RECORD +143 -136
  141. {roms_tools-2.3.0.dist-info → roms_tools-2.4.0.dist-info}/LICENSE +0 -0
  142. {roms_tools-2.3.0.dist-info → roms_tools-2.4.0.dist-info}/WHEEL +0 -0
  143. {roms_tools-2.3.0.dist-info → roms_tools-2.4.0.dist-info}/top_level.txt +0 -0
roms_tools/setup/utils.py CHANGED
@@ -3,7 +3,6 @@ import numpy as np
3
3
  from typing import Union, Any, Dict, Type
4
4
  import pandas as pd
5
5
  import cftime
6
- from roms_tools.utils import partition
7
6
  from pathlib import Path
8
7
  from datetime import datetime
9
8
  from dataclasses import fields, asdict
@@ -131,23 +130,36 @@ def interpolate_from_climatology(
131
130
  time_dim_name: str,
132
131
  time: Union[xr.DataArray, pd.DatetimeIndex],
133
132
  ) -> Union[xr.DataArray, xr.Dataset]:
134
- """Interpolates the given field temporally based on the specified time points.
133
+ """Temporally interpolates a field based on specified time points.
135
134
 
136
- If `field` is an xarray.Dataset, this function applies the interpolation to all data variables in the dataset.
135
+ This function performs temporal interpolation on the input `field` to match the provided `time` values.
136
+ If the input `field` is an `xarray.Dataset`, the interpolation is applied to all its data variables individually.
137
137
 
138
138
  Parameters
139
139
  ----------
140
140
  field : xarray.DataArray or xarray.Dataset
141
- The field data to be interpolated. Can be a single DataArray or a Dataset.
141
+ The input field to be interpolated.
142
+ - If `field` is an `xarray.DataArray`, it should have a time dimension identified by `time_dim_name`.
143
+ - If `field` is an `xarray.Dataset`, all variables within the dataset are interpolated along the specified time dimension.
144
+ The time dimension is assumed to represent `day_of_year` for climatological purposes.
142
145
  time_dim_name : str
143
- The name of the dimension in `field` that represents time.
146
+ The name of the time dimension in the `field`. This dimension is used for interpolation.
144
147
  time : xarray.DataArray or pandas.DatetimeIndex
145
- The target time points for interpolation.
148
+ The target time points for interpolation. The time values should be compatible with the time format used in the `field`.
146
149
 
147
150
  Returns
148
151
  -------
149
152
  xarray.DataArray or xarray.Dataset
150
- The field values interpolated to the specified time points. The type matches the input type.
153
+ The interpolated field, with the same type as the input (`xarray.DataArray` or `xarray.Dataset`),
154
+ but aligned to the specified `time` values.
155
+
156
+ Notes
157
+ -----
158
+ - The interpolation assumes the time dimension in `field` corresponds to `day_of_year`.
159
+ If the input time values are in a datetime format, ensure they are converted to `day_of_year` before calling this function.
160
+ For example, you can preprocess the time as follows:
161
+
162
+ >>> field["time"] = field["time"].dt.dayofyear
151
163
  """
152
164
 
153
165
  def interpolate_single_field(data_array: xr.DataArray) -> xr.DataArray:
@@ -157,11 +169,11 @@ def interpolate_from_climatology(
157
169
  day_of_year = time.dt.dayofyear
158
170
  else:
159
171
  if np.size(time) == 1:
160
- day_of_year = time.timetuple().tm_yday
172
+ # Convert single datetime64 object to pandas.Timestamp
173
+ day_of_year = pd.Timestamp(time).dayofyear
161
174
  else:
162
- day_of_year = np.array([t.timetuple().tm_yday for t in time])
163
-
164
- data_array[time_dim_name] = data_array[time_dim_name].dt.days
175
+ # Convert each datetime64 object in the array to pandas.Timestamp
176
+ day_of_year = np.array([pd.Timestamp(t).dayofyear for t in time])
165
177
 
166
178
  # Concatenate across the beginning and end of the year
167
179
  time_concat = xr.concat(
@@ -201,6 +213,7 @@ def interpolate_from_climatology(
201
213
  for var, data_array in field.data_vars.items()
202
214
  }
203
215
  return xr.Dataset(interpolated_data_vars, attrs=field.attrs)
216
+
204
217
  else:
205
218
  raise TypeError("Input 'field' must be an xarray.DataArray or xarray.Dataset.")
206
219
 
@@ -568,59 +581,6 @@ def group_by_year(ds, filepath):
568
581
  return dataset_list, output_filenames
569
582
 
570
583
 
571
- def save_datasets(dataset_list, output_filenames, np_eta=None, np_xi=None):
572
- """Save the list of datasets to netCDF4 files, with optional spatial partitioning.
573
-
574
- Parameters
575
- ----------
576
- dataset_list : list
577
- List of datasets to be saved.
578
- output_filenames : list
579
- List of filenames for the output files.
580
- np_eta : int, optional
581
- The number of partitions along the `eta` direction. If `None`, no spatial partitioning is performed.
582
- np_xi : int, optional
583
- The number of partitions along the `xi` direction. If `None`, no spatial partitioning is performed.
584
-
585
- Returns
586
- -------
587
- List[Path]
588
- A list of Path objects for the filenames that were saved.
589
- """
590
-
591
- saved_filenames = []
592
-
593
- if np_eta is None and np_xi is None:
594
- # Save the dataset as a single file
595
- output_filenames = [f"{filename}.nc" for filename in output_filenames]
596
- xr.save_mfdataset(dataset_list, output_filenames)
597
-
598
- saved_filenames.extend(Path(f) for f in output_filenames)
599
-
600
- else:
601
- # Partition the dataset and save each partition as a separate file
602
- np_eta = np_eta or 1
603
- np_xi = np_xi or 1
604
-
605
- partitioned_datasets = []
606
- partitioned_filenames = []
607
- for dataset, base_filename in zip(dataset_list, output_filenames):
608
- partition_indices, partitions = partition(
609
- dataset, np_eta=np_eta, np_xi=np_xi
610
- )
611
- partition_filenames = [
612
- f"{base_filename}.{index}.nc" for index in partition_indices
613
- ]
614
- partitioned_datasets.extend(partitions)
615
- partitioned_filenames.extend(partition_filenames)
616
-
617
- xr.save_mfdataset(partitioned_datasets, partitioned_filenames)
618
-
619
- saved_filenames.extend(Path(f) for f in partitioned_filenames)
620
-
621
- return saved_filenames
622
-
623
-
624
584
  def get_target_coords(grid, use_coarse_grid=False):
625
585
  """Retrieves longitude and latitude coordinates from the grid, adjusting them based
626
586
  on longitude range.
@@ -652,8 +612,6 @@ def get_target_coords(grid, use_coarse_grid=False):
652
612
  mask = grid.ds.get("mask_coarse")
653
613
  if mask is not None:
654
614
  mask = mask.rename({"eta_coarse": "eta_rho", "xi_coarse": "xi_rho"})
655
- mask_u = interpolate_from_rho_to_u(mask, method="multiplicative")
656
- mask_v = interpolate_from_rho_to_v(mask, method="multiplicative")
657
615
 
658
616
  lat_psi = grid.ds.get("lat_psi_coarse")
659
617
  lon_psi = grid.ds.get("lon_psi_coarse")
@@ -663,8 +621,6 @@ def get_target_coords(grid, use_coarse_grid=False):
663
621
  lon = grid.ds.lon_rho
664
622
  angle = grid.ds.angle
665
623
  mask = grid.ds.get("mask_rho")
666
- mask_u = grid.ds.get("mask_u")
667
- mask_v = grid.ds.get("mask_v")
668
624
  lat_psi = grid.ds.get("lat_psi")
669
625
  lon_psi = grid.ds.get("lon_psi")
670
626
 
@@ -687,8 +643,6 @@ def get_target_coords(grid, use_coarse_grid=False):
687
643
  "lon_psi": lon_psi,
688
644
  "angle": angle,
689
645
  "mask": mask,
690
- "mask_u": mask_u,
691
- "mask_v": mask_v,
692
646
  "straddle": straddle,
693
647
  }
694
648
 
@@ -1,10 +1,11 @@
1
1
  import pytest
2
2
  from datetime import datetime
3
3
  import xarray as xr
4
+ import numpy as np
4
5
  from roms_tools import Grid, BoundaryForcing
5
6
  import textwrap
6
7
  from roms_tools.download import download_test_data
7
- from conftest import calculate_file_hash
8
+ from conftest import calculate_data_hash
8
9
  from pathlib import Path
9
10
  import logging
10
11
 
@@ -13,19 +14,23 @@ import logging
13
14
  "boundary_forcing_fixture",
14
15
  [
15
16
  "boundary_forcing",
16
- # "boundary_forcing_with_2d_fill",
17
+ "boundary_forcing_adjusted_for_zeta",
18
+ "boundary_forcing_with_2d_fill",
19
+ "boundary_forcing_with_2d_fill_adjusted_for_zeta",
17
20
  ],
18
21
  )
19
22
  def test_boundary_forcing_creation(boundary_forcing_fixture, request):
20
23
  """Test the creation of the BoundaryForcing object."""
21
24
 
22
- fname = Path(download_test_data("GLORYS_coarse_test_data.nc"))
23
25
  boundary_forcing = request.getfixturevalue(boundary_forcing_fixture)
24
- assert boundary_forcing.start_time == datetime(2021, 6, 29)
25
- assert boundary_forcing.end_time == datetime(2021, 6, 30)
26
+
27
+ fname1 = Path(download_test_data("GLORYS_NA_20120101.nc"))
28
+ fname2 = Path(download_test_data("GLORYS_NA_20121231.nc"))
29
+ assert boundary_forcing.start_time == datetime(2012, 1, 1)
30
+ assert boundary_forcing.end_time == datetime(2012, 12, 31)
26
31
  assert boundary_forcing.source == {
27
32
  "name": "GLORYS",
28
- "path": fname,
33
+ "path": [fname1, fname2],
29
34
  "climatology": False,
30
35
  }
31
36
  assert boundary_forcing.model_reference_date == datetime(2000, 1, 1)
@@ -44,16 +49,17 @@ def test_boundary_forcing_creation(boundary_forcing_fixture, request):
44
49
  assert f"v_{direction}" in boundary_forcing.ds
45
50
  assert f"zeta_{direction}" in boundary_forcing.ds
46
51
 
47
- assert len(boundary_forcing.ds.bry_time) == 1
52
+ assert len(boundary_forcing.ds.bry_time) == 2
48
53
  assert boundary_forcing.ds.coords["bry_time"].attrs["units"] == "days"
49
54
  assert not hasattr(boundary_forcing.ds, "climatology")
55
+ assert hasattr(boundary_forcing.ds, "adjust_depth_for_sea_surface_height")
56
+ assert hasattr(boundary_forcing.ds, "apply_2d_horizontal_fill")
50
57
 
51
58
 
52
59
  @pytest.mark.parametrize(
53
60
  "boundary_forcing_fixture",
54
61
  [
55
62
  "bgc_boundary_forcing_from_climatology",
56
- # "bgc_boundary_forcing_from_climatology_with_2d_fill",
57
63
  ],
58
64
  )
59
65
  def test_boundary_forcing_creation_with_bgc(boundary_forcing_fixture, request):
@@ -155,6 +161,113 @@ def test_boundary_divided_by_land_warning(caplog, use_dask):
155
161
  assert "the western boundary is divided by land" in caplog.text
156
162
 
157
163
 
164
+ def test_info_depth(caplog, use_dask):
165
+
166
+ grid = Grid(
167
+ nx=3,
168
+ ny=3,
169
+ size_x=400,
170
+ size_y=400,
171
+ center_lon=-8,
172
+ center_lat=58,
173
+ rot=0,
174
+ N=3, # number of vertical levels
175
+ theta_s=5.0, # surface control parameter
176
+ theta_b=2.0, # bottom control parameter
177
+ hc=250.0, # critical depth
178
+ )
179
+
180
+ fname1 = Path(download_test_data("GLORYS_NA_20120101.nc"))
181
+ fname2 = Path(download_test_data("GLORYS_NA_20121231.nc"))
182
+
183
+ with caplog.at_level(logging.INFO):
184
+ BoundaryForcing(
185
+ grid=grid,
186
+ start_time=datetime(2012, 1, 1),
187
+ end_time=datetime(2012, 12, 31),
188
+ source={"name": "GLORYS", "path": [fname1, fname2]},
189
+ adjust_depth_for_sea_surface_height=True,
190
+ use_dask=use_dask,
191
+ )
192
+
193
+ # Verify the warning message in the log
194
+ assert "Sea surface height will be used to adjust depth coordinates." in caplog.text
195
+
196
+ # Clear the log before the next test
197
+ caplog.clear()
198
+
199
+ with caplog.at_level(logging.INFO):
200
+
201
+ BoundaryForcing(
202
+ grid=grid,
203
+ start_time=datetime(2012, 1, 1),
204
+ end_time=datetime(2012, 12, 31),
205
+ source={"name": "GLORYS", "path": [fname1, fname2]},
206
+ adjust_depth_for_sea_surface_height=False,
207
+ use_dask=use_dask,
208
+ )
209
+ # Verify the warning message in the log
210
+ assert (
211
+ "Sea surface height will NOT be used to adjust depth coordinates."
212
+ in caplog.text
213
+ )
214
+
215
+
216
+ def test_info_fill(caplog, use_dask):
217
+
218
+ grid = Grid(
219
+ nx=3,
220
+ ny=3,
221
+ size_x=400,
222
+ size_y=400,
223
+ center_lon=-8,
224
+ center_lat=58,
225
+ rot=0,
226
+ N=3, # number of vertical levels
227
+ theta_s=5.0, # surface control parameter
228
+ theta_b=2.0, # bottom control parameter
229
+ hc=250.0, # critical depth
230
+ )
231
+
232
+ fname1 = Path(download_test_data("GLORYS_NA_20120101.nc"))
233
+ fname2 = Path(download_test_data("GLORYS_NA_20121231.nc"))
234
+
235
+ with caplog.at_level(logging.INFO):
236
+ BoundaryForcing(
237
+ grid=grid,
238
+ start_time=datetime(2012, 1, 1),
239
+ end_time=datetime(2012, 12, 31),
240
+ source={"name": "GLORYS", "path": [fname1, fname2]},
241
+ apply_2d_horizontal_fill=True,
242
+ use_dask=use_dask,
243
+ )
244
+
245
+ # Verify the warning message in the log
246
+ assert (
247
+ "Applying 2D horizontal fill to the source data before regridding."
248
+ in caplog.text
249
+ )
250
+
251
+ # Clear the log before the next test
252
+ caplog.clear()
253
+
254
+ with caplog.at_level(logging.INFO):
255
+
256
+ BoundaryForcing(
257
+ grid=grid,
258
+ start_time=datetime(2012, 1, 1),
259
+ end_time=datetime(2012, 12, 31),
260
+ source={"name": "GLORYS", "path": [fname1, fname2]},
261
+ apply_2d_horizontal_fill=False,
262
+ use_dask=use_dask,
263
+ )
264
+ # Verify the warning message in the log
265
+ assert (
266
+ "Applying 1D horizontal fill separately to each regridded boundary."
267
+ in caplog.text
268
+ )
269
+
270
+
158
271
  def test_1d_and_2d_fill_coincide_if_no_land(use_dask):
159
272
 
160
273
  # this grid lies entirely over open ocean
@@ -182,23 +295,95 @@ def test_1d_and_2d_fill_coincide_if_no_land(use_dask):
182
295
  xr.testing.assert_allclose(bf_1d_fill.ds, bf_2d_fill.ds, rtol=1.0e-4)
183
296
 
184
297
 
185
- def test_boundary_forcing_plot(boundary_forcing):
298
+ @pytest.mark.parametrize(
299
+ "boundary_forcing_fixture",
300
+ [
301
+ "boundary_forcing_adjusted_for_zeta",
302
+ "boundary_forcing_with_2d_fill_adjusted_for_zeta",
303
+ ],
304
+ )
305
+ def test_correct_depth_coords_adjusted_for_zeta(
306
+ boundary_forcing_fixture, request, use_dask
307
+ ):
308
+
309
+ boundary_forcing = request.getfixturevalue(boundary_forcing_fixture)
310
+
311
+ for direction in ["south", "east", "north", "west"]:
312
+
313
+ # Test that uppermost interface coincides with sea surface height
314
+ assert np.allclose(
315
+ boundary_forcing.ds_depth_coords[f"interface_depth_rho_{direction}"]
316
+ .isel(s_w=-1)
317
+ .values,
318
+ -boundary_forcing.ds[f"zeta_{direction}"].values,
319
+ atol=1e-6,
320
+ )
321
+
322
+
323
+ @pytest.mark.parametrize(
324
+ "boundary_forcing_fixture",
325
+ [
326
+ "boundary_forcing",
327
+ "boundary_forcing_with_2d_fill",
328
+ ],
329
+ )
330
+ def test_correct_depth_coords_zero_zeta(boundary_forcing_fixture, request, use_dask):
331
+
332
+ boundary_forcing = request.getfixturevalue(boundary_forcing_fixture)
333
+
334
+ for direction in ["south", "east", "north", "west"]:
335
+
336
+ # Test that uppermost interface coincides with sea surface height
337
+ assert np.allclose(
338
+ boundary_forcing.ds_depth_coords[f"interface_depth_rho_{direction}"]
339
+ .isel(s_w=-1)
340
+ .values,
341
+ 0 * boundary_forcing.ds[f"zeta_{direction}"].values,
342
+ atol=1e-6,
343
+ )
344
+
345
+
346
+ @pytest.mark.parametrize(
347
+ "boundary_forcing_fixture",
348
+ [
349
+ "boundary_forcing",
350
+ "boundary_forcing_with_2d_fill",
351
+ "boundary_forcing_adjusted_for_zeta",
352
+ "boundary_forcing_with_2d_fill_adjusted_for_zeta",
353
+ ],
354
+ )
355
+ def test_boundary_forcing_plot(boundary_forcing_fixture, request):
186
356
  """Test plot."""
357
+ boundary_forcing = request.getfixturevalue(boundary_forcing_fixture)
187
358
 
188
- boundary_forcing.plot(var_name="temp_south", layer_contours=True)
189
- boundary_forcing.plot(var_name="temp_east", layer_contours=True)
190
- boundary_forcing.plot(var_name="temp_north", layer_contours=True)
191
- boundary_forcing.plot(var_name="temp_west", layer_contours=True)
192
- boundary_forcing.plot(var_name="zeta_south")
193
- boundary_forcing.plot(var_name="zeta_east")
194
- boundary_forcing.plot(var_name="zeta_north")
195
- boundary_forcing.plot(var_name="zeta_west")
196
- boundary_forcing.plot(var_name="vbar_north")
197
- boundary_forcing.plot(var_name="ubar_west")
359
+ for direction in ["south", "east", "north", "west"]:
360
+ for layer_contours in [False, True]:
361
+ boundary_forcing.plot(
362
+ var_name=f"temp_{direction}", layer_contours=layer_contours
363
+ )
364
+ boundary_forcing.plot(
365
+ var_name=f"u_{direction}", layer_contours=layer_contours
366
+ )
367
+ boundary_forcing.plot(
368
+ var_name=f"v_{direction}", layer_contours=layer_contours
369
+ )
370
+ boundary_forcing.plot(var_name=f"zeta_{direction}")
371
+ boundary_forcing.plot(var_name=f"vbar_{direction}")
372
+ boundary_forcing.plot(var_name=f"ubar_{direction}")
198
373
 
199
374
 
200
- def test_boundary_forcing_save(boundary_forcing, tmp_path):
375
+ @pytest.mark.parametrize(
376
+ "boundary_forcing_fixture",
377
+ [
378
+ "boundary_forcing",
379
+ "boundary_forcing_with_2d_fill",
380
+ "boundary_forcing_adjusted_for_zeta",
381
+ "boundary_forcing_with_2d_fill_adjusted_for_zeta",
382
+ ],
383
+ )
384
+ def test_boundary_forcing_save(boundary_forcing_fixture, request, tmp_path):
201
385
  """Test save method."""
386
+ boundary_forcing = request.getfixturevalue(boundary_forcing_fixture)
202
387
 
203
388
  for file_str in ["test_bf", "test_bf.nc"]:
204
389
  # Create a temporary filepath using the tmp_path fixture
@@ -207,8 +392,8 @@ def test_boundary_forcing_save(boundary_forcing, tmp_path):
207
392
  str(tmp_path / file_str),
208
393
  ]: # test for Path object and str
209
394
 
210
- # Test saving without partitioning and grouping
211
- saved_filenames = boundary_forcing.save(filepath)
395
+ # Test saving without grouping
396
+ saved_filenames = boundary_forcing.save(filepath, group=False)
212
397
 
213
398
  filepath_str = str(Path(filepath).with_suffix(""))
214
399
  expected_filepath = Path(f"{filepath_str}.nc")
@@ -217,28 +402,16 @@ def test_boundary_forcing_save(boundary_forcing, tmp_path):
217
402
  assert expected_filepath.exists()
218
403
  expected_filepath.unlink()
219
404
 
220
- # Test saving without partitioning but with grouping
405
+ # Test saving with grouping
221
406
  saved_filenames = boundary_forcing.save(filepath, group=True)
222
407
 
223
408
  filepath_str = str(Path(filepath).with_suffix(""))
224
- expected_filepath = Path(f"{filepath_str}_202106.nc")
409
+ expected_filepath = Path(f"{filepath_str}_2012.nc")
225
410
 
226
411
  assert saved_filenames == [expected_filepath]
227
412
  assert expected_filepath.exists()
228
413
  expected_filepath.unlink()
229
414
 
230
- # Test saving with partitioning and grouping
231
- saved_filenames = boundary_forcing.save(filepath, np_eta=2, group=True)
232
- expected_filepath_list = [
233
- Path(filepath_str + f"_202106.{index}.nc") for index in range(2)
234
- ]
235
-
236
- assert saved_filenames == expected_filepath_list
237
-
238
- for expected_filepath in expected_filepath_list:
239
- assert expected_filepath.exists()
240
- expected_filepath.unlink()
241
-
242
415
 
243
416
  def test_bgc_boundary_forcing_plot(bgc_boundary_forcing_from_climatology):
244
417
  """Test plot method."""
@@ -264,7 +437,9 @@ def test_bgc_boundary_forcing_save(bgc_boundary_forcing_from_climatology, tmp_pa
264
437
  ]: # test for Path object and str
265
438
 
266
439
  # Test saving without partitioning and grouping
267
- saved_filenames = bgc_boundary_forcing_from_climatology.save(filepath)
440
+ saved_filenames = bgc_boundary_forcing_from_climatology.save(
441
+ filepath, group=False
442
+ )
268
443
 
269
444
  filepath_str = str(Path(filepath).with_suffix(""))
270
445
  expected_filepath = Path(f"{filepath_str}.nc")
@@ -283,20 +458,6 @@ def test_bgc_boundary_forcing_save(bgc_boundary_forcing_from_climatology, tmp_pa
283
458
  assert expected_filepath.exists()
284
459
  expected_filepath.unlink()
285
460
 
286
- # Test saving with partitioning
287
- saved_filenames = bgc_boundary_forcing_from_climatology.save(
288
- filepath, np_xi=2, group=True
289
- )
290
-
291
- expected_filepath_list = [
292
- Path(filepath_str + f"_clim.{index}.nc") for index in range(2)
293
- ]
294
- assert saved_filenames == expected_filepath_list
295
-
296
- for expected_filepath in expected_filepath_list:
297
- assert expected_filepath.exists()
298
- expected_filepath.unlink()
299
-
300
461
 
301
462
  @pytest.mark.parametrize(
302
463
  "bdry_forcing_fixture",
@@ -341,11 +502,12 @@ def test_files_have_same_hash(boundary_forcing, tmp_path, use_dask):
341
502
 
342
503
  filepath_str1 = str(Path(filepath1).with_suffix(""))
343
504
  filepath_str2 = str(Path(filepath2).with_suffix(""))
344
- expected_filepath1 = f"{filepath_str1}_202106.nc"
345
- expected_filepath2 = f"{filepath_str2}_202106.nc"
505
+ expected_filepath1 = f"{filepath_str1}_2012.nc"
506
+ expected_filepath2 = f"{filepath_str2}_2012.nc"
346
507
 
347
- hash1 = calculate_file_hash(expected_filepath1)
348
- hash2 = calculate_file_hash(expected_filepath2)
508
+ # Only compare hash of datasets because metadata is non-deterministic with dask
509
+ hash1 = calculate_data_hash(expected_filepath1)
510
+ hash2 = calculate_data_hash(expected_filepath2)
349
511
 
350
512
  assert hash1 == hash2, f"Hashes do not match: {hash1} != {hash2}"
351
513
 
@@ -372,8 +534,9 @@ def test_files_have_same_hash_clim(
372
534
  expected_filepath1 = f"{filepath_str1}_clim.nc"
373
535
  expected_filepath2 = f"{filepath_str2}_clim.nc"
374
536
 
375
- hash1 = calculate_file_hash(expected_filepath1)
376
- hash2 = calculate_file_hash(expected_filepath2)
537
+ # Only compare hash of datasets because metadata is non-deterministic with dask
538
+ hash1 = calculate_data_hash(expected_filepath1)
539
+ hash2 = calculate_data_hash(expected_filepath2)
377
540
 
378
541
  assert hash1 == hash2, f"Hashes do not match: {hash1} != {hash2}"
379
542
 
@@ -1,10 +1,12 @@
1
1
  {
2
- "end_time": "2021-06-30 00:00:00",
2
+ "adjust_depth_for_sea_surface_height": "False",
3
+ "apply_2d_horizontal_fill": "False",
4
+ "end_time": "2012-12-31 00:00:00",
3
5
  "hc": 250.0,
4
6
  "model_reference_date": "2000-01-01 00:00:00",
5
- "roms_tools_version": "0.1.dev157+dirty",
7
+ "roms_tools_version": "0.1.dev177",
6
8
  "source": "GLORYS",
7
- "start_time": "2021-06-29 00:00:00",
9
+ "start_time": "2012-01-01 00:00:00",
8
10
  "theta_b": 2.0,
9
11
  "theta_s": 5.0,
10
12
  "title": "ROMS boundary forcing file created by ROMS-Tools"