roms-tools 2.4.0__py3-none-any.whl → 2.6.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 (214) hide show
  1. ci/environment-with-xesmf.yml +16 -0
  2. roms_tools/__init__.py +1 -1
  3. roms_tools/analysis/roms_output.py +339 -234
  4. roms_tools/analysis/utils.py +137 -0
  5. roms_tools/plot.py +353 -214
  6. roms_tools/regrid.py +154 -9
  7. roms_tools/setup/boundary_forcing.py +51 -37
  8. roms_tools/setup/datasets.py +129 -74
  9. roms_tools/setup/grid.py +32 -33
  10. roms_tools/setup/initial_conditions.py +30 -37
  11. roms_tools/setup/nesting.py +238 -64
  12. roms_tools/setup/river_forcing.py +256 -86
  13. roms_tools/setup/surface_forcing.py +40 -28
  14. roms_tools/setup/tides.py +10 -13
  15. roms_tools/setup/topography.py +27 -4
  16. roms_tools/setup/utils.py +28 -12
  17. roms_tools/tests/test_analysis/test_roms_output.py +299 -80
  18. roms_tools/tests/test_regrid.py +85 -2
  19. roms_tools/tests/test_setup/test_boundary_forcing.py +63 -0
  20. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/.zattrs +3 -1
  21. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/.zmetadata +3 -1
  22. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/ALK_ALT_CO2_east/0.0.0 +0 -0
  23. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/ALK_ALT_CO2_south/0.0.0 +0 -0
  24. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/ALK_ALT_CO2_west/0.0.0 +0 -0
  25. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/ALK_east/0.0.0 +0 -0
  26. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/ALK_south/0.0.0 +0 -0
  27. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/ALK_west/0.0.0 +0 -0
  28. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DIC_ALT_CO2_east/0.0.0 +0 -0
  29. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DIC_ALT_CO2_south/0.0.0 +0 -0
  30. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DIC_ALT_CO2_west/0.0.0 +0 -0
  31. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DIC_east/0.0.0 +0 -0
  32. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DIC_south/0.0.0 +0 -0
  33. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DIC_west/0.0.0 +0 -0
  34. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DOC_east/0.0.0 +0 -0
  35. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DOC_south/0.0.0 +0 -0
  36. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DOC_west/0.0.0 +0 -0
  37. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DOCr_east/0.0.0 +0 -0
  38. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DOCr_south/0.0.0 +0 -0
  39. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DOCr_west/0.0.0 +0 -0
  40. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DON_east/0.0.0 +0 -0
  41. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DON_south/0.0.0 +0 -0
  42. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DON_west/0.0.0 +0 -0
  43. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DONr_east/0.0.0 +0 -0
  44. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DONr_south/0.0.0 +0 -0
  45. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DONr_west/0.0.0 +0 -0
  46. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DOP_east/0.0.0 +0 -0
  47. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DOP_south/0.0.0 +0 -0
  48. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DOP_west/0.0.0 +0 -0
  49. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DOPr_east/0.0.0 +0 -0
  50. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DOPr_south/0.0.0 +0 -0
  51. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DOPr_west/0.0.0 +0 -0
  52. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/Fe_east/0.0.0 +0 -0
  53. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/Fe_south/0.0.0 +0 -0
  54. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/Fe_west/0.0.0 +0 -0
  55. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/Lig_east/0.0.0 +0 -0
  56. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/Lig_south/0.0.0 +0 -0
  57. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/Lig_west/0.0.0 +0 -0
  58. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/NH4_east/0.0.0 +0 -0
  59. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/NH4_north/0.0.0 +0 -0
  60. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/NH4_south/0.0.0 +0 -0
  61. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/NH4_west/0.0.0 +0 -0
  62. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/NO3_east/0.0.0 +0 -0
  63. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/NO3_south/0.0.0 +0 -0
  64. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/NO3_west/0.0.0 +0 -0
  65. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/O2_east/0.0.0 +0 -0
  66. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/O2_south/0.0.0 +0 -0
  67. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/O2_west/0.0.0 +0 -0
  68. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/PO4_east/0.0.0 +0 -0
  69. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/PO4_south/0.0.0 +0 -0
  70. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/PO4_west/0.0.0 +0 -0
  71. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/SiO3_east/0.0.0 +0 -0
  72. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/SiO3_south/0.0.0 +0 -0
  73. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/SiO3_west/0.0.0 +0 -0
  74. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatC_east/0.0.0 +0 -0
  75. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatC_north/0.0.0 +0 -0
  76. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatC_south/0.0.0 +0 -0
  77. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatC_west/0.0.0 +0 -0
  78. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatChl_east/0.0.0 +0 -0
  79. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatChl_north/0.0.0 +0 -0
  80. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatChl_south/0.0.0 +0 -0
  81. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatChl_west/0.0.0 +0 -0
  82. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatFe_east/0.0.0 +0 -0
  83. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatFe_north/0.0.0 +0 -0
  84. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatFe_south/0.0.0 +0 -0
  85. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatFe_west/0.0.0 +0 -0
  86. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatP_east/0.0.0 +0 -0
  87. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatP_north/0.0.0 +0 -0
  88. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatP_south/0.0.0 +0 -0
  89. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatP_west/0.0.0 +0 -0
  90. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatSi_east/0.0.0 +0 -0
  91. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatSi_north/0.0.0 +0 -0
  92. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatSi_south/0.0.0 +0 -0
  93. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatSi_west/0.0.0 +0 -0
  94. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazC_east/0.0.0 +0 -0
  95. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazC_north/0.0.0 +0 -0
  96. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazC_south/0.0.0 +0 -0
  97. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazC_west/0.0.0 +0 -0
  98. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazChl_east/0.0.0 +0 -0
  99. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazChl_north/0.0.0 +0 -0
  100. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazChl_south/0.0.0 +0 -0
  101. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazChl_west/0.0.0 +0 -0
  102. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazFe_east/0.0.0 +0 -0
  103. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazFe_north/0.0.0 +0 -0
  104. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazFe_south/0.0.0 +0 -0
  105. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazFe_west/0.0.0 +0 -0
  106. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazP_east/0.0.0 +0 -0
  107. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazP_north/0.0.0 +0 -0
  108. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazP_south/0.0.0 +0 -0
  109. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazP_west/0.0.0 +0 -0
  110. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spC_east/0.0.0 +0 -0
  111. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spC_north/0.0.0 +0 -0
  112. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spC_south/0.0.0 +0 -0
  113. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spC_west/0.0.0 +0 -0
  114. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spCaCO3_east/0.0.0 +0 -0
  115. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spCaCO3_north/0.0.0 +0 -0
  116. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spCaCO3_south/0.0.0 +0 -0
  117. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spCaCO3_west/0.0.0 +0 -0
  118. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spChl_east/0.0.0 +0 -0
  119. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spChl_north/0.0.0 +0 -0
  120. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spChl_south/0.0.0 +0 -0
  121. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spChl_west/0.0.0 +0 -0
  122. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spFe_east/0.0.0 +0 -0
  123. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spFe_north/0.0.0 +0 -0
  124. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spFe_south/0.0.0 +0 -0
  125. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spFe_west/0.0.0 +0 -0
  126. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spP_east/0.0.0 +0 -0
  127. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spP_north/0.0.0 +0 -0
  128. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spP_south/0.0.0 +0 -0
  129. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spP_west/0.0.0 +0 -0
  130. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/zooC_east/0.0.0 +0 -0
  131. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/zooC_north/0.0.0 +0 -0
  132. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/zooC_south/0.0.0 +0 -0
  133. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/zooC_west/0.0.0 +0 -0
  134. roms_tools/tests/test_setup/test_data/bgc_surface_forcing.zarr/.zattrs +2 -2
  135. roms_tools/tests/test_setup/test_data/bgc_surface_forcing.zarr/.zmetadata +8 -7
  136. roms_tools/tests/test_setup/test_data/bgc_surface_forcing.zarr/abs_time/.zattrs +1 -0
  137. roms_tools/tests/test_setup/test_data/bgc_surface_forcing.zarr/dust/0.0.0 +0 -0
  138. roms_tools/tests/test_setup/test_data/bgc_surface_forcing.zarr/dust_time/.zattrs +1 -1
  139. roms_tools/tests/test_setup/test_data/bgc_surface_forcing.zarr/iron/0.0.0 +0 -0
  140. roms_tools/tests/test_setup/test_data/bgc_surface_forcing.zarr/iron_time/.zattrs +1 -1
  141. roms_tools/tests/test_setup/test_data/bgc_surface_forcing.zarr/nhy/0.0.0 +0 -0
  142. roms_tools/tests/test_setup/test_data/bgc_surface_forcing.zarr/nhy_time/.zattrs +1 -1
  143. roms_tools/tests/test_setup/test_data/bgc_surface_forcing.zarr/nox/0.0.0 +0 -0
  144. roms_tools/tests/test_setup/test_data/bgc_surface_forcing.zarr/nox_time/.zattrs +1 -1
  145. roms_tools/tests/test_setup/test_data/bgc_surface_forcing.zarr/pco2_air/0.0.0 +0 -0
  146. roms_tools/tests/test_setup/test_data/bgc_surface_forcing.zarr/pco2_air_alt/0.0.0 +0 -0
  147. roms_tools/tests/test_setup/test_data/bgc_surface_forcing.zarr/pco2_time/.zattrs +1 -1
  148. roms_tools/tests/test_setup/test_data/bgc_surface_forcing_from_climatology.zarr/.zattrs +2 -2
  149. roms_tools/tests/test_setup/test_data/bgc_surface_forcing_from_climatology.zarr/.zmetadata +2 -2
  150. roms_tools/tests/test_setup/test_data/bgc_surface_forcing_from_climatology.zarr/dust/0.0.0 +0 -0
  151. roms_tools/tests/test_setup/test_data/bgc_surface_forcing_from_climatology.zarr/iron/0.0.0 +0 -0
  152. roms_tools/tests/test_setup/test_data/bgc_surface_forcing_from_climatology.zarr/nhy/0.0.0 +0 -0
  153. roms_tools/tests/test_setup/test_data/bgc_surface_forcing_from_climatology.zarr/nox/0.0.0 +0 -0
  154. roms_tools/tests/test_setup/test_data/bgc_surface_forcing_from_climatology.zarr/pco2_air/0.0.0 +0 -0
  155. roms_tools/tests/test_setup/test_data/bgc_surface_forcing_from_climatology.zarr/pco2_air_alt/0.0.0 +0 -0
  156. roms_tools/tests/test_setup/test_data/grid.zarr/.zattrs +1 -1
  157. roms_tools/tests/test_setup/test_data/grid.zarr/.zmetadata +2 -2
  158. roms_tools/tests/test_setup/test_data/grid.zarr/angle/0.0 +0 -0
  159. roms_tools/tests/test_setup/test_data/grid.zarr/angle_coarse/0.0 +0 -0
  160. roms_tools/tests/test_setup/test_data/grid.zarr/f/0.0 +0 -0
  161. roms_tools/tests/test_setup/test_data/grid.zarr/h/.zattrs +1 -1
  162. roms_tools/tests/test_setup/test_data/grid.zarr/h/0.0 +0 -0
  163. roms_tools/tests/test_setup/test_data/grid.zarr/lat_coarse/0.0 +0 -0
  164. roms_tools/tests/test_setup/test_data/grid.zarr/lat_rho/0.0 +0 -0
  165. roms_tools/tests/test_setup/test_data/grid.zarr/lat_u/0.0 +0 -0
  166. roms_tools/tests/test_setup/test_data/grid.zarr/lat_v/0.0 +0 -0
  167. roms_tools/tests/test_setup/test_data/grid.zarr/pm/0.0 +0 -0
  168. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/.zattrs +1 -1
  169. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/.zmetadata +1 -1
  170. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/angle/0.0 +0 -0
  171. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/angle_coarse/0.0 +0 -0
  172. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/f/0.0 +0 -0
  173. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/h/0.0 +0 -0
  174. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lat_coarse/0.0 +0 -0
  175. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lat_rho/0.0 +0 -0
  176. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lat_u/0.0 +0 -0
  177. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lat_v/0.0 +0 -0
  178. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lon_coarse/0.0 +0 -0
  179. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lon_rho/0.0 +0 -0
  180. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lon_u/0.0 +0 -0
  181. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/lon_v/0.0 +0 -0
  182. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/pm/0.0 +0 -0
  183. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/pn/0.0 +0 -0
  184. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/.zattrs +1 -1
  185. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/.zmetadata +1 -1
  186. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/salt/0.0.0.0 +0 -0
  187. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/temp/0.0.0.0 +0 -0
  188. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/u/0.0.0.0 +0 -0
  189. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/ubar/0.0.0 +0 -0
  190. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/v/0.0.0.0 +0 -0
  191. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/vbar/0.0.0 +0 -0
  192. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/zeta/0.0.0 +0 -0
  193. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/.zmetadata +27 -1
  194. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/nriver/.zarray +20 -0
  195. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/nriver/.zattrs +6 -0
  196. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/nriver/0 +0 -0
  197. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/river_location/.zattrs +1 -1
  198. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/.zmetadata +27 -1
  199. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/nriver/.zarray +20 -0
  200. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/nriver/.zattrs +6 -0
  201. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/nriver/0 +0 -0
  202. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/river_location/.zattrs +1 -1
  203. roms_tools/tests/test_setup/test_initial_conditions.py +16 -0
  204. roms_tools/tests/test_setup/test_nesting.py +141 -104
  205. roms_tools/tests/test_setup/test_river_forcing.py +580 -266
  206. roms_tools/tests/test_setup/test_surface_forcing.py +47 -0
  207. roms_tools/tests/test_setup/test_validation.py +34 -2
  208. roms_tools/utils.py +11 -7
  209. roms_tools/vertical_coordinate.py +1 -0
  210. {roms_tools-2.4.0.dist-info → roms_tools-2.6.0.dist-info}/METADATA +22 -11
  211. {roms_tools-2.4.0.dist-info → roms_tools-2.6.0.dist-info}/RECORD +214 -206
  212. {roms_tools-2.4.0.dist-info → roms_tools-2.6.0.dist-info}/WHEEL +1 -1
  213. {roms_tools-2.4.0.dist-info → roms_tools-2.6.0.dist-info/licenses}/LICENSE +0 -0
  214. {roms_tools-2.4.0.dist-info → roms_tools-2.6.0.dist-info}/top_level.txt +0 -0
