roms-tools 3.4.0__py3-none-any.whl → 3.5.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 (111) hide show
  1. roms_tools/datasets/lat_lon_datasets.py +12 -0
  2. roms_tools/datasets/roms_dataset.py +140 -53
  3. roms_tools/datasets/utils.py +14 -2
  4. roms_tools/regrid.py +76 -0
  5. roms_tools/setup/boundary_forcing.py +2 -2
  6. roms_tools/setup/grid.py +17 -3
  7. roms_tools/setup/initial_conditions.py +314 -55
  8. roms_tools/setup/mask.py +2 -5
  9. roms_tools/setup/nesting.py +6 -3
  10. roms_tools/setup/surface_forcing.py +1 -2
  11. roms_tools/setup/tides.py +6 -5
  12. roms_tools/setup/utils.py +220 -142
  13. roms_tools/tests/test_datasets/test_roms_dataset.py +225 -21
  14. roms_tools/tests/test_regrid.py +120 -1
  15. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/ALK/c/0/0/0/0 +0 -0
  16. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/ALK/zarr.json +57 -0
  17. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/ALK_ALT_CO2/c/0/0/0/0 +0 -0
  18. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/ALK_ALT_CO2/zarr.json +57 -0
  19. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/Cs_r/c/0 +0 -0
  20. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/Cs_r/zarr.json +47 -0
  21. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/Cs_w/c/0 +0 -0
  22. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/Cs_w/zarr.json +47 -0
  23. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DIC/c/0/0/0/0 +0 -0
  24. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DIC/zarr.json +57 -0
  25. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DIC_ALT_CO2/c/0/0/0/0 +0 -0
  26. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DIC_ALT_CO2/zarr.json +57 -0
  27. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DOC/c/0/0/0/0 +0 -0
  28. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DOC/zarr.json +57 -0
  29. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DOCr/c/0/0/0/0 +0 -0
  30. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DOCr/zarr.json +57 -0
  31. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DON/c/0/0/0/0 +0 -0
  32. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DON/zarr.json +57 -0
  33. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DONr/c/0/0/0/0 +0 -0
  34. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DONr/zarr.json +57 -0
  35. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DOP/c/0/0/0/0 +0 -0
  36. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DOP/zarr.json +57 -0
  37. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DOPr/c/0/0/0/0 +0 -0
  38. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DOPr/zarr.json +57 -0
  39. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/Fe/c/0/0/0/0 +0 -0
  40. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/Fe/zarr.json +57 -0
  41. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/Lig/c/0/0/0/0 +0 -0
  42. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/Lig/zarr.json +57 -0
  43. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/NH4/c/0/0/0/0 +0 -0
  44. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/NH4/zarr.json +57 -0
  45. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/NO3/c/0/0/0/0 +0 -0
  46. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/NO3/zarr.json +57 -0
  47. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/O2/c/0/0/0/0 +0 -0
  48. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/O2/zarr.json +57 -0
  49. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/PO4/c/0/0/0/0 +0 -0
  50. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/PO4/zarr.json +57 -0
  51. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/SiO3/c/0/0/0/0 +0 -0
  52. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/SiO3/zarr.json +57 -0
  53. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/abs_time/zarr.json +47 -0
  54. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diatC/c/0/0/0/0 +0 -0
  55. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diatC/zarr.json +57 -0
  56. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diatChl/c/0/0/0/0 +0 -0
  57. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diatChl/zarr.json +57 -0
  58. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diatFe/c/0/0/0/0 +0 -0
  59. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diatFe/zarr.json +57 -0
  60. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diatP/c/0/0/0/0 +0 -0
  61. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diatP/zarr.json +57 -0
  62. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diatSi/c/0/0/0/0 +0 -0
  63. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diatSi/zarr.json +57 -0
  64. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diazC/c/0/0/0/0 +0 -0
  65. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diazC/zarr.json +57 -0
  66. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diazChl/c/0/0/0/0 +0 -0
  67. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diazChl/zarr.json +57 -0
  68. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diazFe/c/0/0/0/0 +0 -0
  69. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diazFe/zarr.json +57 -0
  70. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diazP/c/0/0/0/0 +0 -0
  71. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diazP/zarr.json +57 -0
  72. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/ocean_time/c/0 +0 -0
  73. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/ocean_time/zarr.json +47 -0
  74. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/salt/c/0/0/0/0 +0 -0
  75. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/salt/zarr.json +57 -0
  76. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/spC/c/0/0/0/0 +0 -0
  77. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/spC/zarr.json +57 -0
  78. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/spCaCO3/c/0/0/0/0 +0 -0
  79. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/spCaCO3/zarr.json +57 -0
  80. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/spChl/c/0/0/0/0 +0 -0
  81. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/spChl/zarr.json +57 -0
  82. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/spFe/c/0/0/0/0 +0 -0
  83. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/spFe/zarr.json +57 -0
  84. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/spP/c/0/0/0/0 +0 -0
  85. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/spP/zarr.json +57 -0
  86. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/temp/c/0/0/0/0 +0 -0
  87. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/temp/zarr.json +57 -0
  88. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/u/c/0/0/0/0 +0 -0
  89. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/u/zarr.json +57 -0
  90. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/ubar/c/0/0/0 +0 -0
  91. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/ubar/zarr.json +54 -0
  92. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/v/c/0/0/0/0 +0 -0
  93. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/v/zarr.json +57 -0
  94. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/vbar/c/0/0/0 +0 -0
  95. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/vbar/zarr.json +54 -0
  96. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/w/zarr.json +57 -0
  97. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/zarr.json +2481 -0
  98. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/zeta/c/0/0/0 +0 -0
  99. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/zeta/zarr.json +54 -0
  100. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/zooC/c/0/0/0/0 +0 -0
  101. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/zooC/zarr.json +57 -0
  102. roms_tools/tests/test_setup/test_grid.py +24 -0
  103. roms_tools/tests/test_setup/test_initial_conditions.py +128 -11
  104. roms_tools/tests/test_setup/test_validation.py +15 -0
  105. roms_tools/tests/test_utils.py +287 -0
  106. roms_tools/utils.py +177 -72
  107. {roms_tools-3.4.0.dist-info → roms_tools-3.5.0.dist-info}/METADATA +2 -3
  108. {roms_tools-3.4.0.dist-info → roms_tools-3.5.0.dist-info}/RECORD +111 -24
  109. {roms_tools-3.4.0.dist-info → roms_tools-3.5.0.dist-info}/WHEEL +1 -1
  110. {roms_tools-3.4.0.dist-info → roms_tools-3.5.0.dist-info}/licenses/LICENSE +0 -0
  111. {roms_tools-3.4.0.dist-info → roms_tools-3.5.0.dist-info}/top_level.txt +0 -0
