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,487 @@
1
+ import logging
2
+ from datetime import datetime, timedelta
3
+
4
+ import cftime
5
+ import numpy as np
6
+ import xarray as xr
7
+
8
+ from roms_tools.fill import one_dim_fill
9
+ from roms_tools.utils import interpolate_from_climatology
10
+
11
+
12
+ def extrapolate_deepest_to_bottom(ds: xr.Dataset, depth_dim: str) -> xr.Dataset:
13
+ """Extrapolate the deepest non-NaN values downward along a depth dimension.
14
+
15
+ For each variable in the dataset that includes the specified depth dimension,
16
+ missing values at the bottom are filled by propagating the deepest available
17
+ data downward.
18
+
19
+ Parameters
20
+ ----------
21
+ ds : xr.Dataset
22
+ Dataset containing variables with a depth dimension.
23
+ depth_dim : str
24
+ Name of the depth dimension (e.g., 's_rho') along which to extrapolate.
25
+
26
+ Returns
27
+ -------
28
+ xr.Dataset
29
+ Dataset with bottom NaNs filled along the specified depth dimension.
30
+ """
31
+ for var_name in ds.data_vars:
32
+ if depth_dim in ds[var_name].dims:
33
+ ds[var_name] = one_dim_fill(ds[var_name], depth_dim, direction="forward")
34
+
35
+ return ds
36
+
37
+
38
+ def convert_to_float64(ds: xr.Dataset) -> xr.Dataset:
39
+ """Convert all data variables in the dataset to float64.
40
+
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.
54
+ """
55
+ dtype_map = {
56
+ name: ("float64" if not name.startswith("mask_") else var.dtype)
57
+ for name, var in ds.data_vars.items()
58
+ }
59
+
60
+ return ds.astype(dtype_map)
61
+
62
+
63
+ def check_dataset(
64
+ ds: xr.Dataset,
65
+ dim_names: dict[str, str] | None = None,
66
+ var_names: dict[str, str] | None = None,
67
+ opt_var_names: dict[str, str] | None = None,
68
+ ) -> None:
69
+ """Check if the dataset contains the specified variables and dimensions.
70
+
71
+ Parameters
72
+ ----------
73
+ ds : xr.Dataset
74
+ The xarray Dataset to check.
75
+ dim_names: dict[str, str], optional
76
+ Dictionary specifying the names of dimensions in the dataset.
77
+ var_names: dict[str, str], optional
78
+ Dictionary of variable names that are required in the dataset.
79
+ opt_var_names : dict[str, str], optional
80
+ Dictionary of optional variable names.
81
+ These variables are not strictly required, and the function will not raise an error if they are missing.
82
+ Default is None, meaning no optional variables are considered.
83
+
84
+
85
+ Raises
86
+ ------
87
+ ValueError
88
+ If the dataset does not contain the specified variables or dimensions.
89
+ """
90
+ if dim_names:
91
+ missing_dims = [dim for dim in dim_names.values() if dim not in ds.dims]
92
+ if missing_dims:
93
+ raise ValueError(
94
+ f"Dataset does not contain all required dimensions. The following dimensions are missing: {missing_dims}"
95
+ )
96
+
97
+ if var_names:
98
+ missing_vars = [var for var in var_names.values() if var not in ds.data_vars]
99
+ if missing_vars:
100
+ raise ValueError(
101
+ f"Dataset does not contain all required variables. The following variables are missing: {missing_vars}"
102
+ )
103
+
104
+ if opt_var_names:
105
+ missing_optional_vars = [
106
+ var for var in opt_var_names.values() if var not in ds.data_vars
107
+ ]
108
+ if missing_optional_vars:
109
+ logging.warning(
110
+ f"Optional variables missing (but not critical): {missing_optional_vars}"
111
+ )
112
+
113
+
114
+ def validate_start_end_time(
115
+ start_time: datetime | None = None, end_time: datetime | None = None
116
+ ) -> None:
117
+ """
118
+ Validate the provided start and end times.
119
+
120
+ Parameters
121
+ ----------
122
+ start_time : datetime or None
123
+ Start of the time interval. Must be a `datetime` object if provided.
124
+ end_time : datetime or None
125
+ End of the time interval. Must be a `datetime` object if provided.
126
+
127
+ Raises
128
+ ------
129
+ TypeError
130
+ If `start_time` or `end_time` is provided but is not a `datetime`.
131
+ ValueError
132
+ If both `start_time` and `end_time` are provided and
133
+ `end_time` occurs before `start_time`.
134
+ """
135
+ if start_time is not None and not isinstance(start_time, datetime):
136
+ raise TypeError(
137
+ f"`start_time` must be a datetime object or None, "
138
+ f"but got {type(start_time).__name__}."
139
+ )
140
+
141
+ if end_time is not None and not isinstance(end_time, datetime):
142
+ raise TypeError(
143
+ f"`end_time` must be a datetime object or None, "
144
+ f"but got {type(end_time).__name__}."
145
+ )
146
+
147
+ if start_time is not None and end_time is not None:
148
+ if end_time < start_time:
149
+ raise ValueError(
150
+ f"`end_time` ({end_time}) cannot be earlier than "
151
+ f"`start_time` ({start_time})."
152
+ )
153
+
154
+
155
+ def select_relevant_fields(ds: xr.Dataset, var_names: list[str]) -> xr.Dataset:
156
+ """
157
+ Return a subset of the dataset containing only the specified variables.
158
+
159
+ All data variables not listed in ``var_names`` are removed, except for the
160
+ special variable ``"mask"``, which is always retained if present.
161
+
162
+ Parameters
163
+ ----------
164
+ ds : xr.Dataset
165
+ The input dataset from which variables will be selected.
166
+ var_names : list of str
167
+ Names of variables that should be kept in the resulting dataset.
168
+
169
+ Returns
170
+ -------
171
+ xr.Dataset
172
+ A new dataset containing only the variables in ``var_names`` and
173
+ ``"mask"`` (if it exists in the input dataset).
174
+ """
175
+ vars_to_keep = set(var_names)
176
+ vars_to_drop = [
177
+ var for var in ds.data_vars if var not in vars_to_keep and var != "mask"
178
+ ]
179
+
180
+ if vars_to_drop:
181
+ ds = ds.drop_vars(vars_to_drop)
182
+
183
+ return ds
184
+
185
+
186
+ def select_relevant_times(
187
+ ds: xr.Dataset,
188
+ time_dim: str,
189
+ time_coord: str,
190
+ start_time: datetime,
191
+ end_time: datetime | None = None,
192
+ climatology: bool = False,
193
+ allow_flex_time: bool = False,
194
+ ) -> xr.Dataset:
195
+ """
196
+ Select a subset of the dataset based on time constraints.
197
+
198
+ This function supports two main use cases:
199
+
200
+ 1. **Time range selection (start_time + end_time provided):**
201
+ - Returns all records strictly between `start_time` and `end_time`.
202
+ - Ensures at least one record at or before `start_time` and one record at or
203
+ after `end_time` are included, even if they fall outside the strict range.
204
+
205
+ 2. **Initial condition selection (start_time provided, end_time=None):**
206
+ - Delegates to `_select_initial_time`, which reduces the dataset to exactly one
207
+ time entry.
208
+ - If `allow_flex_time=True`, a +24-hour buffer around `start_time` is allowed,
209
+ and the closest timestamp is chosen.
210
+ - If `allow_flex_time=False`, requires an exact timestamp match.
211
+
212
+ Additional behavior:
213
+ - If `climatology=True`, the dataset must contain exactly 12 time steps. If valid,
214
+ the climatology dataset is returned without further filtering.
215
+ - If the dataset uses `cftime` datetime objects, these are converted to
216
+ `np.datetime64` before filtering.
217
+
218
+ Parameters
219
+ ----------
220
+ ds : xr.Dataset
221
+ The dataset to filter. Must contain a valid time dimension.
222
+ time_dim : str
223
+ Name of the time dimension in `ds`.
224
+ time_coord : str
225
+ Name of the time coordinate in `ds`.
226
+ start_time : datetime
227
+ Start time for filtering.
228
+ end_time : datetime or None
229
+ End time for filtering. If `None`, the function assumes an initial condition
230
+ use case and selects exactly one timestamp.
231
+ climatology : bool, optional
232
+ If True, requires exactly 12 time steps and bypasses normal filtering.
233
+ Defaults to False.
234
+ allow_flex_time : bool, optional
235
+ Whether to allow a +24h search window after `start_time` when `end_time`
236
+ is None. If False (default), requires an exact match.
237
+
238
+ Returns
239
+ -------
240
+ xr.Dataset
241
+ A filtered dataset containing only the selected time entries.
242
+
243
+ Raises
244
+ ------
245
+ ValueError
246
+ - If `climatology=True` but the dataset does not contain exactly 12 time steps.
247
+ - If `climatology=False` and the dataset contains integer time values.
248
+ - If no valid records are found within the requested range or window.
249
+
250
+ Warns
251
+ -----
252
+ UserWarning
253
+ - If no records exist at or before `start_time` or at or after `end_time`.
254
+ - If the specified time dimension does not exist in the dataset.
255
+
256
+ Notes
257
+ -----
258
+ - For initial conditions (end_time=None), see `_select_initial_time` for details
259
+ on strict vs. flexible selection behavior.
260
+ - Logs warnings instead of failing hard when boundary records are missing, and
261
+ defaults to using the earliest or latest available time in such cases.
262
+ """
263
+ if time_dim not in ds.dims:
264
+ logging.warning(
265
+ f"Dataset does not contain time dimension '{time_dim}'. "
266
+ "Please check variable naming or dataset structure."
267
+ )
268
+ return ds
269
+
270
+ if time_coord not in ds.variables:
271
+ logging.warning(
272
+ f"Dataset does not contain time coordinate '{time_coord}'. "
273
+ "Please check variable naming or dataset structure."
274
+ )
275
+ return ds
276
+
277
+ time_type = get_time_type(ds[time_coord])
278
+
279
+ if climatology:
280
+ if len(ds[time_coord]) != 12:
281
+ raise ValueError(
282
+ f"The dataset contains {len(ds[time_coord])} time steps, but the climatology flag is set to True, which requires exactly 12 time steps."
283
+ )
284
+ else:
285
+ if time_type == "int":
286
+ raise ValueError(
287
+ "The dataset contains integer time values, which are only supported when the climatology flag is set to True. However, your climatology flag is set to False."
288
+ )
289
+ if time_type == "cftime":
290
+ ds = ds.assign_coords({time_dim: convert_cftime_to_datetime(ds[time_coord])})
291
+
292
+ if not end_time:
293
+ # Assume we are looking for exactly one time record for initial conditions
294
+ return _select_initial_time(
295
+ ds, time_dim, time_coord, start_time, climatology, allow_flex_time
296
+ )
297
+
298
+ if climatology:
299
+ return ds
300
+
301
+ # Identify records before or at start_time
302
+ before_start = ds[time_coord] <= np.datetime64(start_time)
303
+ if before_start.any():
304
+ closest_before_start = ds[time_coord].where(before_start, drop=True)[-1]
305
+ else:
306
+ logging.warning(f"No records found at or before the start_time: {start_time}.")
307
+ closest_before_start = ds[time_coord][0]
308
+
309
+ # Identify records after or at end_time
310
+ after_end = ds[time_coord] >= np.datetime64(end_time)
311
+ if after_end.any():
312
+ closest_after_end = ds[time_coord].where(after_end, drop=True).min()
313
+ else:
314
+ logging.warning(f"No records found at or after the end_time: {end_time}.")
315
+ closest_after_end = ds[time_coord].max()
316
+
317
+ # Select records within the time range and add the closest before/after
318
+ within_range = (ds[time_coord] > np.datetime64(start_time)) & (
319
+ ds[time_coord] < np.datetime64(end_time)
320
+ )
321
+ selected_times = ds[time_coord].where(
322
+ within_range
323
+ | (ds[time_coord] == closest_before_start)
324
+ | (ds[time_coord] == closest_after_end),
325
+ drop=True,
326
+ )
327
+ ds = ds.sel({time_dim: selected_times})
328
+
329
+ return ds
330
+
331
+
332
+ def _select_initial_time(
333
+ ds: xr.Dataset,
334
+ time_dim: str,
335
+ time_coord: str,
336
+ ini_time: datetime,
337
+ climatology: bool,
338
+ allow_flex_time: bool = False,
339
+ ) -> xr.Dataset:
340
+ """Select exactly one initial time from dataset.
341
+
342
+ Parameters
343
+ ----------
344
+ ds : xr.Dataset
345
+ The input dataset with a time dimension.
346
+ time_dim : str
347
+ Name of the time dimension.
348
+ time_coord : str
349
+ Name of the time coordinate.
350
+ ini_time : datetime
351
+ The desired initial time.
352
+ allow_flex_time : bool
353
+ - If True: allow a +24h window and pick the closest available timestamp.
354
+ - If False (default): require an exact match, otherwise raise ValueError.
355
+
356
+ Returns
357
+ -------
358
+ xr.Dataset
359
+ Dataset reduced to exactly one timestamp.
360
+
361
+ Raises
362
+ ------
363
+ ValueError
364
+ If no matching time is found (when `allow_flex_time=False`), or no entries are
365
+ available within the +24h window (when `allow_flex_time=True`).
366
+ """
367
+ if climatology:
368
+ # Convert from timedelta64[ns] to fractional days
369
+ ds["time"] = ds["time"] / np.timedelta64(1, "D")
370
+ # Interpolate from climatology for initial conditions
371
+ return interpolate_from_climatology(ds, time_dim, time_coord, ini_time)
372
+
373
+ if allow_flex_time:
374
+ # Look in time range [ini_time, ini_time + 24h)
375
+ end_time = ini_time + timedelta(days=1)
376
+ times = (np.datetime64(ini_time) <= ds[time_coord]) & (
377
+ ds[time_coord] < np.datetime64(end_time)
378
+ )
379
+
380
+ if np.all(~times):
381
+ raise ValueError(
382
+ f"No time entries found between {ini_time} and {end_time}."
383
+ )
384
+
385
+ ds = ds.where(times, drop=True)
386
+ if ds.sizes[time_dim] > 1:
387
+ # Pick the time closest to start_time
388
+ ds = ds.isel({time_dim: 0})
389
+
390
+ logging.warning(
391
+ f"Selected time entry closest to the specified start_time in +24 hour range: {ds[time_coord].values}"
392
+ )
393
+
394
+ else:
395
+ # Strict match required
396
+ if not (ds[time_coord].values == np.datetime64(ini_time)).any():
397
+ raise ValueError(
398
+ f"No exact match found for initial time {ini_time}. Consider setting allow_flex_time to True."
399
+ )
400
+
401
+ ds = ds.sel({time_coord: np.datetime64(ini_time)})
402
+
403
+ if time_dim not in ds.dims:
404
+ ds = ds.expand_dims(time_dim)
405
+
406
+ return ds
407
+
408
+
409
+ def get_time_type(data_array: xr.DataArray) -> str:
410
+ """Determines the type of time values in the xarray DataArray.
411
+
412
+ Parameters
413
+ ----------
414
+ data_array : xr.DataArray
415
+ The xarray DataArray to be checked for time data types.
416
+
417
+ Returns
418
+ -------
419
+ str
420
+ A string indicating the type of the time data: 'cftime', 'datetime', or 'int'.
421
+
422
+ Raises
423
+ ------
424
+ TypeError
425
+ If the values in the DataArray are not of type numpy.ndarray or list.
426
+ """
427
+ values = data_array.values
428
+
429
+ # 1. numpy datetime64 (any precision)
430
+ if np.issubdtype(values.dtype, np.datetime64):
431
+ return "datetime"
432
+
433
+ # 2. cftime objects
434
+ cftime_types = (
435
+ cftime.DatetimeNoLeap,
436
+ cftime.DatetimeJulian,
437
+ cftime.DatetimeGregorian,
438
+ cftime.Datetime360Day,
439
+ cftime.DatetimeProlepticGregorian,
440
+ )
441
+ if values.dtype == object and any(isinstance(v, cftime_types) for v in values):
442
+ return "cftime"
443
+
444
+ # 3. integer axis
445
+ if np.issubdtype(values.dtype, np.integer):
446
+ return "int"
447
+
448
+ raise ValueError(
449
+ f"Unsupported data type for time values: {values.dtype}. "
450
+ "Expected datetime64, cftime objects, or integer."
451
+ )
452
+
453
+
454
+ def convert_cftime_to_datetime(data_array: np.ndarray) -> np.ndarray:
455
+ """Converts cftime datetime objects to numpy datetime64 objects in a numpy ndarray.
456
+
457
+ Parameters
458
+ ----------
459
+ data_array : np.ndarray
460
+ The numpy ndarray containing cftime datetime objects to be converted.
461
+
462
+ Returns
463
+ -------
464
+ np.ndarray
465
+ The ndarray with cftime datetimes converted to numpy datetime64 objects.
466
+
467
+ Notes
468
+ -----
469
+ This function is intended to be used with numpy ndarrays. If you need to convert
470
+ cftime datetime objects in an xarray.DataArray, please use the appropriate function
471
+ to handle xarray.DataArray conversions.
472
+ """
473
+ # List of cftime datetime types
474
+ cftime_types = (
475
+ cftime.DatetimeNoLeap,
476
+ cftime.DatetimeJulian,
477
+ cftime.DatetimeGregorian,
478
+ )
479
+
480
+ # Define a conversion function for cftime to numpy datetime64
481
+ def convert_datetime(dt):
482
+ if isinstance(dt, cftime_types):
483
+ # Convert to ISO format and then to nanosecond precision
484
+ return np.datetime64(dt.isoformat(), "ns")
485
+ return np.datetime64(dt, "ns")
486
+
487
+ return np.vectorize(convert_datetime)(data_array)
@@ -5,28 +5,53 @@ from scipy import sparse
5
5
 
