roms-tools 3.3.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 (246) hide show
  1. roms_tools/__init__.py +1 -1
  2. roms_tools/analysis/cdr_ensemble.py +10 -13
  3. roms_tools/analysis/roms_output.py +5 -304
  4. roms_tools/{download.py → datasets/download.py} +1 -0
  5. roms_tools/{setup → datasets}/lat_lon_datasets.py +88 -64
  6. roms_tools/{setup → datasets}/river_datasets.py +9 -4
  7. roms_tools/datasets/roms_dataset.py +854 -0
  8. roms_tools/datasets/utils.py +487 -0
  9. roms_tools/{setup/fill.py → fill.py} +110 -13
  10. roms_tools/plot.py +4 -4
  11. roms_tools/regrid.py +76 -0
  12. roms_tools/setup/boundary_forcing.py +53 -45
  13. roms_tools/setup/cdr_release.py +2 -4
  14. roms_tools/setup/grid.py +46 -15
  15. roms_tools/setup/initial_conditions.py +330 -71
  16. roms_tools/setup/mask.py +2 -5
  17. roms_tools/setup/nesting.py +13 -6
  18. roms_tools/setup/river_forcing.py +4 -4
  19. roms_tools/setup/surface_forcing.py +15 -11
  20. roms_tools/setup/tides.py +7 -6
  21. roms_tools/setup/topography.py +10 -2
  22. roms_tools/setup/utils.py +292 -666
  23. roms_tools/tests/test_analysis/test_cdr_ensemble.py +4 -6
  24. roms_tools/tests/test_analysis/test_roms_output.py +1 -220
  25. roms_tools/tests/{test_setup → test_datasets}/test_lat_lon_datasets.py +4 -4
  26. roms_tools/tests/{test_setup → test_datasets}/test_river_datasets.py +1 -1
  27. roms_tools/tests/test_datasets/test_roms_dataset.py +743 -0
  28. roms_tools/tests/test_datasets/test_utils.py +527 -0
  29. roms_tools/tests/{test_setup/test_fill.py → test_fill.py} +72 -9
  30. roms_tools/tests/test_regrid.py +120 -1
  31. roms_tools/tests/test_setup/test_boundary_forcing.py +57 -138
  32. roms_tools/tests/test_setup/test_cdr_release.py +4 -5
  33. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/zarr.json +293 -2021
  34. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/zarr.json +294 -2022
  35. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/ALK/c/0/0/0/0 +0 -0
  36. roms_tools/tests/test_setup/test_data/{bgc_boundary_forcing_from_climatology.zarr/ALK_west → initial_conditions_from_roms.zarr/ALK}/zarr.json +11 -8
  37. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/ALK_ALT_CO2/c/0/0/0/0 +0 -0
  38. roms_tools/tests/test_setup/test_data/{bgc_boundary_forcing_from_climatology.zarr/ALK_ALT_CO2_west → initial_conditions_from_roms.zarr/ALK_ALT_CO2}/zarr.json +11 -8
  39. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/Cs_r/c/0 +0 -0
  40. roms_tools/tests/test_setup/test_data/{bgc_boundary_forcing_from_unified_climatology.zarr/diatFe_west → initial_conditions_from_roms.zarr/Cs_r}/zarr.json +5 -12
  41. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/Cs_w/c/0 +0 -0
  42. roms_tools/tests/test_setup/test_data/{bgc_boundary_forcing_from_climatology.zarr/diatFe_west → initial_conditions_from_roms.zarr/Cs_w}/zarr.json +3 -10
  43. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DIC/c/0/0/0/0 +0 -0
  44. roms_tools/tests/test_setup/test_data/{bgc_boundary_forcing_from_climatology.zarr/DOCr_west → initial_conditions_from_roms.zarr/DIC}/zarr.json +11 -8
  45. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DIC_ALT_CO2/c/0/0/0/0 +0 -0
  46. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DIC_ALT_CO2/zarr.json +57 -0
  47. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DOC/c/0/0/0/0 +0 -0
  48. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DOC/zarr.json +57 -0
  49. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DOCr/c/0/0/0/0 +0 -0
  50. roms_tools/tests/test_setup/test_data/{bgc_boundary_forcing_from_climatology.zarr/DIC_ALT_CO2_west → initial_conditions_from_roms.zarr/DOCr}/zarr.json +11 -8
  51. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DON/c/0/0/0/0 +0 -0
  52. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DON/zarr.json +57 -0
  53. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DONr/c/0/0/0/0 +0 -0
  54. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DONr/zarr.json +57 -0
  55. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DOP/c/0/0/0/0 +0 -0
  56. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DOP/zarr.json +57 -0
  57. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DOPr/c/0/0/0/0 +0 -0
  58. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DOPr/zarr.json +57 -0
  59. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/Fe/c/0/0/0/0 +0 -0
  60. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/Fe/zarr.json +57 -0
  61. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/Lig/c/0/0/0/0 +0 -0
  62. roms_tools/tests/test_setup/test_data/{bgc_boundary_forcing_from_climatology.zarr/DOP_west → initial_conditions_from_roms.zarr/Lig}/zarr.json +11 -8
  63. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/NH4/c/0/0/0/0 +0 -0
  64. roms_tools/tests/test_setup/test_data/{bgc_boundary_forcing_from_climatology.zarr/DON_west → initial_conditions_from_roms.zarr/NH4}/zarr.json +11 -8
  65. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/NO3/c/0/0/0/0 +0 -0
  66. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/NO3/zarr.json +57 -0
  67. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/O2/c/0/0/0/0 +0 -0
  68. roms_tools/tests/test_setup/test_data/{bgc_boundary_forcing_from_climatology.zarr/Lig_west → initial_conditions_from_roms.zarr/O2}/zarr.json +11 -8
  69. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/PO4/c/0/0/0/0 +0 -0
  70. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/PO4/zarr.json +57 -0
  71. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/SiO3/c/0/0/0/0 +0 -0
  72. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/SiO3/zarr.json +57 -0
  73. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/abs_time/zarr.json +47 -0
  74. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diatC/c/0/0/0/0 +0 -0
  75. roms_tools/tests/test_setup/test_data/{bgc_boundary_forcing_from_climatology.zarr/diatC_west → initial_conditions_from_roms.zarr/diatC}/zarr.json +11 -8
  76. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diatChl/c/0/0/0/0 +0 -0
  77. roms_tools/tests/test_setup/test_data/{bgc_boundary_forcing_from_climatology.zarr/diatChl_west → initial_conditions_from_roms.zarr/diatChl}/zarr.json +11 -8
  78. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diatFe/c/0/0/0/0 +0 -0
  79. roms_tools/tests/test_setup/test_data/{bgc_boundary_forcing_from_climatology.zarr/O2_west → initial_conditions_from_roms.zarr/diatFe}/zarr.json +11 -8
  80. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diatP/c/0/0/0/0 +0 -0
  81. roms_tools/tests/test_setup/test_data/{bgc_boundary_forcing_from_climatology.zarr/DIC_west → initial_conditions_from_roms.zarr/diatP}/zarr.json +11 -8
  82. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diatSi/c/0/0/0/0 +0 -0
  83. roms_tools/tests/test_setup/test_data/{bgc_boundary_forcing_from_climatology.zarr/DOC_west → initial_conditions_from_roms.zarr/diatSi}/zarr.json +11 -8
  84. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diazC/c/0/0/0/0 +0 -0
  85. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diazC/zarr.json +57 -0
  86. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diazChl/c/0/0/0/0 +0 -0
  87. roms_tools/tests/test_setup/test_data/{bgc_boundary_forcing_from_climatology.zarr/diazChl_west → initial_conditions_from_roms.zarr/diazChl}/zarr.json +11 -8
  88. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diazFe/c/0/0/0/0 +0 -0
  89. roms_tools/tests/test_setup/test_data/{bgc_boundary_forcing_from_climatology.zarr/Fe_west → initial_conditions_from_roms.zarr/diazFe}/zarr.json +11 -8
  90. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diazP/c/0/0/0/0 +0 -0
  91. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diazP/zarr.json +57 -0
  92. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/ocean_time/c/0 +0 -0
  93. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/ocean_time/zarr.json +47 -0
  94. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/salt/c/0/0/0/0 +0 -0
  95. roms_tools/tests/test_setup/test_data/{bgc_boundary_forcing_from_unified_climatology.zarr/ALK_west → initial_conditions_from_roms.zarr/salt}/zarr.json +12 -9
  96. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/spC/c/0/0/0/0 +0 -0
  97. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/spC/zarr.json +57 -0
  98. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/spCaCO3/c/0/0/0/0 +0 -0
  99. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/spCaCO3/zarr.json +57 -0
  100. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/spChl/c/0/0/0/0 +0 -0
  101. roms_tools/tests/test_setup/test_data/{bgc_boundary_forcing_from_climatology.zarr/spChl_west → initial_conditions_from_roms.zarr/spChl}/zarr.json +11 -8
  102. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/spFe/c/0/0/0/0 +0 -0
  103. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/spFe/zarr.json +57 -0
  104. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/spP/c/0/0/0/0 +0 -0
  105. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/spP/zarr.json +57 -0
  106. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/temp/c/0/0/0/0 +0 -0
  107. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/temp/zarr.json +57 -0
  108. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/u/c/0/0/0/0 +0 -0
  109. roms_tools/tests/test_setup/test_data/{bgc_boundary_forcing_from_climatology.zarr/NH4_west → initial_conditions_from_roms.zarr/u}/zarr.json +12 -9
  110. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/ubar/c/0/0/0 +0 -0
  111. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/ubar/zarr.json +54 -0
  112. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/v/c/0/0/0/0 +0 -0
  113. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/v/zarr.json +57 -0
  114. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/vbar/c/0/0/0 +0 -0
  115. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/vbar/zarr.json +54 -0
  116. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/w/zarr.json +57 -0
  117. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/zarr.json +2481 -0
  118. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/zeta/c/0/0/0 +0 -0
  119. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/zeta/zarr.json +54 -0
  120. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/zooC/c/0/0/0/0 +0 -0
  121. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/zooC/zarr.json +57 -0
  122. roms_tools/tests/test_setup/test_grid.py +66 -1
  123. roms_tools/tests/test_setup/test_initial_conditions.py +130 -104
  124. roms_tools/tests/test_setup/test_nesting.py +2 -1
  125. roms_tools/tests/test_setup/test_surface_forcing.py +1 -1
  126. roms_tools/tests/test_setup/test_tides.py +1 -1
  127. roms_tools/tests/test_setup/test_utils.py +100 -15
  128. roms_tools/tests/test_setup/test_validation.py +15 -0
  129. roms_tools/tests/test_tiling/test_partition.py +63 -15
  130. roms_tools/tests/test_utils.py +365 -0
  131. roms_tools/tiling/partition.py +81 -211
  132. roms_tools/utils.py +360 -62
  133. {roms_tools-3.3.0.dist-info → roms_tools-3.5.0.dist-info}/METADATA +2 -3
  134. {roms_tools-3.3.0.dist-info → roms_tools-3.5.0.dist-info}/RECORD +137 -174
  135. {roms_tools-3.3.0.dist-info → roms_tools-3.5.0.dist-info}/WHEEL +1 -1
  136. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/ALK_ALT_CO2_west/c/0/0/0 +0 -0
  137. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/ALK_west/c/0/0/0 +0 -0
  138. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DIC_ALT_CO2_west/c/0/0/0 +0 -0
  139. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DIC_west/c/0/0/0 +0 -0
  140. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DOC_west/c/0/0/0 +0 -0
  141. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DOCr_west/c/0/0/0 +0 -0
  142. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DON_west/c/0/0/0 +0 -0
  143. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DONr_west/c/0/0/0 +0 -0
  144. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DONr_west/zarr.json +0 -54
  145. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DOP_west/c/0/0/0 +0 -0
  146. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DOPr_west/c/0/0/0 +0 -0
  147. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DOPr_west/zarr.json +0 -54
  148. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/Fe_west/c/0/0/0 +0 -0
  149. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/Lig_west/c/0/0/0 +0 -0
  150. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/NH4_west/c/0/0/0 +0 -0
  151. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/NO3_west/c/0/0/0 +0 -0
  152. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/NO3_west/zarr.json +0 -54
  153. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/O2_west/c/0/0/0 +0 -0
  154. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/PO4_west/c/0/0/0 +0 -0
  155. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/PO4_west/zarr.json +0 -54
  156. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/SiO3_west/c/0/0/0 +0 -0
  157. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/SiO3_west/zarr.json +0 -54
  158. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatC_west/c/0/0/0 +0 -0
  159. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatChl_west/c/0/0/0 +0 -0
  160. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatFe_west/c/0/0/0 +0 -0
  161. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatP_west/c/0/0/0 +0 -0
  162. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatP_west/zarr.json +0 -54
  163. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatSi_west/c/0/0/0 +0 -0
  164. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatSi_west/zarr.json +0 -54
  165. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazC_west/c/0/0/0 +0 -0
  166. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazC_west/zarr.json +0 -54
  167. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazChl_west/c/0/0/0 +0 -0
  168. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazFe_west/c/0/0/0 +0 -0
  169. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazFe_west/zarr.json +0 -54
  170. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazP_west/c/0/0/0 +0 -0
  171. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazP_west/zarr.json +0 -54
  172. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spC_west/c/0/0/0 +0 -0
  173. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spC_west/zarr.json +0 -54
  174. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spCaCO3_west/c/0/0/0 +0 -0
  175. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spCaCO3_west/zarr.json +0 -54
  176. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spChl_west/c/0/0/0 +0 -0
  177. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spFe_west/c/0/0/0 +0 -0
  178. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spFe_west/zarr.json +0 -54
  179. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spP_west/c/0/0/0 +0 -0
  180. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spP_west/zarr.json +0 -54
  181. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/zooC_west/c/0/0/0 +0 -0
  182. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/zooC_west/zarr.json +0 -54
  183. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/ALK_ALT_CO2_west/c/0/0/0 +0 -0
  184. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/ALK_ALT_CO2_west/zarr.json +0 -54
  185. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/ALK_west/c/0/0/0 +0 -0
  186. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/DIC_ALT_CO2_west/c/0/0/0 +0 -0
  187. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/DIC_ALT_CO2_west/zarr.json +0 -54
  188. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/DIC_west/c/0/0/0 +0 -0
  189. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/DIC_west/zarr.json +0 -54
  190. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/DOC_west/c/0/0/0 +0 -0
  191. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/DOC_west/zarr.json +0 -54
  192. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/DOCr_west/c/0/0/0 +0 -0
  193. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/DOCr_west/zarr.json +0 -54
  194. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/DON_west/c/0/0/0 +0 -0
  195. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/DON_west/zarr.json +0 -54
  196. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/DONr_west/c/0/0/0 +0 -0
  197. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/DONr_west/zarr.json +0 -54
  198. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/DOP_west/c/0/0/0 +0 -0
  199. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/DOP_west/zarr.json +0 -54
  200. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/DOPr_west/c/0/0/0 +0 -0
  201. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/DOPr_west/zarr.json +0 -54
  202. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/Fe_west/c/0/0/0 +0 -0
  203. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/Fe_west/zarr.json +0 -54
  204. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/Lig_west/c/0/0/0 +0 -0
  205. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/Lig_west/zarr.json +0 -54
  206. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/NH4_west/c/0/0/0 +0 -0
  207. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/NH4_west/zarr.json +0 -54
  208. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/NO3_west/c/0/0/0 +0 -0
  209. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/NO3_west/zarr.json +0 -54
  210. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/O2_west/c/0/0/0 +0 -0
  211. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/O2_west/zarr.json +0 -54
  212. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/PO4_west/c/0/0/0 +0 -0
  213. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/PO4_west/zarr.json +0 -54
  214. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/SiO3_west/c/0/0/0 +0 -0
  215. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/SiO3_west/zarr.json +0 -54
  216. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/diatC_west/c/0/0/0 +0 -0
  217. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/diatC_west/zarr.json +0 -54
  218. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/diatChl_west/c/0/0/0 +0 -0
  219. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/diatChl_west/zarr.json +0 -54
  220. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/diatFe_west/c/0/0/0 +0 -0
  221. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/diatP_west/c/0/0/0 +0 -0
  222. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/diatP_west/zarr.json +0 -54
  223. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/diatSi_west/c/0/0/0 +0 -0
  224. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/diatSi_west/zarr.json +0 -54
  225. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/diazC_west/c/0/0/0 +0 -0
  226. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/diazC_west/zarr.json +0 -54
  227. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/diazChl_west/c/0/0/0 +0 -0
  228. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/diazChl_west/zarr.json +0 -54
  229. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/diazFe_west/c/0/0/0 +0 -0
  230. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/diazFe_west/zarr.json +0 -54
  231. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/diazP_west/c/0/0/0 +0 -0
  232. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/diazP_west/zarr.json +0 -54
  233. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/spC_west/c/0/0/0 +0 -0
  234. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/spC_west/zarr.json +0 -54
  235. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/spCaCO3_west/c/0/0/0 +0 -0
  236. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/spCaCO3_west/zarr.json +0 -54
  237. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/spChl_west/c/0/0/0 +0 -0
  238. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/spChl_west/zarr.json +0 -54
  239. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/spFe_west/c/0/0/0 +0 -0
  240. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/spFe_west/zarr.json +0 -54
  241. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/spP_west/c/0/0/0 +0 -0
  242. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/spP_west/zarr.json +0 -54
  243. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/zooC_west/c/0/0/0 +0 -0
  244. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_unified_climatology.zarr/zooC_west/zarr.json +0 -54
  245. {roms_tools-3.3.0.dist-info → roms_tools-3.5.0.dist-info}/licenses/LICENSE +0 -0
  246. {roms_tools-3.3.0.dist-info → roms_tools-3.5.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,854 @@
1
+ import logging
2
+ import re
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime, timedelta
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import numpy as np
9
+ import xarray as xr
10
+
11
+ from roms_tools import Grid
12
+ from roms_tools.datasets.utils import (
13
+ check_dataset,
14
+ convert_to_float64,
15
+ extrapolate_deepest_to_bottom,
16
+ select_relevant_fields,
17
+ select_relevant_times,
18
+ validate_start_end_time,
19
+ )
20
+ from roms_tools.fill import LateralFill
21
+ from roms_tools.utils import (
22
+ get_dask_chunks,
23
+ load_data,
24
+ rotate_velocities,
25
+ wrap_longitudes,
26
+ )
27
+ from roms_tools.vertical_coordinate import (
28
+ compute_depth_coordinates,
29
+ )
30
+
31
+ DEFAULT_NR_BUFFER_POINTS = (
32
+ 20 # Default number of buffer points for subdomain selection.
33
+ )
34
+ # Balances performance and accuracy:
35
+ # - Too many points → more expensive computations
36
+ # - Too few points → potential boundary artifacts when lateral refill is performed
37
+ # See discussion: https://github.com/CWorthy-ocean/roms-tools/issues/153
38
+ # This default will be applied consistently across all datasets requiring lateral fill.
39
+
40
+
41
+ @dataclass(kw_only=True)
42
+ class ROMSDataset:
43
+ """Represents ROMS model output.
44
+
45
+ Parameters
46
+ ----------
47
+ path: str | Path | list[str | Path]
48
+ Filename, or list of filenames with model output.
49
+ grid : Grid
50
+ Object representing the grid information.
51
+ start_time : Optional[datetime], optional
52
+ Start time for selecting relevant data. If not provided, no time-based filtering is applied.
53
+ end_time : Optional[datetime], optional
54
+ End time for selecting relevant data. If not provided, the dataset selects the time entry
55
+ closest to `start_time` within the range `[start_time, start_time + 24 hours)`.
56
+ If `start_time` is also not provided, no time-based filtering is applied.
57
+ allow_flex_time: bool, optional
58
+ Controls how strictly the dataset selects a time entry when `end_time` is not provided (relevant for initial conditions):
59
+
60
+ - If False (default): requires an exact match to `start_time`. Raises a ValueError if no match exists.
61
+ - If True: allows a +24h search window after `start_time` and selects the closest available
62
+ time entry within that window. Raises a ValueError if none are found.
63
+
64
+ Only used when `end_time` is None. Has no effect otherwise.
65
+ dim_names: dict[str, str], optional
66
+ Dictionary specifying the names of dimensions in the dataset.
67
+ var_names: dict[str, str], optional
68
+ Dictionary of variable names that are required in the dataset.
69
+ opt_var_names: dict[str, str], optional
70
+ Dictionary of variable names that are optional in the dataset.
71
+ Defaults to an empty dictionary.
72
+ model_reference_date : datetime, optional
73
+ Reference date of ROMS simulation.
74
+ If not specified, this is inferred from metadata of the model output
75
+ If specified and does not coincide with metadata, a warning is raised.
76
+ adjust_depth_for_sea_surface_height : bool, optional
77
+ Whether to account for sea surface height variations when computing depth coordinates.
78
+ Defaults to `False`.
79
+ use_dask: bool, optional
80
+ Indicates whether to use dask for processing. If True, data is processed with dask; if False, data is processed eagerly. Defaults to False.
81
+ """
82
+
83
+ path: str | Path | list[str | Path]
84
+ """Filename, or list of filenames with model output."""
85
+ grid: Grid
86
+ """Object representing the grid information."""
87
+ start_time: datetime | None = None
88
+ """Start time for selecting relevant data."""
89
+ end_time: datetime | None = None
90
+ """End time for selecting relevant data."""
91
+ allow_flex_time: bool = False
92
+ """Controls how strictly the dataset selects a time entry when `end_time` is not provided."""
93
+ dim_names: dict[str, str] = field(
94
+ default_factory=lambda: {
95
+ "eta_rho": "eta_rho",
96
+ "xi_rho": "xi_rho",
97
+ "time": "time",
98
+ }
99
+ )
100
+ """Dictionary specifying the names of dimensions in the dataset."""
101
+ var_names: dict[str, str] | None = None
102
+ """Dictionary of variable names that are required in the dataset."""
103
+ opt_var_names: dict[str, str] = field(default_factory=dict)
104
+ """Dictionary of variable names that are optional in the dataset."""
105
+ use_dask: bool = False
106
+ """Whether to use dask for processing."""
107
+ model_reference_date: datetime | None = None
108
+ """Reference date of ROMS simulation."""
109
+ adjust_depth_for_sea_surface_height: bool | None = False
110
+ """Whether to account for sea surface height variations when computing depth
111
+ coordinates."""
112
+
113
+ ds: xr.Dataset = field(init=False, repr=False)
114
+ """An xarray Dataset containing the ROMS output."""
115
+
116
+ def __post_init__(self):
117
+ validate_start_end_time(self.start_time, self.end_time)
118
+ ds = self.load_data()
119
+ self._check_consistency_data_grid(ds)
120
+
121
+ self._set_default_var_names(ds)
122
+
123
+ check_dataset(ds, self.dim_names, self.var_names)
124
+ self._check_vertical_coordinate(ds)
125
+ self._infer_model_reference_date_from_metadata(ds)
126
+ ds = self._add_absolute_time(ds)
127
+
128
+ ds = self.select_relevant_fields(ds)
129
+ if self.start_time is not None:
130
+ ds = self.select_relevant_times(ds)
131
+
132
+ ds = self._add_lat_lon_coords_and_masks(ds)
133
+ self.ds = ds
134
+
135
+ # Dataset for depth coordinates
136
+ self.ds_depth_coords = xr.Dataset()
137
+
138
+ def _check_consistency_data_grid(self, ds: xr.Dataset) -> None:
139
+ """
140
+ Ensure that the input dataset `ds` is consistent with the grid dataset.
141
+
142
+ Specifically, checks that the dimensions of the dataset match the grid's
143
+ `eta_rho` and `xi_rho` dimensions.
144
+
145
+ Parameters
146
+ ----------
147
+ ds : xr.Dataset
148
+ The dataset to check against the grid.
149
+
150
+ Raises
151
+ ------
152
+ ValueError
153
+ If the `eta_rho` or `xi_rho` dimensions of `ds` do not match those of `self.grid.ds`.
154
+ """
155
+ eta = self.dim_names["eta_rho"]
156
+ xi = self.dim_names["xi_rho"]
157
+ grid_eta = self.grid.ds.sizes.get(eta)
158
+ grid_xi = self.grid.ds.sizes.get(xi)
159
+ ds_eta = ds.sizes.get(eta)
160
+ ds_xi = ds.sizes.get(xi)
161
+
162
+ if grid_eta != ds_eta or grid_xi != ds_xi:
163
+ raise ValueError(
164
+ f"Inconsistent dataset dimensions: "
165
+ f"grid ({eta}={grid_eta}, {xi}={grid_xi}), "
166
+ f"dataset ({eta}={ds_eta}, {xi}={ds_xi})."
167
+ )
168
+
169
+ def _set_default_var_names(self, ds: xr.Dataset) -> None:
170
+ """
171
+ Ensure ``self.var_names`` is a valid mapping.
172
+
173
+ If ``self.var_names`` is ``None``, it is initialized as an identity
174
+ mapping for all variables in ``ds``.
175
+
176
+ Parameters
177
+ ----------
178
+ ds : xr.Dataset
179
+ Dataset whose variable names are used when creating defaults.
180
+ """
181
+ if self.var_names is None:
182
+ self.var_names = {name: name for name in ds.data_vars}
183
+
184
+ def _get_depth_coordinates(self, depth_type="layer", locations=["rho"]):
185
+ """Ensure depth coordinates are stored for a given location and depth type.
186
+
187
+ Calculates vertical depth coordinates (layer or interface) for specified locations (e.g., rho, u, v points)
188
+ and updates them in the dataset (`self.ds`).
189
+
190
+ Parameters
191
+ ----------
192
+ depth_type : str
193
+ The type of depth coordinate to compute. Valid options:
194
+ - "layer": Compute layer depth coordinates.
195
+ - "interface": Compute interface depth coordinates.
196
+ locations : list[str], optional
197
+ Locations for which to compute depth coordinates. Default is ["rho", "u", "v"].
198
+ Valid options include:
199
+ - "rho": Depth coordinates at rho points.
200
+ - "u": Depth coordinates at u points.
201
+ - "v": Depth coordinates at v points.
202
+
203
+ Updates
204
+ -------
205
+ self.ds_depth_coords : xarray.Dataset
206
+
207
+ Raises
208
+ ------
209
+ ValueError
210
+ If `adjust_depth_for_sea_surface_height` is enabled but `zeta` is missing from `self.ds`.
211
+
212
+ Notes
213
+ -----
214
+ - This method relies on the `compute_depth_coordinates` function to perform calculations.
215
+ - If `adjust_depth_for_sea_surface_height` is `True`, the method accounts for variations
216
+ in sea surface height (`zeta`).
217
+ """
218
+ if self.adjust_depth_for_sea_surface_height:
219
+ if "zeta" not in self.ds:
220
+ raise ValueError(
221
+ "`zeta` is required in provided ROMS output when `adjust_depth_for_sea_surface_height` is enabled."
222
+ )
223
+ zeta = self.ds.zeta
224
+ else:
225
+ zeta = 0
226
+
227
+ for location in locations:
228
+ var_name = f"{depth_type}_depth_{location}"
229
+ if var_name not in self.ds_depth_coords:
230
+ depth_da = compute_depth_coordinates(
231
+ self.grid.ds, zeta, depth_type, location
232
+ )
233
+ depth_da = depth_da.assign_coords(
234
+ {
235
+ f"lat_{location}": self.grid.ds[f"lat_{location}"],
236
+ f"lon_{location}": self.grid.ds[f"lon_{location}"],
237
+ }
238
+ )
239
+ self.ds_depth_coords[var_name] = depth_da
240
+
241
+ def load_data(self) -> xr.Dataset:
242
+ """Load the ROMS data."""
243
+ ds = load_data(
244
+ filename=self.path,
245
+ dim_names={"time": "time"},
246
+ use_dask=self.use_dask,
247
+ decode_times=False,
248
+ decode_timedelta=False,
249
+ time_chunking=True,
250
+ force_combine_nested=True,
251
+ )
252
+
253
+ return ds
254
+
255
+ def select_relevant_fields(self, ds: xr.Dataset) -> xr.Dataset:
256
+ """
257
+ Return a subset of the dataset containing only the required and optional
258
+ variables defined for this object.
259
+
260
+ Variables retained are those listed in ``self.var_names`` and
261
+ ``self.opt_var_names``. Any other data variables are removed, except for
262
+ the special variable ``"mask"``, which is always preserved if present.
263
+
264
+ Parameters
265
+ ----------
266
+ ds : xr.Dataset
267
+ The input dataset from which relevant variables will be selected.
268
+
269
+ Returns
270
+ -------
271
+ xr.Dataset
272
+ A new dataset containing only the required variables specified in
273
+ ``self.var_names`` and the optional variables specified in
274
+ ``self.opt_var_names``, along with ``"mask"`` if present.
275
+ """
276
+ return select_relevant_fields(
277
+ ds,
278
+ [*self.var_names.values(), *self.opt_var_names.values()], # type: ignore
279
+ )
280
+
281
+ def select_relevant_times(self, ds: xr.Dataset) -> xr.Dataset:
282
+ """Select a subset of the dataset based on the specified time range.
283
+
284
+ This method filters the dataset to include all records between `start_time` and `end_time`.
285
+ Additionally, it ensures that one record at or before `start_time` and one record at or
286
+ after `end_time` are included, even if they fall outside the strict time range.
287
+
288
+ If no `end_time` is specified, the method will select the time range of
289
+ [start_time, start_time + 24 hours) and return the closest time entry to `start_time` within that range.
290
+
291
+ Parameters
292
+ ----------
293
+ ds : xr.Dataset
294
+ The input dataset to be filtered. Must contain a time dimension.
295
+
296
+ Returns
297
+ -------
298
+ xr.Dataset
299
+ A dataset filtered to the specified time range, including the closest entries
300
+ at or before `start_time` and at or after `end_time` if applicable.
301
+
302
+ Raises
303
+ ------
304
+ ValueError
305
+ If no matching times are found between `start_time` and `start_time + 24 hours`.
306
+
307
+ Warns
308
+ -----
309
+ UserWarning
310
+ If the dataset contains exactly 12 time steps but the climatology flag is not set.
311
+ This may indicate that the dataset represents climatology data.
312
+
313
+ UserWarning
314
+ If no records at or before `start_time` or no records at or after `end_time` are found.
315
+
316
+ UserWarning
317
+ If the dataset does not contain any time dimension or the time dimension is incorrectly named.
318
+
319
+ Notes
320
+ -----
321
+ - If the `climatology` flag is set and `end_time` is not provided, the method will
322
+ interpolate initial conditions from climatology data.
323
+ - If the dataset uses `cftime` datetime objects, these will be converted to standard
324
+ `np.datetime64` objects before filtering.
325
+ """
326
+ time_dim = self.dim_names["time"]
327
+
328
+ # Ensure start_time is not None for type safety
329
+ if self.start_time is None:
330
+ raise ValueError("select_relevant_times called but start_time is None.")
331
+
332
+ ds = select_relevant_times(
333
+ ds=ds,
334
+ time_dim=time_dim,
335
+ time_coord="time",
336
+ start_time=self.start_time,
337
+ end_time=self.end_time,
338
+ allow_flex_time=self.allow_flex_time,
339
+ )
340
+
341
+ return ds
342
+
343
+ def _infer_model_reference_date_from_metadata(self, ds: xr.Dataset) -> None:
344
+ """Infer and validate the model reference date from `ocean_time` metadata.
345
+
346
+ Parameters
347
+ ----------
348
+ ds : xr.Dataset
349
+ Dataset with an `ocean_time` variable and a `long_name` attribute
350
+ in the format `Time since YYYY/MM/DD`.
351
+
352
+ Raises
353
+ ------
354
+ ValueError
355
+ If `self.model_reference_date` is not set and the reference date cannot
356
+ be inferred, or if the inferred date does not match `self.model_reference_date`.
357
+
358
+ Warns
359
+ -----
360
+ UserWarning
361
+ If `self.model_reference_date` is set but the reference date cannot be inferred.
362
+ """
363
+ # Check if 'long_name' exists in the attributes of 'ocean_time'
364
+ if "long_name" in ds.ocean_time.attrs:
365
+ input_string = ds.ocean_time.attrs["long_name"]
366
+ match = re.search(r"(\d{4})/(\d{2})/(\d{2})", input_string)
367
+
368
+ if match:
369
+ # If a match is found, extract year, month, day and create the inferred date
370
+ year, month, day = map(int, match.groups())
371
+ inferred_date = datetime(year, month, day)
372
+
373
+ if hasattr(self, "model_reference_date") and self.model_reference_date:
374
+ # Check if the inferred date matches the provided model reference date
375
+ if self.model_reference_date != inferred_date:
376
+ raise ValueError(
377
+ f"Mismatch between `self.model_reference_date` ({self.model_reference_date}) "
378
+ f"and inferred reference date ({inferred_date})."
379
+ )
380
+ else:
381
+ # Set the model reference date if not already set
382
+ self.model_reference_date = inferred_date
383
+ else:
384
+ # Handle case where no match is found
385
+ if hasattr(self, "model_reference_date") and self.model_reference_date:
386
+ logging.warning(
387
+ "Could not infer the model reference date from the metadata. "
388
+ "`self.model_reference_date` will be used.",
389
+ )
390
+ else:
391
+ raise ValueError(
392
+ "Model reference date could not be inferred from the metadata, "
393
+ "and `self.model_reference_date` is not set."
394
+ )
395
+ else:
396
+ # Handle case where 'long_name' attribute doesn't exist
397
+ if hasattr(self, "model_reference_date") and self.model_reference_date:
398
+ logging.warning(
399
+ "`long_name` attribute not found in ocean_time. "
400
+ "`self.model_reference_date` will be used instead.",
401
+ )
402
+ else:
403
+ raise ValueError(
404
+ "Model reference date could not be inferred from the metadata, "
405
+ "and `self.model_reference_date` is not set."
406
+ )
407
+
408
+ def _check_vertical_coordinate(self, ds: xr.Dataset) -> None:
409
+ """Check that the vertical coordinate parameters in the dataset are consistent
410
+ with the model grid.
411
+
412
+ This method compares the vertical coordinate parameters (`theta_s`, `theta_b`, `hc`, `Cs_r`, `Cs_w`) in
413
+ the provided dataset (`ds`) with those in the model grid (`self.grid`). The first three parameters are
414
+ checked for exact equality, while the last two are checked for numerical closeness.
415
+
416
+ Parameters
417
+ ----------
418
+ ds : xarray.Dataset
419
+ The dataset containing vertical coordinate parameters in its attributes, such as `theta_s`, `theta_b`,
420
+ `hc`, `Cs_r`, and `Cs_w`.
421
+
422
+ Raises
423
+ ------
424
+ ValueError
425
+ If the vertical coordinate parameters do not match the expected values (based on exact or approximate equality).
426
+
427
+ Notes
428
+ -----
429
+ - Missing attributes trigger a warning instead of an exception.
430
+ - `theta_s`, `theta_b`, and `hc` are checked for exact equality using `np.array_equal`.
431
+ - `Cs_r` and `Cs_w` are checked for numerical closeness using `np.allclose`.
432
+ """
433
+ required_exact = ["theta_s", "theta_b", "hc"]
434
+ required_close = ["Cs_r", "Cs_w"]
435
+
436
+ # Check exact equality
437
+ for param in required_exact:
438
+ value = ds.attrs.get(param, None)
439
+ if value is None:
440
+ logging.warning(
441
+ f"Dataset is missing attribute '{param}'. Skipping this check."
442
+ )
443
+ continue
444
+ if not np.array_equal(getattr(self.grid, param), value):
445
+ raise ValueError(
446
+ f"{param} from grid ({getattr(self.grid, param)}) does not match dataset ({value})."
447
+ )
448
+
449
+ # Check numerical closeness
450
+ for param in required_close:
451
+ value = ds.attrs.get(param, None)
452
+ if value is None:
453
+ logging.warning(
454
+ f"Dataset is missing attribute '{param}'. Skipping this check."
455
+ )
456
+ continue
457
+ grid_value = getattr(self.grid.ds, param)
458
+ if not np.allclose(grid_value, value):
459
+ raise ValueError(
460
+ f"{param} from grid ({grid_value}) is not close to dataset ({value})."
461
+ )
462
+
463
+ def _add_absolute_time(self, ds: xr.Dataset) -> xr.Dataset:
464
+ """Add absolute time as a coordinate to the dataset.
465
+
466
+ Parameters
467
+ ----------
468
+ ds : xarray.Dataset
469
+ Dataset containing "ocean_time" in seconds since the model reference date.
470
+
471
+ Returns
472
+ -------
473
+ xarray.Dataset
474
+ Dataset with absolute time added.
475
+ """
476
+ if self.model_reference_date is None:
477
+ raise ValueError(
478
+ "`model_reference_date` must be set before computing absolute time."
479
+ )
480
+
481
+ ocean_time_seconds = ds["ocean_time"].values
482
+
483
+ abs_time = np.array(
484
+ [
485
+ self.model_reference_date + timedelta(seconds=seconds)
486
+ for seconds in ocean_time_seconds
487
+ ]
488
+ )
489
+
490
+ abs_time = xr.DataArray(
491
+ abs_time, dims=["time"], coords={"time": ds["ocean_time"]}
492
+ )
493
+ abs_time.attrs["long_name"] = "absolute time"
494
+ ds = ds.assign_coords({"abs_time": abs_time})
495
+ ds = ds.drop_vars("time")
496
+ ds = ds.set_index(time="abs_time")
497
+
498
+ return ds
499
+
500
+ def _add_lat_lon_coords_and_masks(self, ds: xr.Dataset) -> xr.Dataset:
501
+ """
502
+ Attach horizontal coordinate fields (lat/lon) and grid masks to a dataset.
503
+
504
+ This method augments the input dataset with the appropriate geographic
505
+ coordinates taken from the grid object. It *always* adds `lat_rho` and
506
+ `lon_rho`. If the dataset contains staggered horizontal dimensions
507
+ (`xi_u` or `eta_v`), the corresponding `u`- or `v`-point coordinates are
508
+ added as well (`lat_u`, `lon_u`, `lat_v`, `lon_v`).
509
+
510
+ In addition, the grid masks (`mask_rho`, `mask_u`, `mask_v`) are copied
511
+ into the dataset for later use in operations such as lateral filling.
512
+
513
+ Parameters
514
+ ----------
515
+ ds : xarray.Dataset
516
+ Dataset to be augmented with horizontal coordinates and masks.
517
+
518
+ Returns
519
+ -------
520
+ xarray.Dataset
521
+ A new dataset with the appropriate latitude/longitude coordinates
522
+ and grid masks assigned based on the dataset's horizontal staggering.
523
+
524
+ Notes
525
+ -----
526
+ This routine does not modify the input dataset in place; a new dataset
527
+ with added coordinates and mask variables is returned.
528
+ """
529
+ coords_to_add = {
530
+ "lat_rho": self.grid.ds["lat_rho"],
531
+ "lon_rho": self.grid.ds["lon_rho"],
532
+ }
533
+ vars_to_add = {"mask_rho": self.grid.ds["mask_rho"]}
534
+
535
+ if "xi_u" in ds.dims:
536
+ coords_to_add.update(
537
+ {"lat_u": self.grid.ds["lat_u"], "lon_u": self.grid.ds["lon_u"]}
538
+ )
539
+ vars_to_add.update({"mask_u": self.grid.ds["mask_u"]})
540
+ if "eta_v" in ds.dims:
541
+ coords_to_add.update(
542
+ {"lat_v": self.grid.ds["lat_v"], "lon_v": self.grid.ds["lon_v"]}
543
+ )
544
+ vars_to_add.update({"mask_v": self.grid.ds["mask_v"]})
545
+
546
+ ds = ds.assign_coords(coords_to_add)
547
+ for mask_name, mask_data in vars_to_add.items():
548
+ ds[mask_name] = mask_data
549
+
550
+ return ds
551
+
552
+ def choose_subdomain(
553
+ self,
554
+ target_coords: dict[str, Any],
555
+ buffer_points: int = DEFAULT_NR_BUFFER_POINTS,
556
+ ) -> None:
557
+ """Selects a subdomain from the xarray Dataset based on specified target
558
+ coordinates, extending the selection by a defined buffer. Adjusts longitude
559
+ ranges as necessary to accommodate the dataset's expected range and handles
560
+ potential discontinuities.
561
+
562
+ Parameters
563
+ ----------
564
+ target_coords : dict
565
+ A dictionary containing the target latitude and longitude coordinates, typically
566
+ with keys "lat", "lon", and "straddle".
567
+ buffer_points : int
568
+ The number of grid points to extend beyond the specified latitude and longitude
569
+ ranges when selecting the subdomain. Defaults to 20.
570
+
571
+ Returns
572
+ -------
573
+ None
574
+ The subdomain of the xarray Dataset is assigned to `self.ds`.
575
+
576
+ Raises
577
+ ------
578
+ ValueError
579
+ If the selected latitude or longitude range does not intersect with the dataset.
580
+ """
581
+ subdomain = choose_subdomain(
582
+ self.ds, self.grid.ds, target_coords, buffer_points
583
+ )
584
+ self.ds = subdomain
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
+
592
+ def convert_to_float64(self) -> None:
593
+ """Convert all data variables in the dataset to float64.
594
+
595
+ This method updates the dataset by converting all of its data variables to the
596
+ `float64` data type, ensuring consistency for numerical operations that require
597
+ high precision. The dataset is modified in place.
598
+
599
+ Parameters
600
+ ----------
601
+ None
602
+
603
+ Returns
604
+ -------
605
+ None
606
+ This method modifies the dataset in place and does not return anything.
607
+ """
608
+ ds = convert_to_float64(self.ds)
609
+ self.ds = ds
610
+
611
+ return None
612
+
613
+ def extrapolate_deepest_to_bottom(self):
614
+ """Extrapolate deepest non-NaN values to fill bottom NaNs along the s (depth)
615
+ dimension.
616
+
617
+ For each variable with a depth dimension, fills missing values at the bottom by
618
+ propagating the deepest available data downward.
619
+ """
620
+ self.ds = extrapolate_deepest_to_bottom(self.ds, "s_rho")
621
+
622
+ def apply_lateral_fill(self) -> None:
623
+ """Apply lateral fill to variables using available masks and grid dimensions.
624
+
625
+ Lateral fill is applied only when:
626
+ - A corresponding mask exists in the dataset, and
627
+ - At least one variable is defined on the associated horizontal grid.
628
+
629
+ Raises
630
+ ------
631
+ ValueError
632
+ If variables exist on a horizontal grid (rho, u, or v) but the
633
+ corresponding mask is missing.
634
+ """
635
+ # Mapping of horizontal dims to required mask name
636
+ dim_to_mask: dict[tuple[str, str], str] = {
637
+ ("eta_rho", "xi_rho"): "mask_rho",
638
+ ("eta_rho", "xi_u"): "mask_u",
639
+ ("eta_v", "xi_rho"): "mask_v",
640
+ }
641
+ horiz_dim_order = ("eta_rho", "eta_v", "xi_rho", "xi_u")
642
+
643
+ # Identify which horizontal dim sets are actually used
644
+ used_dim_sets: set[tuple[str, str]] = set()
645
+
646
+ for var in self.ds.data_vars.values():
647
+ horiz_dims = tuple(d for d in horiz_dim_order if d in var.dims)
648
+ if len(horiz_dims) == 2:
649
+ used_dim_sets.add(horiz_dims)
650
+
651
+ # Enforce required masks for all grids (rho, u, v)
652
+ for dims, mask_name in dim_to_mask.items():
653
+ if dims in used_dim_sets and mask_name not in self.ds:
654
+ raise ValueError(
655
+ f"Variable(s) found on grid {tuple(dims)}, but required mask "
656
+ f"'{mask_name}' is missing from the dataset."
657
+ )
658
+
659
+ # Build lateral fillers
660
+ lateral_fillers: dict[tuple[str, str], LateralFill] = {
661
+ dims: LateralFill(xr.where(self.ds[mask_name] == 1, True, False), dims)
662
+ for dims, mask_name in dim_to_mask.items()
663
+ if dims in used_dim_sets
664
+ }
665
+
666
+ # Apply lateral fill
667
+ for var_name, var in self.ds.data_vars.items():
668
+ if var_name.startswith("mask"):
669
+ continue
670
+
671
+ # Keep dims in canonical order
672
+ var_horiz_dims = tuple(d for d in horiz_dim_order if d in var.dims)
673
+ if len(var_horiz_dims) == 2:
674
+ filler = lateral_fillers.get(var_horiz_dims)
675
+ if filler is not None:
676
+ self.ds[var_name] = filler.apply(var)
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
+
717
+
718
+ def choose_subdomain(
719
+ ds: xr.Dataset,
720
+ ds_grid: xr.Dataset,
721
+ target_coords: dict[str, Any],
722
+ buffer_points: int = DEFAULT_NR_BUFFER_POINTS,
723
+ ):
724
+ """Selects a subdomain from the xarray Dataset based on specified target
725
+ coordinates, extending the selection by a defined buffer. Adjusts longitude
726
+ ranges as necessary to accommodate the dataset's expected range and handles
727
+ potential discontinuities.
728
+
729
+ Parameters
730
+ ----------
731
+ ds : xr.Dataset
732
+ The full ROMS xarray Dataset to subset.
733
+ ds_grid: xr.Dataset
734
+ Dataset containing the grid coordinates, in particular `pm` and `pn`.
735
+ target_coords : dict
736
+ A dictionary containing the target latitude and longitude coordinates, typically
737
+ with keys "lat", "lon", and "straddle".
738
+ buffer_points : int
739
+ The number of grid points to extend beyond the specified latitude and longitude
740
+ ranges when selecting the subdomain. Defaults to 20.
741
+
742
+ Returns
743
+ -------
744
+ xr.Dataset
745
+ Returns the subset of the original dataset.
746
+
747
+ Raises
748
+ ------
749
+ ValueError
750
+ If the selected latitude or longitude range does not intersect with the dataset.
751
+ """
752
+ # Extract lat/lon min/max from target
753
+ lat_min = target_coords["lat"].min().values
754
+ lat_max = target_coords["lat"].max().values
755
+ lon_min = target_coords["lon"].min().values
756
+ lon_max = target_coords["lon"].max().values
757
+
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"])
784
+
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
+ )
803
+
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
+ )
818
+
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
+ )
826
+
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
+ )
834
+
835
+ return ds
836
+
837
+
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.
843
+
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}"
849
+
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
+ )