@@ -1,22 +1,23 @@
1
1
  import xarray as xr
2
2
  import numpy as np
3
3
  import matplotlib.pyplot as plt
4
+ from roms_tools.plot import _plot, _section_plot, _profile_plot, _line_plot
4
5
  from roms_tools.utils import _load_data
6
+ from roms_tools.regrid import LateralRegridFromROMS, VerticalRegridFromROMS
5
7
  from dataclasses import dataclass, field
6
8
  from typing import Union, Optional
7
9
  from pathlib import Path
8
- import os
9
10
  import re
10
11
  import logging
11
12
  from datetime import datetime, timedelta
12
13
  from roms_tools import Grid
13
- from roms_tools.plot import _plot, _section_plot, _profile_plot, _line_plot
14
14
  from roms_tools.vertical_coordinate import (
15
15
  compute_depth_coordinates,
16
16
  )
17
+ from roms_tools.analysis.utils import _validate_plot_inputs, _generate_coordinate_range
17
18
 
18
19
 
19
- @dataclass(frozen=True, kw_only=True)
20
+ @dataclass(kw_only=True)
20
21
  class ROMSOutput:
21
22
  """Represents ROMS model output.
22
23
 
@@ -25,41 +26,35 @@ class ROMSOutput:
25
26
  grid : Grid
26
27
  Object representing the grid information.
27
28
  path : Union[str, Path, List[Union[str, Path]]]
28
- Directory, filename, or list of filenames with model output.
29
- type : str
30
- Specifies the type of model output. Options are:
31
-
32
- - "restart": for restart files.
33
- - "average": for time-averaged files.
34
- - "snapshot": for snapshot files.
35
-
29
+ Filename, or list of filenames with model output.
36
30
  model_reference_date : datetime, optional
37
31
  If not specified, this is inferred from metadata of the model output
38
32
  If specified and does not coincide with metadata, a warning is raised.
33
+ adjust_depth_for_sea_surface_height : bool, optional
34
+ Whether to account for sea surface height variations when computing depth coordinates.
35
+ Defaults to `False`.
39
36
  use_dask: bool, optional
40
37
  Indicates whether to use dask for processing. If True, data is processed with dask; if False, data is processed eagerly. Defaults to False.
41
38
  """