6
6
 
7
7
  class LateralFill:
8
- def __init__(self, mask, dims, tol=1.0e-4):
9
- """Initializes the LateralFill class, which fills NaN values in a DataArray by
10
- iteratively solving a Poisson equation using a lateral diffusion approach.
8
+ """
9
+ Fill NaN values in a 2D field using an iterative lateral diffusion (Poisson) solver.
10
+
11
+ The fill is performed along two horizontal dimensions. The **order of these
12
+ dimensions is significant** and must match the order of the dimensions of the
13
+ input data passed to :meth:`apply`.
14
+ """
15
+
16
+ def __init__(self, mask: xr.DataArray, dims: tuple[str, str], tol: float = 1.0e-4):
17
+ """
18
+ Initialize a lateral fill operator.
11
19
 
12
20
  Parameters
13
21
  ----------
14
- mask : xarray.DataArray or ndarray of bool
15
- A 2D boolean mask indicating valid points (True) and land points (False).
16
- Boundary points are automatically set to land (True).
17
- dims : list of str
18
- Dimensions along which to perform the lateral fill.
22
+ mask : xarray.DataArray
23
+ A 2D boolean mask defining valid points (True) and masked points (False).
24
+ The mask dimensions **must be ordered consistently with `dims`**.
25
+ dims : tuple of str
26
+ The two horizontal dimensions along which the fill is applied.
27
+ **Order matters** and must match the dimension order of both `mask`
28
+ and the data passed to :meth:`apply` (e.g., ``("eta_rho", "xi_rho")``).
19
29
  tol : float, optional
