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
@@ -4,9 +4,10 @@ import logging
4
4
  from dataclasses import dataclass, field
5
5
  import cartopy.crs as ccrs
6
6
  from datetime import datetime
7
- from typing import Dict, Union, List
7
+ from typing import Dict, Union, List, Optional
8
8
  from pathlib import Path
9
9
  import matplotlib.pyplot as plt
10
+ import matplotlib.cm as cm
10
11
  from roms_tools import Grid
11
12
  from roms_tools.plot import _get_projection, _add_field_to_ax
12
13
  from roms_tools.utils import save_datasets
@@ -22,7 +23,7 @@ from roms_tools.setup.utils import (
22
23
  )
23
24
 
24
25
 
25
- @dataclass(frozen=True, kw_only=True)
26
+ @dataclass(kw_only=True)
26
27
  class RiverForcing:
27
28
  """Represents river forcing input data for ROMS.
28
29
 
@@ -57,6 +58,24 @@ class RiverForcing:
57
58
  Whether to include BGC tracers. Defaults to `False`.
58
59
  model_reference_date : datetime, optional
59
60
  Reference date for the model. Default is January 1, 2000.
61
+ indices : dict[str, list[tuple]], optional
62
+ A dictionary specifying the river indices for each river to be included in the river forcing. This parameter is optional. If not provided,
63
+ the river indices will be automatically determined based on the grid and the source dataset. If provided, it allows for explicit specification
64
+ of river locations. The dictionary structure consists of river names as keys, and each value is a list of tuples. Each tuple represents
65
+ a pair of indices corresponding to the `eta_rho` and `xi_rho` grid coordinates of the river.
66
+
67
+ Example:
68
+ indices = {
69
+ 'Hvita(Olfusa)': [(8, 6), (7, 6)],
70
+ 'Thjorsa': [(8, 6)],
71
+ 'JkulsFjll': [(11, 12)],
72
+ 'Lagarfljot': [(9, 13), (8, 13), (10, 13)],
73
+ 'Bruara': [(8, 6)],
74
+ 'Svarta': [(12, 9)]
75
+ }
76
+
77
+ In the example, the dictionary provides the river names as keys, and the values are lists of tuples, where each tuple represents the
78
+ `(eta_rho, xi_rho)` indices for a river location.
60
79
 
61
80
  Attributes
62
81
  ----------
@@ -64,6 +83,9 @@ class RiverForcing:
64
83
  The xarray Dataset containing the river forcing data.
65
84
  climatology : bool
66
85
  Indicates whether the final river forcing is climatological.
86
+ Dict[str, Union[int, List[int]]]
87
+ A dictionary of river indices. If not provided during initialization, it will be automatically determined
88
+ based on the grid and the source dataset. The dictionary structure is the same as described in the `indices` parameter docstring.
67
89
  """
68
90
 
69
91
  grid: Grid
@@ -73,44 +95,51 @@ class RiverForcing:
73
95
  convert_to_climatology: str = "if_any_missing"
74
96
  include_bgc: bool = False
75
97
  model_reference_date: datetime = datetime(2000, 1, 1)
98
+ indices: Optional[Dict[str, Dict[str, Union[int, List[int]]]]] = None
76
99
 
77
100
  ds: xr.Dataset = field(init=False, repr=False)
78
101
  climatology: xr.Dataset = field(init=False, repr=False)
79
102
 
80
103
  def __post_init__(self):
81
104
  self._input_checks()
82
- target_coords = get_target_coords(self.grid)
83
- # maximum dx in grid
84
- dx = (
85
- np.sqrt((1 / self.grid.ds.pm) ** 2 + (1 / self.grid.ds.pn) ** 2) / 2
86
- ).max()
87
-
88
105
  data = self._get_data()
89
106
 