42
39
 
43
40
  grid: Grid
44
41
  path: Union[str, Path]
45
- type: Union[str, Path]
46
42
  use_dask: bool = False
47
43
  model_reference_date: Optional[datetime] = None
44
+ adjust_depth_for_sea_surface_height: Optional[bool] = False
48
45
  ds: xr.Dataset = field(init=False, repr=False)
49
46
 
50
47
  def __post_init__(self):
51
- # Validate `type`
52
- if self.type not in {"restart", "average", "snapshot"}:
53
- raise ValueError(
54
- f"Invalid type '{self.type}'. Must be one of 'restart', 'average', or 'snapshot'."
55
- )
56
48
 
57
49
  ds = self._load_model_output()
58
50
  self._infer_model_reference_date_from_metadata(ds)
59
51
  self._check_vertical_coordinate(ds)
60
52
  ds = self._add_absolute_time(ds)
61
53
  ds = self._add_lat_lon_coords(ds)
62
- object.__setattr__(self, "ds", ds)
54
+ self.ds = ds
55
+
56
+ # Dataset for depth coordinates
57
+ self.ds_depth_coords = xr.Dataset()
63
58
 
64
59
  def plot(
65
60
  self,
@@ -68,11 +63,15 @@ class ROMSOutput:
68
63
  s=None,
69
64
  eta=None,
70
65
  xi=None,
66
+ depth=None,
67
+ lat=None,
68
+ lon=None,
69
+ include_boundary=False,
71
70
  depth_contours=False,
72
- layer_contours=False,
73
71
  ax=None,
72
+ save_path=None,
74
73
  ) -> None:
75
- """Plot a ROMS output field for a given vertical (s_rho) or horizontal (eta, xi)
74
+ """Generate a plot of a ROMS output field for a specified vertical or horizontal
76
75
  slice.
77
76
 
78
77
  Parameters
@@ -85,26 +84,56 @@ class ROMSOutput:
85
84
 
86
85
  time : int, optional
87
86
  Index of the time dimension to plot. Default is 0.
87
+
88
88
  s : int, optional
89
- The index of the vertical layer (`s_rho`) to plot. If not specified, the plot
90
- will represent a horizontal slice (eta- or xi- plane). Default is None.
89
+ The index of the vertical layer (`s_rho`) to plot. If specified, the plot
90
+ will display a horizontal slice at that layer. Cannot be used simultaneously
91
+ with `depth`. Default is None.
92
+
91
93
  eta : int, optional
92
- The eta-index to plot. Used for vertical sections or horizontal slices.
93
- Default is None.
94
+ The eta-index to plot. Used for generating vertical sections or plotting
95
+ horizontal slices along a constant eta-coordinate. Cannot be used simultaneously
96
+ with `lat` or `lon`, but can be combined with `xi`. Default is None.
97
+
94
98
  xi : int, optional
95
- The xi-index to plot. Used for vertical sections or horizontal slices.
96
- Default is None.
97
- depth_contours : bool, optional
98
- If True, depth contours will be overlaid on the plot, showing lines of constant
99
- depth. This is typically used for plots that show a single vertical layer.
99
+ The xi-index to plot. Used for generating vertical sections or plotting
100
+ horizontal slices along a constant xi-coordinate. Cannot be used simultaneously
101
+ with `lat` or `lon`, but can be combined with `eta`. Default is None.
102
+
103
+ depth : float, optional
104
+ Depth (in meters) to plot a horizontal slice at a specific depth level.
105
+ If specified, the plot will interpolate the field to the given depth.
106
+ Cannot be used simultaneously with `s` or for fields that are inherently
107
+ 2D (such as "zeta"). Default is None.
108
+
109
+ lat : float, optional
110
+ Latitude (in degrees) to plot a vertical section at a specific
111
+ latitude. This option is useful for generating zonal (west-east)
112
+ sections. Cannot be used simultaneously with `eta` or `xi`, bu can be
113
+ combined with `lon`. Default is None.
114
+
115
+ lon : float, optional
116
+ Longitude (in degrees) to plot a vertical section at a specific
117
+ longitude. This option is useful for generating meridional (south-north) sections.
118
+ Cannot be used simultaneously with `eta` or `xi`, but can be combined
119
+ with `lat`. Default is None.
120
+
121
+ include_boundary : bool, optional
122
+ Whether to include the outermost grid cells along the `eta`- and `xi`-boundaries in the plot.
123
+ In diagnostic ROMS output fields, these boundary cells are set to zero, so excluding them can improve visualization.
100
124
  Default is False.
101
- layer_contours : bool, optional
102
- If True, contour lines representing the boundaries between vertical layers will
103
- be added to the plot. This is particularly useful in vertical sections to
104
- visualize the layering of the water column. For clarity, the number of layer
105
- contours displayed is limited to a maximum of 10. Default is False.
125
+
126
+ depth_contours : bool, optional
127
+ If True, overlays contours representing lines of constant depth on the plot.
128
+ This option is only relevant when the `s` parameter is provided (i.e., not None).
129
+ By default, depth contours are not shown (False).
130
+
106
131
  ax : matplotlib.axes.Axes, optional
107
- The axes to plot on. If None, a new figure is created. Note that this argument does not work for horizontal plots that display the eta- and xi-dimensions at the same time.
132
+ The axes to plot on. If None, a new figure is created. Note that this argument does not work for 2D horizontal plots. Default is None.
133
+
134
+ save_path : str, optional
135
+ Path to save the generated plot. If None, the plot is shown interactively.
136
+ Default is None.
108
137
 
109
138
  Returns
110
139
  -------
@@ -114,44 +143,86 @@ class ROMSOutput:
114
143
  Raises
115
144
  ------
116
145
  ValueError