20
- Tolerance for the iterative solver, determining convergence. Default is 1.0e-4.
30
+ Convergence tolerance for the iterative solver. Default is ``1.0e-4``.
21
31
 
22
32
  Raises
23
33
  ------
34
+ ValueError
35
+ If the mask dimensionality or dimension order is inconsistent with `dims`.
24
36
  NotImplementedError
25
- If the input mask has more than two dimensions, which is not supported by the current implementation.
37
+ If the mask is not two-dimensional.
26
38
  """
27
- if len(mask.shape) > 2:
39
+ # Type check
40
+ if not isinstance(dims, tuple):
41
+ raise TypeError(
42
+ f"LateralFill error: 'dims' must be a tuple of two strings, got {type(dims).__name__}."
43
+ )
44
+ if len(dims) != 2:
45
+ raise ValueError(
46
+ f"LateralFill error: 'dims' must contain exactly two dimension names, got {len(dims)}: {dims}"
47
+ )
48
+
49
+ if len(mask.shape) != 2:
28
50
  raise NotImplementedError("LateralFill currently supports only 2D masks.")
29
51
 
52
+ _check_dimension_match(dims, mask)
53
+
54
+ self.dims = dims
30
55
  self.mask = mask
31
56
 
32
57
  # Ensure the mask is 2D, copy it and set boundary values to True
@@ -35,7 +60,6 @@ class LateralFill:
35
60
  mask[-1, :] = True
36
61
  mask[:, 0] = True
37
62
  mask[:, -1] = True
38
-
39
63
  # Flatten the mask for use in the sparse matrix solver
40
64
  mask_flat = mask.values.flatten()
41
65
 
@@ -45,7 +69,6 @@ class LateralFill:
45
69
  # Use algebraic multigrid solver for solving the Poisson equation with set seed to ensure reproducibility
46
70
  np.random.seed(123089)
47
71
  self.ml = pyamg.smoothed_aggregation_solver(A, max_coarse=10)
48
- self.dims = dims
49
72
  self.tol = tol
50
73
 
51
74
  def apply(self, var):
@@ -63,6 +86,8 @@ class LateralFill:
63
86
  A DataArray with NaN values filled by iterative smoothing, while preserving
64
87
  non-NaN values.
65
88
  """