90
- original_indices = data.extract_relevant_rivers(target_coords, dx)
91
- object.__setattr__(self, "original_indices", original_indices)
107
+ if self.indices is None:
108
+ logging.info(
109
+ "No river indices provided. Identify all rivers within the ROMS domain and assign each of them to the nearest coastal point."
110
+ )
111
+ target_coords = get_target_coords(self.grid)
112
+ # maximum dx in grid
113
+ dx = (
114
+ np.sqrt((1 / self.grid.ds.pm) ** 2 + (1 / self.grid.ds.pn) ** 2) / 2
115
+ ).max()
116
+ original_indices = data.extract_relevant_rivers(target_coords, dx)
117
+ if len(original_indices) == 0:
118
+ raise ValueError(
119
+ "No relevant rivers found. Consider increasing domain size or using a different river dataset."
120
+ )
121
+ self.original_indices = original_indices
122
+ updated_indices = self._move_rivers_to_closest_coast(target_coords, data)
123
+ self.indices = updated_indices
92
124
 
93
- if len(original_indices["station"]) > 0:
94
- ds = self._create_river_forcing(data)
95
- self._move_rivers_to_closest_coast(target_coords, data)
96
- ds = self._write_indices_into_dataset(ds)
97
- self._validate(ds)
125
+ else:
126
+ logging.info("Use provided river indices.")
127
+ self.original_indices = self.indices
128
+ check_river_locations_are_along_coast(self.grid.ds.mask_rho, self.indices)
129
+ data.extract_named_rivers(self.indices)
98
130
 
99
- for var_name in ds.data_vars:
100
- ds[var_name] = substitute_nans_by_fillvalue(
101
- ds[var_name], fill_value=0.0
102
- )
131
+ ds = self._create_river_forcing(data)
132
+ ds = self._write_indices_into_dataset(ds)
133
+ self._validate(ds)
103
134
 
104
- object.__setattr__(self, "ds", ds)
135
+ for var_name in ds.data_vars:
136
+ ds[var_name] = substitute_nans_by_fillvalue(ds[var_name], fill_value=0.0)
105
137
 
106
- else:
107
- raise ValueError(
108
- "No relevant rivers found. Consider increasing domain size or using a different river dataset."
109
- )
138
+ self.ds = ds
110
139
 
111
140
  def _input_checks(self):
112
141
  if self.source is None:
113
- object.__setattr__(self, "source", {"name": "DAI"})
142
+ self.source = {"name": "DAI"}
114
143
 
115
144
  if "name" not in self.source:
116
145
  raise ValueError("`source` must include a 'name'.")
@@ -119,11 +148,67 @@ class RiverForcing:
119
148
  raise ValueError("`source` must include a 'path'.")
120
149
 
121
150
  # Set 'climatology' to False if not provided in 'source'
122
- object.__setattr__(
123
- self,
124
- "source",
125
- {**self.source, "climatology": self.source.get("climatology", False)},
126
- )
151
+ self.source = {
152
+ **self.source,
153
+ "climatology": self.source.get("climatology", False),
154
+ }
155
+
156
+ # Check if 'indices' is provided and has the correct format
157
+ if self.indices is not None:
158
+ if not isinstance(self.indices, dict):
159
+ raise ValueError("`indices` must be a dictionary.")
160
+
161
+ # Ensure the dictionary contains at least one river
162
+ if len(self.indices) == 0:
163
+ raise ValueError(
164
+ "The provided 'indices' dictionary must contain at least one river."
165
+ )
166
+
167
+ for river_name, river_data in self.indices.items():
168
+ if not isinstance(river_name, str):
169
+ raise ValueError(f"River name `{river_name}` must be a string.")
170
+
171
+ if not isinstance(river_data, list):
172
+ raise ValueError(
173
+ f"Data for river `{river_name}` must be a list of tuples."
174
+ )
175
+
176
+ # Ensure each element in the list is a tuple of length 2
177
+ seen_tuples = set()
178
+ for idx_pair in river_data:
179
+ if not isinstance(idx_pair, tuple) or len(idx_pair) != 2:
180
+ raise ValueError(
181
+ f"Each item for river `{river_name}` must be a tuple of length 2 representing (eta_rho, xi_rho)."
182
+ )
183
+
184
+ eta_rho, xi_rho = idx_pair
185
+
186
+ # Ensure both eta_rho and xi_rho are integers
187
+ if not isinstance(eta_rho, int):
188
+ raise ValueError(
189
+ f"First element of tuple for river `{river_name}` must be an integer (eta_rho), but got {type(eta_rho)}."
190
+ )
191
+ if not isinstance(xi_rho, int):
192
+ raise ValueError(
193
+ f"Second element of tuple for river `{river_name}` must be an integer (xi_rho), but got {type(xi_rho)}."
194
+ )
195
+
196
+ # Check that eta_rho and xi_rho are within the valid range
197
+ if not (0 <= eta_rho < len(self.grid.ds.eta_rho)):
198
+ raise ValueError(
199
+ f"Value of eta_rho for river `{river_name}` ({eta_rho}) is out of valid range [0, {len(self.grid.ds.eta_rho)-1}]."
200
+ )
201
+ if not (0 <= xi_rho < len(self.grid.ds.xi_rho)):
202
+ raise ValueError(
203
+ f"Value of xi_rho for river `{river_name}` ({xi_rho}) is out of valid range [0, {len(self.grid.ds.xi_rho)-1}]."
204
+ )
205
+
206
+ # Check for duplicate tuples
207
+ if idx_pair in seen_tuples:
208
+ raise ValueError(
209
+ f"Duplicate location {idx_pair} found for river `{river_name}`."
210
+ )
211
+ seen_tuples.add(idx_pair)
127
212
 