117
- If the specified `var_name` is not one of the valid options.
118
- If the field specified by `var_name` is 3D and none of `s`, `eta`, or `xi` are specified.
119
- If the field specified by `var_name` is 2D and both `eta` and `xi` are specified.
146
+ - If the specified `var_name` is not one of the valid options.
147
+ - If the field specified by `var_name` is 3D and none of `s`, `eta`, `xi`, `depth`, `lat`, or `lon` are specified.
148
+ - If the field specified by `var_name` is 2D and both `eta` and `xi` or both `lat` and `lon` are specified.
149
+ - If conflicting dimensions are specified (e.g., specifying `eta`/`xi` with `lat`/`lon` or both `s` and `depth`).
150
+ - If more than two dimensions are specified for a 3D field.
151
+ - If `time` exceeds the bounds of the time dimension.
152
+ - If `time` is specified for a field that does not have a time dimension.
153
+ - If `eta` or `xi` indices are out of bounds.
154
+ - If `eta` or `xi` lie on the boundary when `include_boundary=False`.
120
155
  """
121
-
122
- # Input checks
156
+ # Check if variable exists
123
157
  if var_name not in self.ds:
124
- raise ValueError(f"Variable '{var_name}' is not found in dataset.")
158
+ raise ValueError(f"Variable '{var_name}' is not found in the dataset.")
159
+
160
+ # Pick the variable
161
+ field = self.ds[var_name]
125
162
 
126
- if "time" in self.ds[var_name].dims:
127
- if time >= len(self.ds[var_name].time):
163
+ # Check and pick time
164
+ if "time" in field.dims:
165
+ if time >= len(field.time):
128
166
  raise ValueError(
129
167
  f"Invalid time index: The specified time index ({time}) exceeds the maximum index "
130
- f"({len(self.ds[var_name].time) - 1}) for the 'time' dimension in variable '{var_name}'."
168
+ f"({len(field.time) - 1}) for the 'time' dimension."
131
169
  )
132
- field = self.ds[var_name].isel(time=time)
170
+ field = field.isel(time=time)
133
171
  else:
134
172
  if time > 0:
135
173
  raise ValueError(
136
- f"Invalid input: The variable '{var_name}' does not have a 'time' dimension, "
174
+ f"Invalid input: The field does not have a 'time' dimension, "
137
175
  f"but a time index ({time}) greater than 0 was provided."
138
176
  )
139
- field = self.ds[var_name]
140
177
 
141
- if len(field.dims) == 3:
142
- if not any([s is not None, eta is not None, xi is not None]):
143
- raise ValueError(
144
- "Invalid input: For 3D fields, you must specify at least one of the dimensions 's', 'eta', or 'xi'."
145
- )
146
- if all([s is not None, eta is not None, xi is not None]):
147
- raise ValueError(
148
- "Ambiguous input: For 3D fields, specify at most two of 's', 'eta', or 'xi'. Specifying all three is not allowed."
149
- )
178
+ # Input checks
179
+ _validate_plot_inputs(field, s, eta, xi, depth, lat, lon, include_boundary)
180
+
181
+ # Get horizontal dimensions and grid location
182
+ horizontal_dims_dict = {
183
+ "rho": {"eta": "eta_rho", "xi": "xi_rho"},
184
+ "u": {"eta": "eta_rho", "xi": "xi_u"},
185
+ "v": {"eta": "eta_v", "xi": "xi_rho"},
186
+ }
187
+ for loc, horizontal_dims in horizontal_dims_dict.items():
188
+ if all(dim in field.dims for dim in horizontal_dims.values()):
189
+ break
190
+
191
+ # Convert relative to absolute indices
192
+ def _get_absolute_index(idx, field, dim_name):
193
+ index = field[dim_name].isel(**{dim_name: idx}).item()
194
+ return index
195
+
196
+ if eta is not None and eta < 0:
197
+ eta = _get_absolute_index(eta, field, horizontal_dims["eta"])
198
+ if xi is not None and xi < 0:
199
+ xi = _get_absolute_index(xi, field, horizontal_dims["xi"])
200
+ if s is not None and s < 0:
201
+ s = _get_absolute_index(s, field, "s_rho")
202
+
203
+ # Set spatial coordinates
204
+ lat_deg = self.grid.ds[f"lat_{loc}"]
205
+ lon_deg = self.grid.ds[f"lon_{loc}"]
206
+ if self.grid.straddle:
207
+ lon_deg = xr.where(lon_deg > 180, lon_deg - 360, lon_deg)
208
+ field = field.assign_coords({"lon": lon_deg, "lat": lat_deg})
150
209
 
151
- if len(field.dims) == 2 and all([eta is not None, xi is not None]):
152
- raise ValueError(
153
- "Conflicting input: For 2D fields, specify only one dimension, either 'eta' or 'xi', not both."
154
- )
210
+ # Mask the field
211
+ mask = self.grid.ds[f"mask_{loc}"]
212
+ field = field.where(mask)
213
+
214
+ # Assign eta and xi as coordinates
215
+ coords_to_assign = {dim: field[dim] for dim in horizontal_dims.values()}
216
+ field = field.assign_coords(**coords_to_assign)
217
+
218
+ # Remove horizontal boundary if desired
219
+ slice_dict = {
220
+ "rho": {"eta_rho": slice(1, -1), "xi_rho": slice(1, -1)},
221
+ "u": {"eta_rho": slice(1, -1), "xi_u": slice(1, -1)},
222
+ "v": {"eta_v": slice(1, -1), "xi_rho": slice(1, -1)},
223
+ }
224
+ if not include_boundary:
225
+ field = field.isel(**slice_dict[loc])
155
226
 
156
227
  # Load the data
157
228
  if self.use_dask:
@@ -160,106 +231,156 @@ class ROMSOutput:
160
231
  with ProgressBar():
161
232
  field.load()
162
233
 
163
- # Get correct mask and spatial coordinates
164
- if all(dim in field.dims for dim in ["eta_rho", "xi_rho"]):
165
- loc = "rho"
166
- elif all(dim in field.dims for dim in ["eta_rho", "xi_u"]):
167
- loc = "u"
168
- elif all(dim in field.dims for dim in ["eta_v", "xi_rho"]):
169
- loc = "v"
170
- else:
171
- ValueError("provided field does not have two horizontal dimension")
172
-
173
- mask = self.grid.ds[f"mask_{loc}"]
174
- lat_deg = self.grid.ds[f"lat_{loc}"]
175
- lon_deg = self.grid.ds[f"lon_{loc}"]
176
-
177
- if self.grid.straddle:
178
- lon_deg = xr.where(lon_deg > 180, lon_deg - 360, lon_deg)
234
+ # Compute layer depth for 3D fields when depth contours are requested or no vertical layer is specified.
235
+ compute_layer_depth = len(field.dims) > 2 and (depth_contours or s is None)
236
+ if compute_layer_depth:
237
+ if eta is not None or xi is not None:
238
+ # Computing depth coordinates directly for the slice in question is more efficient
239
+ # than using .ds_depth_coords, which computes depth coordinates for full field
240
+ if self.adjust_depth_for_sea_surface_height:
241
+ zeta = self.ds.zeta.isel(time=time)
242
+ else:
243
+ zeta = 0
244
+ if compute_layer_depth:
245
+ layer_depth = compute_depth_coordinates(
246
+ self.grid.ds,
247
+ zeta,
248
+ depth_type="layer",
249
+ location=loc,
250
+ eta=eta,
251
+ xi=xi,
252
+ )
253
+ else:
254
+ self._get_depth_coordinates(depth_type="layer", locations=[loc])
255
+ layer_depth = self.ds_depth_coords[f"layer_depth_{loc}"]
256
+ if self.adjust_depth_for_sea_surface_height:
257
+ layer_depth = layer_depth.isel(time=time)
258
+
259
+ if not include_boundary:
260
+ # Apply valid slices only for dimensions that exist in layer_depth.dims
261
+ layer_depth = layer_depth.isel(
262
+ **{
263
+ dim: s
264
+ for dim, s in slice_dict.get(loc, {}).items()
265
+ if dim in layer_depth.dims
266
+ }
267
+ )
268
+ layer_depth.load()
179
269
 
180
- field = field.assign_coords({"lon": lon_deg, "lat": lat_deg})
270
+ # Prepare figure title
271
+ formatted_time = np.datetime_as_string(field.abs_time.values, unit="m")
272
+ title = f"time: {formatted_time}"
181
273
 
182
- # Retrieve depth coordinates
183
- compute_layer_depth = (depth_contours or s is None) and len(field.dims) > 2
184
- compute_interface_depth = layer_contours and s is None
274
+ # Slice the field horizontally as desired
275
+ def _slice_along_dimension(field, title, dim_name, idx):
276
+ field = field.sel(**{dim_name: idx})
277
+ title = title + f", {dim_name} = {idx}"
278
+ return field, title
185
279
 
186
- if compute_layer_depth:
187
- layer_depth = compute_depth_coordinates(
188
- self.grid.ds,
189
- self.ds.zeta.isel(time=time),
190
- depth_type="layer",
191
- location=loc,
192
- eta=eta,
193
- xi=xi,
280
+ if eta is not None:
281
+ field, title = _slice_along_dimension(
282
+ field, title, horizontal_dims["eta"], eta
194
283
  )
195
- if s is not None:
196
- layer_depth = layer_depth.isel(s_rho=s)
197
- if compute_interface_depth:
198
- interface_depth = compute_depth_coordinates(
199
- self.grid.ds,
200
- self.ds.zeta.isel(time=time),
201
- depth_type="interface",
202
- location=loc,
203
- eta=eta,
204
- xi=xi,
284
+ if xi is not None:
285
+ field, title = _slice_along_dimension(
286
+ field, title, horizontal_dims["xi"], xi
205
287
  )
206
- if s is not None:
207
- interface_depth = interface_depth.isel(s_w=s)
208
-
209
- # Slice the field as desired
210
- title = field.long_name
211
288
  if s is not None:
212
- title = title + f", s_rho = {field.s_rho[s].item()}"
213
- field = field.isel(s_rho=s)
289
+ field, title = _slice_along_dimension(field, title, "s_rho", s)
290
+ if compute_layer_depth:
291
+ layer_depth = layer_depth.isel(s_rho=s)
214
292
  else:
215
293
  depth_contours = False
216
294
 
217
- def _process_dimension(field, mask, dim_name, dim_values, idx, title):
218
- if dim_name in field.dims:
219
- title = title + f", {dim_name} = {dim_values[idx].item()}"
220
- field = field.isel(**{dim_name: idx})
221
- mask = mask.isel(**{dim_name: idx})
295
+ # Regrid laterally
296
+ if lat is not None or lon is not None:
297
+
298
+ if lat is not None:
299
+ lats = [lat]
300
+ title = title + f", lat = {lat}°N"
222
301
  else:
223
- raise ValueError(
224
- f"None of the expected dimensions ({dim_name}) found in field."
302
+ resolution = self._infer_nominal_horizontal_resolution()
303
+ lats = _generate_coordinate_range(
304
+ field.lat.min().values, field.lat.max().values, resolution
225
305
  )
226
- return field, mask, title
227
-
228
- if eta is not None:
229
- field, mask, title = _process_dimension(
230
- field,
231
- mask,
232
- "eta_rho" if "eta_rho" in field.dims else "eta_v",
233
- field.eta_rho if "eta_rho" in field.dims else field.eta_v,
234
- eta,
235
- title,
236
- )
306
+ lats = xr.DataArray(lats, dims=["lat"], attrs={"units": "°N"})
237
307
 
238
- if xi is not None:
239
- field, mask, title = _process_dimension(
240
- field,
241
- mask,
242
- "xi_rho" if "xi_rho" in field.dims else "xi_u",
243
- field.xi_rho if "xi_rho" in field.dims else field.xi_u,
244
- xi,
245
- title,
246
- )
308
+ if lon is not None:
309
+ lons = [lon]
310
+ title = title + f", lon = {lon}°E"
311
+ else:
312
+ resolution = self._infer_nominal_horizontal_resolution(lat)
313
+ lons = _generate_coordinate_range(
314
+ field.lon.min().values, field.lon.max().values, resolution
315
+ )
316
+ lons = xr.DataArray(lons, dims=["lon"], attrs={"units": "°E"})
247
317
 
248
- # Format to exclude seconds
249
- formatted_time = np.datetime_as_string(field.abs_time.values, unit="m")
250
- title = title + f", time: {formatted_time}"
318
+ target_coords = {"lat": lats, "lon": lons}
319
+ lateral_regrid = LateralRegridFromROMS(field, target_coords)
320
+ field = lateral_regrid.apply(field).squeeze()
321
+ if compute_layer_depth:
322
+ layer_depth = lateral_regrid.apply(layer_depth).squeeze()
251
323
 
324
+ # Assign depth as coordinate
252
325
  if compute_layer_depth:
253
326
  field = field.assign_coords({"layer_depth": layer_depth})
254
327
 
328
+ def _remove_edge_nans(field, xdim, layer_depth=None):
329
+ """Removes NaNs from the edges along the specified dimension."""
330
+ if xdim in field.dims:
331
+ if layer_depth is not None:
332
+ nan_mask = layer_depth.isnull().sum(
333
+ dim=[dim for dim in layer_depth.dims if dim != xdim]
334
+ )
335
+ else:
336
+ nan_mask = field.isnull().sum(
337
+ dim=[dim for dim in field.dims if dim != xdim]
338
+ )
339
+
340
+ # Find the valid indices where the sum of the nans is 0
341
+ valid_indices = np.where(nan_mask.values == 0)[0]
342
+
343
+ if len(valid_indices) > 0:
344
+ first_valid = valid_indices[0]
345
+ last_valid = valid_indices[-1]
346
+
347
+ field = field.isel({xdim: slice(first_valid, last_valid + 1)})
348
+ if layer_depth is not None:
349
+ layer_depth = layer_depth.isel(
350
+ {xdim: slice(first_valid, last_valid + 1)}
351
+ )
352
+
353
+ return field, layer_depth
354
+
355
+ if lat is not None:
356
+ field, layer_depth = _remove_edge_nans(
357
+ field, "lon", layer_depth if "layer_depth" in locals() else None
358
+ )
359
+ if lon is not None:
360
+ field, layer_depth = _remove_edge_nans(
361
+ field, "lat", layer_depth if "layer_depth" in locals() else None
362
+ )
363
+
364
+ # Regrid vertically
365
+ if depth is not None:
366
+ vertical_regrid = VerticalRegridFromROMS(self.ds)
367
+ # Save attributes before vertical regridding
368
+ attrs = field.attrs
369
+ field = vertical_regrid.apply(
370
+ field, layer_depth, np.array([depth])
371
+ ).squeeze()
372
+ # Reset attributes
373
+ field.attrs = attrs
374
+ title = title + f", depth = {depth}m"
375
+
255
376
  # Choose colorbar
256
377
  if var_name in ["u", "v", "w", "ubar", "vbar", "zeta"]:
257
- vmax = max(field.where(mask).max().values, -field.where(mask).min().values)
378
+ vmax = max(field.max().values, -field.min().values)
258
379
  vmin = -vmax
259
380
  cmap = plt.colormaps.get_cmap("RdBu_r")
260
381
  else:
261
- vmax = field.where(mask).max().values
262
- vmin = field.where(mask).min().values
382
+ vmax = field.max().values
383
+ vmin = field.min().values
263
384
  if var_name in ["temp", "salt"]:
264
385
  cmap = plt.colormaps.get_cmap("YlOrRd")
265
386
  else:
@@ -268,40 +389,34 @@ class ROMSOutput:
268
389
  kwargs = {"vmax": vmax, "vmin": vmin, "cmap": cmap}
269
390
 
270
391
  # Plotting
271
- if eta is None and xi is None:
272
- _plot(
273
- field=field.where(mask),
392
+ if (eta is None and xi is None) and (lat is None and lon is None):
393
+ fig = _plot(
394
+ field=field,
274
395
  depth_contours=depth_contours,
275
396
  title=title,
276
397
  kwargs=kwargs,
277
- c="g",
398
+ c=None,
278
399
  )
279
400
  else:
280
401
  if len(field.dims) == 2:
281
- if not layer_contours:
282
- interface_depth = None
283
- else:
284
- # restrict number of layer_contours to 10 for the sake of plot clearity
285
- nr_layers = len(interface_depth["s_w"])
286
- selected_layers = np.linspace(
287
- 0, nr_layers - 1, min(nr_layers, 10), dtype=int
288
- )
289
- interface_depth = interface_depth.isel(s_w=selected_layers)
290
- _section_plot(
291
- field.where(mask),
292
- interface_depth=interface_depth,
402
+ fig = _section_plot(
403
+ field,
404
+ interface_depth=None,
293
405
  title=title,
294
406
  kwargs=kwargs,
295
407
  ax=ax,
296
408
  )
297
409
  else:
298
410
  if "s_rho" in field.dims:
299
- _profile_plot(field.where(mask), title=title, ax=ax)
411
+ fig = _profile_plot(field, title=title, ax=ax)
300
412
  else:
301
- _line_plot(field.where(mask), title=title, ax=ax)
413
+ fig = _line_plot(field, title=title, ax=ax)
414
+
415
+ if save_path:
416
+ plt.savefig(save_path, dpi=300, bbox_inches="tight")
302
417
 
303
- def compute_depth_coordinates(self, depth_type="layer", locations=["rho"]):
304
- """Compute and update vertical depth coordinates.
418
+ def _get_depth_coordinates(self, depth_type="layer", locations=["rho"]):
419
+ """Ensure depth coordinates are stored for a given location and depth type.
305
420
 
306
421
  Calculates vertical depth coordinates (layer or interface) for specified locations (e.g., rho, u, v points)
307
422
  and updates them in the dataset (`self.ds`).
@@ -321,61 +436,44 @@ class ROMSOutput:
321
436
 
322
437
  Updates
323
438
  -------
324
- self.ds : xarray.Dataset
325
- The dataset (`self.ds`) is updated with the following depth coordinate variables:
326
- - f"{depth_type}_depth_rho": Depth coordinates at rho points.
327
- - f"{depth_type}_depth_u": Depth coordinates at u points (if included in `locations`).
328
- - f"{depth_type}_depth_v": Depth coordinates at v points (if included in `locations`).
439
+ self.ds_depth_coords : xarray.Dataset
440
+
441
+ Raises
442
+ ------
443
+ ValueError
444
+ If `adjust_depth_for_sea_surface_height` is enabled but `zeta` is missing from `self.ds`.
329
445
 
330
446
  Notes
331
447
  -----
332
- This method uses the `compute_and_update_depth_coordinates` function to perform calculations and updates.
448
+ - This method relies on the `compute_depth_coordinates` function to perform calculations.
449
+ - If `adjust_depth_for_sea_surface_height` is `True`, the method accounts for variations
450
+ in sea surface height (`zeta`).
333
451
  """