89
+ _check_dimension_match(self.dims, var)
90
+
66
91
  # Apply fill to anomaly field
67
92
  mean = var.where(self.mask).mean(dim=self.dims, skipna=True)
68
93
  var = var - mean
@@ -73,6 +98,11 @@ class LateralFill:
73
98
  # Initial guess: ocean points take their original values, land points are set to 0
74
99
  x0 = xr.where(self.mask, var, 0)
75
100
 
101
+ if x0.isnull().any():
102
+ raise ValueError(
103
+ "LateralFill error: The fill operation cannot proceed because the input field contains NaNs at grid points marked as valid by the mask."
104
+ )
105
+
76
106
  # Apply the iterative solver using a custom NumPy function
77
107
  var_filled = xr.apply_ufunc(
78
108
  _lateral_fill_np_array,
@@ -298,3 +328,70 @@ def stencil_grid_mod(S, grid, msk, dtype=None, format=None):
298
328
  data[4, i + diags[4]] = 0
299
329
 
300
330
  return sparse.dia_matrix((data, diags), shape=(N_v, N_v)).asformat(format)
331
+
332
+
333
+ def _check_dimension_match(dims: tuple[str, str], da: xr.DataArray):
334
+ """
335
+ Validate that a DataArray contains the required horizontal dimensions
336
+ in the correct order.
337
+
338
+ Parameters
339
+ ----------
340
+ dims : tuple[str, str]
341
+ Names of the two horizontal dimensions, in the required order
342
+ (e.g., ``("eta_rho", "xi_rho")``).
343
+ da : xarray.DataArray
344
+ The DataArray to validate.
345
+
346
+ Raises
347
+ ------
348
+ ValueError
349
+ If the required dimensions are missing, extra, or their order in ``da``
350
+ does not exactly match ``dims``. The error message includes guidance
351
+ on how to reorder the DataArray using ``transpose``.
352
+ """
353
+ # Extract the horizontal dims from the DataArray
354
+ var_horiz_dims = tuple(d for d in da.dims if d in dims)
355
+
356
+ if set(var_horiz_dims) != set(dims):
357
+ raise ValueError(
358
+ "LateralFill error: DataArray does not contain the required horizontal dimensions.\n"
359
+ f" expected dims = {dims}\n"
360
+ f" found dims = {tuple(da.dims)}\n"
361
+ "Ensure the DataArray includes all required dimensions."
362
+ )
363
+
364
+ if var_horiz_dims != dims:
365
+ raise ValueError(
366
+ "LateralFill error: DataArray horizontal dimension order is incorrect.\n"
367
+ f" expected order = {dims}\n"
368
+ f" found order = {var_horiz_dims}\n"
369
+ "Reorder the DataArray before applying LateralFill, e.g.:\n"
370
+ f" var = var.transpose(..., {', '.join(dims)})"
371
+ )
372
+
373
+
374
+ def one_dim_fill(da: xr.DataArray, dim: str, direction="forward") -> xr.DataArray:
375
+ """Fill NaN values in a DataArray along a specified dimension.
376
+
377
+ Parameters
378
+ ----------
379
+ da : xr.DataArray
380
+ The input DataArray with NaN values to be filled, which must include the specified dimension.
381
+ dim : str
382
+ The name of the dimension along which to fill NaN values (e.g., 'depth' or 'time').
383
+ direction : str, optional
384
+ The filling direction; either "forward" to propagate non-NaN values downward or "backward" to propagate them upward.
385
+ Defaults to "forward".
386
+
387
+ Returns
388
+ -------
389
+ xr.DataArray
390
+ A new DataArray with NaN values filled in the specified direction, leaving the original data unchanged.
391
+ """
392
+ if dim in da.dims:
393
+ if direction == "forward":
394
+ return da.ffill(dim=dim)
395
+ elif direction == "backward":
396
+ return da.bfill(dim=dim)
397
+ return da
roms_tools/plot.py CHANGED
@@ -1276,7 +1276,7 @@ def plot_uptake_efficiency(ds: xr.Dataset) -> None:
1276
1276
  ----------
1277
1277
  ds : xarray.Dataset
1278
1278
  Dataset containing the following variables:
1279
- - "abs_time": array of timestamps (datetime-like)
1279
+ - "time": array of timestamps (datetime-like)
1280
1280
  - "cdr_efficiency": uptake efficiency from flux differences
1281
1281
  - "cdr_efficiency_from_delta_diff": uptake efficiency from DIC differences
1282
1282
 
@@ -1289,16 +1289,16 @@ def plot_uptake_efficiency(ds: xr.Dataset) -> None:
1289
1289
  -------
1290
1290
  None
1291
1291
  """
1292
- required_vars = ["abs_time", "cdr_efficiency", "cdr_efficiency_from_delta_diff"]
1292
+ required_vars = ["time", "cdr_efficiency", "cdr_efficiency_from_delta_diff"]
1293
1293
  for var in required_vars:
1294
1294
  if var not in ds or ds[var].size == 0:
1295
1295
  raise ValueError(f"Dataset must contain non-empty variable '{var}'.")
1296
1296
 
1297
- times = ds["abs_time"]
1297
+ times = ds["time"]
1298
1298
 
1299
1299
  # Check for monotonically increasing times
1300
1300
  if not np.all(times[1:] >= times[:-1]):
1301
- raise ValueError("abs_time must be strictly increasing.")
1301
+ raise ValueError("time must be strictly increasing.")
1302
1302
 
1303
1303
  fig, ax = plt.subplots(figsize=(10, 4))
1304
1304