128
213
  def _get_data(self):
129
214
 
@@ -169,23 +254,23 @@ class RiverForcing:
169
254
  - `river_tracer`: A `DataArray` representing tracer data for temperature, salinity and BGC tracers (if specified) for each river over time.
170
255
  """
171
256
  if self.source["climatology"]:
172
- object.__setattr__(self, "climatology", True)
257
+ self.climatology = True
173
258
  else:
174
259
  if self.convert_to_climatology in ["never", "if_any_missing"]:
175
260
  data_ds = data.select_relevant_times(data.ds)
176
261
  if self.convert_to_climatology == "if_any_missing":
177
262
  if data_ds[data.var_names["flux"]].isnull().any():
178
263
  data.compute_climatology()
179
- object.__setattr__(self, "climatology", True)
264
+ self.climatology = True
180
265
  else:
181
- object.__setattr__(data, "ds", data_ds)
182
- object.__setattr__(self, "climatology", False)
266
+ data.ds = data_ds
267
+ self.climatology = False
183
268
  else:
184
- object.__setattr__(data, "ds", data_ds)
185
- object.__setattr__(self, "climatology", False)
269
+ data.ds = data_ds
270
+ self.climatology = False
186
271
  elif self.convert_to_climatology == "always":
187
272
  data.compute_climatology()
188
- object.__setattr__(self, "climatology", True)
273
+ self.climatology = True
189
274
 
190
275
  ds = xr.Dataset()
191
276
 
@@ -197,13 +282,19 @@ class RiverForcing:
197
282
  river_volume = river_volume.rename(
198
283
  {data.dim_names["time"]: "river_time", data.dim_names["station"]: "nriver"}
199
284
  )
285
+
200
286
  name = data.ds[data.var_names["name"]].rename(
201
287
  {data.dim_names["station"]: "nriver"}
202
288
  )
203
289
  name.attrs["long_name"] = "River name"
204
290
  river_volume.coords["river_name"] = name
291
+
205
292
  ds["river_volume"] = river_volume
206
293
 
294
+ nriver = xr.DataArray(np.arange(1, len(ds.nriver) + 1), dims="nriver")
295
+ nriver.attrs["long_name"] = "River ID (1-based Fortran indexing)"
296
+ ds = ds.assign_coords({"nriver": nriver})
297
+
207
298
  if self.include_bgc:
208
299
  ntracers = 2 + 32
209
300
  else:
@@ -272,15 +363,13 @@ class RiverForcing:
272
363
 
273
364
  ds = ds.assign_coords({"river_time": time})
274
365
 
275
- ds = ds.drop_vars("nriver")
276
-
277
366
  return ds
278
367
 
279
368
  def _move_rivers_to_closest_coast(self, target_coords, data):
280
369
  """Move river mouths to the closest coastal grid cell.
281
370
 
282
371
  This method computes the closest coastal grid point to each river mouth
283
- based on geographical distance.
372
+ based on geographical distance. It identifies the nearest grid point on the coast and returns the updated river mouth indices.
284
373
 
285
374
  Parameters:
286
375
  -----------