334
452
 
335
- for location in locations:
336
- self.ds[f"{depth_type}_depth_{location}"] = compute_depth_coordinates(
337
- self.grid.ds, self.ds.zeta, depth_type, location
338
- )
339
-
340
- def _load_model_output(self) -> xr.Dataset:
341
- """Load the model output based on the type."""
342
- if isinstance(self.path, list):
343
- filetype = "list"
344
- force_combine_nested = True
345
- # Check if all items in the list are files
346
- if not all(Path(item).is_file() for item in self.path):
347
- raise FileNotFoundError(
348
- "All items in the provided list must be valid files."
453
+ if self.adjust_depth_for_sea_surface_height:
454
+ if "zeta" not in self.ds:
455
+ raise ValueError(
456
+ "`zeta` is required in provided ROMS output when `adjust_depth_for_sea_surface_height` is enabled."
349
457
  )
350
- elif Path(self.path).is_file():
351
- filetype = "file"
352
- force_combine_nested = False
353
- elif Path(self.path).is_dir():
354
- filetype = "dir"
355
- force_combine_nested = True
458
+ zeta = self.ds.zeta
356
459
  else:
357
- raise FileNotFoundError(
358
- f"The specified path '{self.path}' is neither a file, nor a list of files, nor a directory."
359
- )
460
+ zeta = 0
360
461
 
361
- time_chunking = True
362
- if self.type == "restart":
363
- time_chunking = False
364
- filename = _validate_and_set_filenames(self.path, filetype, "rst")
365
- elif self.type == "average":
366
- filename = _validate_and_set_filenames(self.path, filetype, "avg")
367
- elif self.type == "snapshot":
368
- filename = _validate_and_set_filenames(self.path, filetype, "his")
369
- else:
370
- raise ValueError(f"Unsupported type '{self.type}'.")
462
+ for location in locations:
463
+ self.ds_depth_coords[
464
+ f"{depth_type}_depth_{location}"
465
+ ] = compute_depth_coordinates(self.grid.ds, zeta, depth_type, location)
466
+
467
+ def _load_model_output(self) -> xr.Dataset:
468
+ """Load the model output."""
371
469
 
372
470
  # Load the dataset
373
471
  ds = _load_data(
374
- filename,
472
+ self.path,
375
473
  dim_names={"time": "time"},
376
474
  use_dask=self.use_dask,
377
- time_chunking=time_chunking,
378
- force_combine_nested=force_combine_nested,
475
+ time_chunking=True,
476
+ force_combine_nested=True,
379
477
  )
380
478
 
381
479
  return ds
@@ -419,7 +517,7 @@ class ROMSOutput:
419
517
  )