@@ -661,6 +661,18 @@ class LatLonDataset:
661
661
  if "depth" in self.dim_names:
662
662
  self.ds = extrapolate_deepest_to_bottom(self.ds, self.dim_names["depth"])
663
663
 
664
+ def rotate_velocities_to_east_and_north(self) -> None:
665
+ """
666
+ Rotate velocity components to east/north directions.
667
+
668
+ For lat-lon datasets, velocity components are already defined in
669
+ earth-relative east/north coordinates. Therefore, no rotation is
670
+ required and this method is a no-op.
671
+ This method is provided for API compatibility with ROMSDataset,
672
+ where an explicit rotation using the grid angle is necessary.
673
+ """
674
+ return None
675
+
664
676
  @classmethod
665
677
  def from_ds(cls, original_dataset: LatLonDataset, ds: xr.Dataset) -> LatLonDataset:
666
678
  """Substitute the internal dataset of a LatLonDataset object with a new xarray
@@ -18,7 +18,12 @@ from roms_tools.datasets.utils import (
18
18
  validate_start_end_time,
19
19
  )
20
20
  from roms_tools.fill import LateralFill
21
- from roms_tools.utils import load_data, wrap_longitudes
21
+ from roms_tools.utils import (
22
+ get_dask_chunks,
23
+ load_data,
24
+ rotate_velocities,
25
+ wrap_longitudes,
26
+ )
22
27
  from roms_tools.vertical_coordinate import (
23
28
  compute_depth_coordinates,
24
29
  )
@@ -578,6 +583,12 @@ class ROMSDataset:
578
583
  )
579
584
  self.ds = subdomain
580
585
 
586
+ subdomain_grid_ds = choose_subdomain(
587
+ self.grid.ds, self.grid.ds, target_coords, buffer_points
588
+ )
589
+
590
+ self.grid = self.grid.copy_with_ds(subdomain_grid_ds)
591
+
581
592
  def convert_to_float64(self) -> None:
582
593
  """Convert all data variables in the dataset to float64.