@@ -296,11 +385,11 @@ class RiverForcing:
296
385
  - `var_names`: A dictionary of variable names in the dataset (e.g., longitude, latitude, station names).
297
386
  - `dim_names`: A dictionary containing dimension names for the dataset (e.g., "station", "eta_rho", "xi_rho").
298
387
 
299
- Returns:
300
- --------
301
- None
302
- This method modifies the `self.updated_indices` attribute and writes the updated indices
303
- of the river mouths to the grid file using `write_indices_into_grid_file`.
388
+ Returns
389
+ -------
390
+ indices : dict[str, list[tuple]]
391
+ A dictionary consisting of river names as keys, and each value is a list of tuples. Each tuple represents
392
+ a pair of indices corresponding to the `eta_rho` and `xi_rho` grid coordinates of the river.
304
393
  """
305
394
 
306
395
  # Retrieve longitude and latitude of river mouths
@@ -333,27 +422,29 @@ class RiverForcing:
333
422
 
334
423
  # Find the indices of the closest coastal grid cell to the river mouth
335
424
  indices = np.where(dist_coast == dist_coast_min)
425
+ stations = indices[0]
426
+ eta_rho_values = indices[1]
427
+ xi_rho_values = indices[2]
336
428
  names = (
337
429
  data.ds[data.var_names["name"]]
338
- .isel({data.dim_names["station"]: indices[0]})
430
+ .isel({data.dim_names["station"]: stations})
339
431
  .values
340
432
  )
341
-
342
433
  # Return the indices in a dictionary format
343
- indices = {
344
- "station": indices[0],
345
- "eta_rho": indices[1],
346
- "xi_rho": indices[2],
347
- "name": names,
348
- }
349
- object.__setattr__(self, "updated_indices", indices)
434
+ river_indices = {}
435
+ for i in range(len(stations)):
436
+ river_name = names[i]
437
+ river_indices[river_name] = [
438
+ (int(eta_rho_values[i]), int(xi_rho_values[i]))
439
+ ] # list of tuples
440
+
441
+ return river_indices
350
442
 
351
443
  def _write_indices_into_dataset(self, ds):
352
444
  """Adds river location indices to the dataset as the "river_location" variable.
353
445
 
354
- This method checks if the "river_location" variable already exists in the dataset.
355
- If it does, the method removes it. Then, it creates a new "river_location" variable
356
- using river station indices from `self.updated_indices` and assigns it to the dataset.
446
+ This method creates a new "river_location" variable
447
+ using river station indices from `self.indices` and assigns it to the dataset.
357
448
  The indices specify the river station locations in terms of eta_rho and xi_rho grid cell indices.
358
449
 
359
450
  Parameters
@@ -367,17 +458,21 @@ class RiverForcing:
367
458
  The modified dataset with the "river_location" variable added.