420
518
  else:
421
519
  # Set the model reference date if not already set
422
- object.__setattr__(self, "model_reference_date", inferred_date)
520
+ self.model_reference_date = inferred_date
423
521
  else:
424
522
  # Handle case where no match is found
425
523
  if hasattr(self, "model_reference_date") and self.model_reference_date:
@@ -552,39 +650,46 @@ class ROMSOutput:
552
650
 
553
651
  return ds
554
652
 
653
+ def _infer_nominal_horizontal_resolution(self, lat=None):
654
+ """Estimate the nominal horizontal resolution of the grid in degrees at a
655
+ specified latitude.
555
656
 
556
- def _validate_and_set_filenames(
557
- filenames: Union[str, list], filetype: str, string: str
558
- ) -> Union[str, list]:
559
- """Validates and adjusts the filename or list of filenames based on the specified
560
- type and checks for the presence of a string in the filename.
657
+ This method calculates the nominal horizontal resolution of the grid by first
658
+ determining the average grid spacing in meters. The spacing is then converted
659
+ to degrees, accounting for the Earth's curvature, and the latitude where the
660
+ resolution is being computed.
561
661
 
562
- Parameters
563
- ----------
564
- filenames : Union[str, list]
565
- A single filename (str), a list of filenames, or a directory path.
566
- filetype : str
567
- The type of input: 'file' for a single file, 'list' for a list of files, or 'dir' for a directory.
568
- string : str
569
- The string that should be present in each filename.
570
-
571
- Returns
572
- -------
573
- Union[str, list]
574
- The validated filename(s). If a directory is provided, the function returns the adjusted file pattern.
575
- """
576
- if filetype == "file":
577
- if string not in os.path.basename(filenames):
578
- logging.warning(
579
- f"The file '{filenames}' does not appear to contain '{string}' in the name."
580
- )
581
- elif filetype == "list":
582
- for file in filenames:
583
- if string not in os.path.basename(file):
584
- logging.warning(
585
- f"The file '{file}' does not appear to contain '{string}' in the name."
586
- )
587
- elif filetype == "dir":
588
- filenames = os.path.join(filenames, f"*{string}.*.nc")
662
+ Parameters
663
+ ----------
664
+ lat : float, optional
665
+ Latitude (in degrees) at which to estimate the horizontal resolution.
666
+ If not provided, the resolution is calculated at the average latitude of
667
+ the grid (`lat_rho`).
668
+
669
+ Returns
670
+ -------
671
+ float
672
+ The estimated horizontal resolution in degrees, adjusted for the Earth's curvature.
673
+ """
674
+ # Earth radius in meters
675
+ r_earth = 6371315.0
676
+
677
+ if lat is None:
678
+ # Center latitude in degrees
679
+ lat = (self.grid.ds.lat_rho.max() + self.grid.ds.lat_rho.min()) / 2
680
+
681
+ # Convert latitude to radians
682
+ lat_rad = np.deg2rad(lat)
683
+
684
+ # Mean resolution in meters
685
+ resolution_in_m = (
686
+ (1 / self.grid.ds.pm).mean() + (1 / self.grid.ds.pn).mean()
687
+ ) / 2
688
+
689
+ # Meters per degree at the equator
690
+ meters_per_degree = 2 * np.pi * r_earth / 360
691
+
692
+ # Correct for latitude by multiplying by cos(latitude) for longitude
693
+ resolution_in_degrees = resolution_in_m / (meters_per_degree * np.cos(lat_rad))
589
694
 
590
- return filenames
695
+ return resolution_in_degrees