583
594
 
@@ -664,6 +675,45 @@ class ROMSDataset:
664
675
  if filler is not None:
665
676
  self.ds[var_name] = filler.apply(var)
666
677
 
678
+ def rotate_velocities_to_east_and_north(
679
+ self,
680
+ velocity_pairs: tuple[tuple[str, str], ...] = (("u", "v"),),
681
+ ) -> None:
682
+ """
683
+ Rotate model-grid velocity components to earth-relative east/north directions.
684
+
685
+ Parameters
686
+ ----------
687
+ velocity_pairs : tuple of (str, str), optional
688
+ Pairs of velocity variable keys (as used in ``self.var_names``) to rotate,
689
+ e.g. ("u", "v") or ("ubar", "vbar"). By default, only the 3D velocities
690
+ ("u", "v") are rotated.
691
+ """
692
+ if self.var_names is None:
693
+ return
694
+
695
+ angle = -self.grid.ds["angle"]
696
+
697
+ for u_key, v_key in velocity_pairs:
698
+ if u_key not in self.var_names or v_key not in self.var_names:
699
+ continue
700
+
701
+ u_name = self.var_names[u_key]
702
+ v_name = self.var_names[v_key]
703
+
704
+ self.ds[u_name], self.ds[v_name] = rotate_velocities(
705
+ self.ds[u_name],
706
+ self.ds[v_name],
707
+ angle,
708
+ interpolate_before=True,
709
+ )
710
+
711
+ if self.use_dask:
712
+ chunks = get_dask_chunks(self.dim_names)
713
+ # Only keep chunks for dimensions that exist in the dataset
714
+ chunks = {dim: size for dim, size in chunks.items() if dim in self.ds.dims}
715
+ self.ds = self.ds.chunk(chunks)
716
+
667
717
 