368
459
  """
369
460
 
370
- if "river_location" in ds:
371
- ds = ds.drop_vars("river_location")
461
+ river_locations = xr.zeros_like(self.grid.ds.h)
372
462
 
373
- river_locations = xr.zeros_like(self.grid.ds.mask_rho)
374
- for i in range(len(self.updated_indices["name"])):
375
- station = self.updated_indices["station"][i]
376
- eta_index = self.updated_indices["eta_rho"][i]
377
- xi_index = self.updated_indices["xi_rho"][i]
378
- river_locations[eta_index, xi_index] = station + 2
463
+ for nriver in ds.nriver:
464
+ river_name = str(ds.river_name.sel(nriver=nriver).values)
465
+ indices = self.indices[river_name]
466
+ fraction = 1.0 / len(indices)
379
467
 
380
- river_locations.attrs["long_name"] = "River index plus local volume fraction"
468
+ for eta_index, xi_index in indices:
469
+
470
+ river_locations[eta_index, xi_index] = (
471
+ nriver # assign unique nriver ID (Fortran-based indexing)
472
+ + fraction # Fractional contribution for multiple grid points
473
+ )
474
+
475
+ river_locations.attrs["long_name"] = "River ID plus local volume fraction"
381
476
  river_locations.attrs["units"] = "none"
382
477
  ds["river_location"] = river_locations
383
478
 
@@ -463,26 +558,46 @@ class RiverForcing:
463
558
  "color": "black",
464
559
  } # Customize latitude label style
465
560
 
466
- for ax, indices in zip(axs, [self.original_indices, self.updated_indices]):
467
- for i in range(len(indices["name"])):
468
- name = indices["name"][i]
469
- xi_index = indices["xi_rho"][i]
470
- eta_index = indices["eta_rho"][i]
471
- # transform coordinates to projected space
472
- proj = ccrs.PlateCarree()
473
- transformed_lon, transformed_lat = trans.transform_point(
474
- self.grid.ds.lon_rho[eta_index, xi_index],
475
- self.grid.ds.lat_rho[eta_index, xi_index],
476
- proj,
477
- )
478
- ax.plot(
479
- transformed_lon,
480
- transformed_lat,
481
- marker="x",
482
- markersize=8,
483
- markeredgewidth=2,
484
- label=name,
485
- )
561
+ proj = ccrs.PlateCarree()
562
+
563
+ if len(self.indices) <= 10:
564
+ color_map = cm.get_cmap("tab10")
565
+ elif len(self.indices) <= 20:
566
+ color_map = cm.get_cmap("tab20")
567
+ else:
568
+ color_map = cm.get_cmap("tab20b")
569
+ # Create a dictionary of colors
570
+ colors = {name: color_map(i) for i, name in enumerate(self.indices.keys())}
571
+
572
+ for ax, indices in zip(axs, [self.original_indices, self.indices]):
573
+ added_labels = set()
574
+ for name in indices.keys():
575
+ for tuple in indices[name]:
576
+ eta_index = tuple[0]
577
+ xi_index = tuple[1]
578
+
579
+ # transform coordinates to projected space
580
+ transformed_lon, transformed_lat = trans.transform_point(
581
+ self.grid.ds.lon_rho[eta_index, xi_index],
582
+ self.grid.ds.lat_rho[eta_index, xi_index],
583
+ proj,
584
+ )
585
+
586
+ if name not in added_labels:
587
+ added_labels.add(name)
588
+ label = name
589
+ else:
590
+ label = "_None"
591
+
592
+ ax.plot(
593
+ transformed_lon,
594
+ transformed_lat,
595
+ marker="x",
596
+ markersize=8,
597
+ markeredgewidth=2,
598
+ label=label,
599
+ color=colors[name],
600
+ )
486
601
 
487
602
  axs[0].set_title("Original river locations")
488
603
  axs[1].set_title("Updated river locations")
@@ -647,4 +762,59 @@ class RiverForcing:
647
762
  grid = Grid.from_yaml(filepath)
648
763
  params = _from_yaml(cls, filepath)
649
764
 
765
+ def convert_indices_format(indices):
766
+ # Remove the '_convention' key from the dictionary if present
767
+ indices = {
768
+ key: value for key, value in indices.items() if key != "_convention"
769
+ }
770
+
771
+ # Convert the string of indices into tuples
772
+ for river, index_list in indices.items():
773
+ # Split the string by ',' and convert to tuples of integers
774
+ indices[river] = [tuple(map(int, idx.split(","))) for idx in index_list]
775
+
776
+ return indices
777
+
778
+ params["indices"] = convert_indices_format(params["indices"])
779
+
650
780
  return cls(grid=grid, **params)
781
+
782
+
783
+ def check_river_locations_are_along_coast(mask, indices):
784
+ """Check if the river locations are along the coast.
785
+
786
+ This function checks if the river locations specified in the `indices` dictionary are located on coastal grid cells.
787
+ A coastal grid cell is defined as a land grid cell adjacent to an ocean grid cell.
788
+
789
+ Parameters
790
+ ----------
791
+ mask : xarray.DataArray
792
+ A mask representing the land and ocean cells in the grid, where 1 represents ocean and 0 represents land.
793
+
794
+ indices : dict
795
+ A dictionary where the keys are river names, and the values are dictionaries containing the river's grid locations (`eta_rho` and `xi_rho`).
796
+ Each entry should have keys `"eta_rho"` and `"xi_rho"`, which are lists of grid cell indices representing river mouth locations.
797
+
798
+ Raises
799
+ ------
800
+ ValueError
801
+ If any river is not located on the coast.
802
+ """
803
+
804
+ faces = (
805
+ mask.shift(eta_rho=1)
806
+ + mask.shift(eta_rho=-1)
807
+ + mask.shift(xi_rho=1)
808
+ + mask.shift(xi_rho=-1)
809
+ )
810
+ coast = (1 - mask) * (faces > 0)
811
+
812
+ for key, river_data in indices.items():
813
+ for idx_pair in river_data:
814
+ eta_rho, xi_rho = idx_pair
815
+
816
+ # Check if the river location is along the coast
817
+ if not coast[eta_rho, xi_rho]:
818
+ raise ValueError(
819
+ f"River `{key}` is not located on the coast at grid cell ({eta_rho}, {xi_rho})."
820
+ )
@@ -6,10 +6,10 @@ import numpy as np
6
6
  import matplotlib.pyplot as plt
7
7
  from pathlib import Path
8
8
  import logging
9
- from typing import Dict, Union, List
9
+ from typing import Dict, Union, List, Optional
10
10
  from roms_tools import Grid
11
11
  from roms_tools.utils import save_datasets
12
- from roms_tools.regrid import LateralRegrid
12
+ from roms_tools.regrid import LateralRegridToROMS
13
13
  from roms_tools.plot import _plot
14
14
  from roms_tools.setup.datasets import (
15
15
  ERA5Dataset,
@@ -30,7 +30,7 @@ from roms_tools.setup.utils import (
30
30
  )
31
31
 
32
32
 
33
- @dataclass(frozen=True, kw_only=True)
33
+ @dataclass(kw_only=True)
34
34
  class SurfaceForcing:
35
35
  """Represents surface forcing input data for ROMS.
36
36
 
@@ -38,10 +38,14 @@ class SurfaceForcing:
38
38
  ----------
39
39
  grid : Grid
40
40
  Object representing the grid information.
41
- start_time : datetime
42
- Start time of the desired surface forcing data.
43
- end_time : datetime
44
- End time of the desired surface forcing data.
41
+ start_time : datetime, optional
42
+ The start time of the desired surface forcing data. This time is used to filter the dataset
43
+ to include only records on or after this time, with a single record at or before this time.
44
+ If no time filtering is desired, set it to None. Default is None.
45
+ end_time : datetime, optional
46
+ The end time of the desired surface forcing data. This time is used to filter the dataset
47
+ to include only records on or before this time, with a single record at or after this time.
48
+ If no time filtering is desired, set it to None. Default is None.
45
49
  source : Dict[str, Union[str, Path, List[Union[str, Path]]], bool]
46
50
  Dictionary specifying the source of the surface forcing data. Keys include:
47
51
 
@@ -90,8 +94,8 @@ class SurfaceForcing:
90
94
  """
91
95
 
92
96
  grid: Grid
93
- start_time: datetime
94
- end_time: datetime
97
+ start_time: Optional[datetime] = None
98
+ end_time: Optional[datetime] = None
95
99
  source: Dict[str, Union[str, Path, List[Union[str, Path]]]]
96
100
  type: str = "physics"
97
101
  correct_radiation: bool = True
@@ -117,15 +121,17 @@ class SurfaceForcing:
117
121
  logging.info("Data will be interpolated onto grid coarsened by factor 2.")
118
122
  else:
119
123
  logging.info("Data will be interpolated onto fine grid.")
120
- object.__setattr__(self, "use_coarse_grid", use_coarse_grid)
124
+ self.use_coarse_grid = use_coarse_grid
121
125
 
122
126
  target_coords = get_target_coords(self.grid, self.use_coarse_grid)
123
- object.__setattr__(self, "target_coords", target_coords)
127
+ self.target_coords = target_coords
124
128
 
125
129
  data.choose_subdomain(
126
130
  target_coords,
127
131
  buffer_points=20, # lateral fill needs some buffer from data margin
128
132
  )
133
+ # Enforce double precision to ensure reproducibility
134
+ data.convert_to_float64()
129
135
 
130
136
  data.apply_lateral_fill()
131
137
 
@@ -134,7 +140,7 @@ class SurfaceForcing:
134
140
 
135
141
  processed_fields = {}
136
142
  # lateral regridding
137
- lateral_regrid = LateralRegrid(target_coords, data.dim_names)
143
+ lateral_regrid = LateralRegridToROMS(target_coords, data.dim_names)
138
144
  for var_name in var_names:
139
145
  if var_name in data.var_names.keys():
140
146
  processed_fields[var_name] = lateral_regrid.apply(
@@ -165,9 +171,21 @@ class SurfaceForcing:
165
171
  for var_name in ds.data_vars:
166
172
  ds[var_name] = substitute_nans_by_fillvalue(ds[var_name])
167
173
 
168
- object.__setattr__(self, "ds", ds)
174
+ self.ds = ds
169
175
 
170
176
  def _input_checks(self):
177
+ # Check that start_time and end_time are both None or none of them is
178
+ if (self.start_time is None) != (self.end_time is None):
179
+ raise ValueError(
180
+ "Both `start_time` and `end_time` must be provided together as datetime objects or both should be None."
181
+ )
182
+
183
+ # Trigger a warning if both are None
184
+ if self.start_time is None and self.end_time is None:
185
+ logging.warning(
186
+ "Both `start_time` and `end_time` are None. No time filtering will be applied to the source data."
187
+ )
188
+
171
189
  # Validate the 'type' parameter
172
190
  if self.type not in ["physics", "bgc"]:
173
191
  raise ValueError("`type` must be either 'physics' or 'bgc'.")
@@ -179,11 +197,10 @@ class SurfaceForcing:
179
197
  raise ValueError("`source` must include a 'path'.")
180
198
 
181
199
  # Set 'climatology' to False if not provided in 'source'
182
- object.__setattr__(
183
- self,
184
- "source",
185
- {**self.source, "climatology": self.source.get("climatology", False)},
186
- )
200
+ self.source = {
201
+ **self.source,
202
+ "climatology": self.source.get("climatology", False),
203
+ }
187
204
 
188
205
  # Validate 'coarse_grid_mode'
189
206
  valid_modes = ["auto", "always", "never"]
@@ -321,7 +338,7 @@ class SurfaceForcing:
321
338
  else:
322
339
  variable_info[var_name] = {**default_info, "validate": False}
323
340
 
324
- object.__setattr__(self, "variable_info", variable_info)
341
+ self.variable_info = variable_info
325
342
 
326
343
  def _apply_correction(self, processed_fields, data):
327
344
 
@@ -364,7 +381,9 @@ class SurfaceForcing:
364
381
  )
365
382
 
366
383
  # Spatial regridding
367
- lateral_regrid = LateralRegrid(self.target_coords, correction_data.dim_names)
384
+ lateral_regrid = LateralRegridToROMS(
385
+ self.target_coords, correction_data.dim_names
386
+ )
368
387
  corr_factor = lateral_regrid.apply(corr_factor)
369
388
 
370
389
  processed_fields["swrad"] = processed_fields["swrad"] * corr_factor
@@ -606,7 +625,6 @@ class SurfaceForcing:
606
625
  cls,
607
626
  filepath: Union[str, Path],
608
627
  use_dask: bool = False,
609
- bypass_validation: bool = False,
610
628
  ) -> "SurfaceForcing":
611
629
  """Create an instance of the SurfaceForcing class from a YAML file.
612
630
 
@@ -616,10 +634,6 @@ class SurfaceForcing:
616
634
  The path to the YAML file from which the parameters will be read.
617
635
  use_dask: bool, optional
618
636
  Indicates whether to use dask for processing. If True, data is processed with dask; if False, data is processed eagerly. Defaults to False.
619
- bypass_validation: bool, optional
620
- Indicates whether to skip validation checks in the processed data. When set to True,
621
- the validation process that ensures no NaN values exist at wet points
622
- in the processed dataset is bypassed. Defaults to False.
623
637
 
624
638
  Returns
625
639
  -------
@@ -631,6 +645,4 @@ class SurfaceForcing:
631
645
  grid = Grid.from_yaml(filepath)
632
646
  params = _from_yaml(cls, filepath)
633
647
 
634
- return cls(
635
- grid=grid, **params, use_dask=use_dask, bypass_validation=bypass_validation
636
- )
648
+ return cls(grid=grid, **params, use_dask=use_dask)