668
718
  def choose_subdomain(
669
719
  ds: xr.Dataset,
@@ -699,69 +749,106 @@ def choose_subdomain(
699
749
  ValueError
700
750
  If the selected latitude or longitude range does not intersect with the dataset.
701
751
  """
702
- # Adjust longitude range if needed to match the expected range
703
- ds = wrap_longitudes(ds, target_coords["straddle"])
704
-
752
+ # Extract lat/lon min/max from target
705
753
  lat_min = target_coords["lat"].min().values
706
754
  lat_max = target_coords["lat"].max().values
707
755
  lon_min = target_coords["lon"].min().values
708
756
  lon_max = target_coords["lon"].max().values
709
757
 
710
- # Extract grid spacing (in meters)
711
- dx = 0.5 * ((1 / ds_grid.pm).mean() + (1 / ds_grid.pn).mean()) # meters
712
- buffer = dx * buffer_points # buffer distance in meters
758
+ # Compute buffer in degrees
759
+ dx = 0.5 * ((1 / ds_grid.pm).mean() + (1 / ds_grid.pn).mean())
760
+ buffer = dx * buffer_points
761
+ lat_center = np.deg2rad(0.5 * (lat_min + lat_max))
762
+ margin_lat = buffer / 111_320.0
763
+ margin_lon = buffer / (111_320.0 * np.cos(lat_center))
764
+
765
+ lon_min_buf = lon_min - margin_lon
766
+ lon_max_buf = lon_max + margin_lon
767
+
768
+ # Normalize buffered bounds to target convention
769
+ if target_coords["straddle"]:
770
+ # [-180, 180]
771
+ if lon_min_buf < -180:
772
+ lon_min_buf += 360
773
+ if lon_max_buf > 180:
774
+ lon_max_buf -= 360
775
+ else:
776
+ # [0, 360]
777
+ if lon_min_buf < 0:
778
+ lon_min_buf += 360
779
+ if lon_max_buf >= 360:
780
+ lon_max_buf -= 360
781
+
782
+ # Wrap dataset longitudes to target convention
783
+ ds = wrap_longitudes(ds, target_coords["straddle"])
713
784
 
714
- lat = np.deg2rad(0.5 * (lat_min + lat_max))
785
+ # Rho points
786
+ location = "rho"
787
+ eta_dim, xi_dim = "eta_rho", "xi_rho"
788
+ lat_coord, lon_coord = f"lat_{location}", f"lon_{location}"
789
+ _check_latlon_coords(ds, eta_dim, xi_dim, location)
790
+ ds_lon = ds[lon_coord]
791
+
792
+ if lon_max_buf < lon_min_buf: # crosses dateline
793
+ subset_mask_lon = (ds_lon >= lon_min_buf) | (ds_lon <= lon_max_buf)
794
+ else:
795
+ subset_mask_lon = (ds_lon >= lon_min_buf) & (ds_lon <= lon_max_buf)
796
+
797
+ # Full mask including latitude
798
+ subset_mask = (
799
+ (ds[lat_coord] >= lat_min - margin_lat)
800
+ & (ds[lat_coord] <= lat_max + margin_lat)
801
+ & subset_mask_lon
802
+ )
715
803
 
716
- deg_per_meter_lat = 1 / 111_320.0
717
- margin_lat = buffer * deg_per_meter_lat
804
+ eta_mask = subset_mask.any(dim=xi_dim)
805
+ xi_mask = subset_mask.any(dim=eta_dim)
806
+ eta_indices = np.where(eta_mask)[0]
807
+ xi_indices = np.where(xi_mask)[0]
808
+ first_eta, last_eta = eta_indices[0], eta_indices[-1]
809
+ first_xi, last_xi = xi_indices[0], xi_indices[-1]
810
+
811
+ # Subset rho points
812
+ ds = ds.isel(
813
+ **{
814
+ "eta_rho": slice(first_eta, last_eta + 1),
815
+ "xi_rho": slice(first_xi, last_xi + 1),
816
+ }
817
+ )
718
818
 
719
- deg_per_meter_lon = 1 / (111_320.0 * np.cos(lat))
720
- margin_lon = buffer * deg_per_meter_lon
819
+ # Subset u points only if these dimensions exist
820
+ if "xi_u" in ds.dims:
821
+ ds = ds.isel(
822
+ **{
823
+ "xi_u": slice(first_xi, last_xi),
824
+ }
825
+ )
721
826
 
722
- mapping = {
723
- "rho": ("eta_rho", "xi_rho"),
724
- "u": ("eta_rho", "xi_u"),
725
- "v": ("eta_v", "xi_rho"),
726
- }
827
+ # Subset v points only if these dimensions exist
828
+ if "eta_v" in ds.dims:
829
+ ds = ds.isel(
830
+ **{
831
+ "eta_v": slice(first_eta, last_eta),
832
+ }
833
+ )
727
834
 
728
- for location in ["rho", "u", "v"]:
729
- eta_dim, xi_dim = mapping[location]
835
+ return ds
730
836
 
731
- if eta_dim in ds.dims and xi_dim in ds.dims:
732
- # Check that lat/lon coordinates exist
733
- lat_coord = f"lat_{location}"
734
- lon_coord = f"lon_{location}"
735
- if lat_coord not in ds.coords or lon_coord not in ds.coords:
736
- raise ValueError(
737
- f"Dataset is missing expected coordinates for location '{location}': "
738
- f"expected '{lat_coord}' and '{lon_coord}'"
739
- )
740
837
 
741
- # Build subset mask
742
- subset_mask = (
743
- (ds[lat_coord] > lat_min - margin_lat)
744
- & (ds[lat_coord] < lat_max + margin_lat)
745
- & (ds[lon_coord] > lon_min - margin_lon)
746
- & (ds[lon_coord] < lon_max + margin_lon)
747
- )
838
+ def _check_latlon_coords(
839
+ ds: xr.Dataset, eta_dim: str, xi_dim: str, location: str
840
+ ) -> None:
841
+ """
842
+ Ensure latitude and longitude coordinates exist for a given grid location.
748
843
 
749
- # Reduce along xi_dim
750
- eta_mask = subset_mask.any(dim=xi_dim)
751
- eta_indices = np.where(eta_mask)[0]
752
- first_eta, last_eta = eta_indices[0], eta_indices[-1]
753
-
754
- # Reduce along eta_dim
755
- xi_mask = subset_mask.any(dim=eta_dim)
756
- xi_indices = np.where(xi_mask)[0]
757
- first_xi, last_xi = xi_indices[0], xi_indices[-1]
758
-
759
- # Subset the dataset
760
- ds = ds.isel(
761
- **{
762
- eta_dim: slice(first_eta, last_eta + 1),
763
- xi_dim: slice(first_xi, last_xi + 1),
764
- }
765
- )
844
+ Raises ValueError if the expected coordinates are missing.
845
+ """
846
+ if eta_dim in ds.dims and xi_dim in ds.dims:
847
+ lat_coord = f"lat_{location}"
848
+ lon_coord = f"lon_{location}"
766
849
 
767
- return ds
850
+ if lat_coord not in ds.coords or lon_coord not in ds.coords:
851
+ raise ValueError(
852
+ f"Dataset missing coordinates for location '{location}': "
853
+ f"expected '{lat_coord}' and '{lon_coord}'"
854
+ )
@@ -36,9 +36,21 @@ def extrapolate_deepest_to_bottom(ds: xr.Dataset, depth_dim: str) -> xr.Dataset:
36
36
 
37
37
 
38
38
  def convert_to_float64(ds: xr.Dataset) -> xr.Dataset:
39
- """Convert all non-mask data variables to float64.
39
+ """Convert all data variables in the dataset to float64.
40
40
 
41
- Variables whose names start with ``"mask_"`` are left unchanged.
41
+ This method updates the dataset by converting all of its data variables to the
42
+ `float64` data type, ensuring consistency for numerical operations that require
43
+ high precision. Variables whose names start with ``"mask_"`` are left unchanged.
44
+
45
+ Parameters
46
+ ----------
47
+ ds : xr.Dataset
48
+ Input dataset
49
+
50
+ Returns
51
+ -------
52
+ xr.Dataset:
53
+ Input dataset with data variables converted to double precision.
42
54
  """
43
55
  dtype_map = {
44
56
  name: ("float64" if not name.startswith("mask_") else var.dtype)
roms_tools/regrid.py CHANGED
@@ -295,3 +295,79 @@ class VerticalRegridFromROMS:
295
295
  )
296
296
 
297
297
  return transformed
298
+
299
+
300
+ class VerticalRegrid:
301
+ """Regrid ROMS variables along the vertical, using spatially varying coordinates.
302
+
303
+ This class uses the `xgcm` package to transform data from a ROMS vertical coordinate
304
+ system (`s_rho`) to a user-defined set of target depth levels, where both the source
305
+ and target coordinates can vary spatially (i.e., 2D fields in horizontal space).
306
+
307
+ Attributes
308
+ ----------
309
+ grid : xgcm.Grid
310
+ The XGCM grid object used for vertical regridding, initialized with the input dataset `ds`.
311
+ """
312
+
313
+ def __init__(self, ds: "xr.Dataset"):
314
+ """Initialize the VerticalRegrid object with a ROMS dataset.
315
+
316
+ Parameters
317
+ ----------
318
+ ds : xarray.Dataset
319
+ The ROMS dataset containing the vertical coordinate `s_rho` and the variable(s)
320
+ to be regridded.
321
+ """
322
+ self.grid = xgcm.Grid(
323
+ ds,
324
+ coords={"s_rho": {"center": "s_rho"}},
325
+ periodic=False,
326
+ autoparse_metadata=False,
327
+ )
328
+
329
+ def apply(
330
+ self,
331
+ da: "xr.DataArray",
332
+ source_depth_coords: "xr.DataArray",
333
+ target_depth_coords: "xr.DataArray",
334
+ mask_edges: bool = True,
335
+ ) -> "xr.DataArray":
336
+ """Regrid a ROMS variable from source vertical coordinates to target vertical coordinates.
337
+
338
+ This method supports spatially varying vertical coordinates for both source and target,
339
+ meaning that the depth levels can vary across the horizontal grid.
340
+
341
+ Parameters
342
+ ----------
343
+ da : xarray.DataArray
344
+ The data array to regrid. Must have a vertical dimension corresponding to `s_rho`.
345
+
346
+ source_depth_coords : array-like (1D or 2D)
347
+ Depth coordinates of the source data. Can be a 1D array (same for all horizontal points)
348
+ or a 2D array (varying in horizontal space).
349
+
350
+ target_depth_coords : array-like (1D or 2D)
351
+ Desired depth coordinates of the regridded data. Can also be 1D or 2D.
352
+
353
+ mask_edges : bool, optional
354
+ If True, target values outside the range of source depth coordinates are masked with NaN.
355
+ Defaults to True.
356
+
357
+ Returns
358
+ -------
359
+ xarray.DataArray
360
+ A new `DataArray` containing the regridded variable at the target depth coordinates.
361
+ """
362
+ with warnings.catch_warnings():
363
+ warnings.filterwarnings("ignore", category=FutureWarning, module="xgcm")
364
+ transformed = self.grid.transform(
365
+ da,
366
+ "s_rho",
367
+ target=target_depth_coords,
368
+ target_data=source_depth_coords,
369
+ target_dim="s_rho",
370
+ mask_edges=mask_edges,
371
+ )
372
+
373
+ return transformed
@@ -32,7 +32,6 @@ from roms_tools.setup.utils import (
32
32
  get_variable_metadata,
33
33
  group_dataset,
34
34
  nan_check,
35
- rotate_velocities,
36
35
  substitute_nans_by_fillvalue,
37
36
  to_dict,
38
37
  write_to_yaml,
@@ -40,6 +39,7 @@ from roms_tools.setup.utils import (
40
39
  from roms_tools.utils import (
41
40
  interpolate_from_rho_to_u,
42
41
  interpolate_from_rho_to_v,
42
+ rotate_velocities,
43
43
  save_datasets,
44
44
  transpose_dimensions,
45
45
  )
@@ -273,7 +273,7 @@ class BoundaryForcing:
273
273
  processed_fields["u"],
274
274
  processed_fields["v"],
275
275
  angle,
276
- interpolate=True,
276
+ interpolate_after=True,
277
277
  )
278
278
  if self.adjust_depth_for_sea_surface_height:
279
279
  zeta_u = interpolate_from_rho_to_u(zeta_vector)
roms_tools/setup/grid.py CHANGED
@@ -1,3 +1,4 @@
1
+ import copy
1
2
  import importlib.metadata
2
3
  import logging
3
4
  import re
@@ -19,12 +20,14 @@ from roms_tools.setup.utils import (
19
20
  extract_single_value,
20
21
  gc_dist,
21
22
  get_target_coords,
22
- interpolate_from_rho_to_u,
23
- interpolate_from_rho_to_v,
24
23
  pop_grid_data,
25
24
  write_to_yaml,
26
25
  )
27
- from roms_tools.utils import save_datasets
26
+ from roms_tools.utils import (
27
+ interpolate_from_rho_to_u,
28
+ interpolate_from_rho_to_v,
29
+ save_datasets,
30
+ )
28
31
  from roms_tools.vertical_coordinate import compute_depth_coordinates, sigma_stretch
29
32
 
30
33
 
@@ -1266,6 +1269,17 @@ class Grid:
1266
1269
 
1267
1270
  return z_centers, z_faces
1268
1271
 
1272
+ def copy_with_ds(self, ds: xr.Dataset) -> "Grid":
1273
+ """
1274
+ Return a copy of this Grid with the given Dataset.
1275
+
1276
+ Grid metadata is preserved; only the backing xarray Dataset
1277
+ is replaced. The original Grid is not modified.
1278
+ """
1279
+ new = copy.copy(self) # shallow copy of metadata
1280
+ new.ds = ds
1281
+ return new
1282
+
1269
1283
 
1270
1284
  def _rotate(coords, rot):
1271
1285
  """Rotate grid counterclockwise relative to surface of Earth by rot degrees."""