roms-tools 3.1.2__py3-none-any.whl → 3.3.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 (221) hide show
  1. roms_tools/__init__.py +3 -0
  2. roms_tools/analysis/cdr_analysis.py +203 -0
  3. roms_tools/analysis/cdr_ensemble.py +198 -0
  4. roms_tools/analysis/roms_output.py +80 -46
  5. roms_tools/data/grids/GLORYS_global_grid.nc +0 -0
  6. roms_tools/download.py +4 -0
  7. roms_tools/plot.py +113 -51
  8. roms_tools/setup/boundary_forcing.py +45 -20
  9. roms_tools/setup/cdr_forcing.py +122 -8
  10. roms_tools/setup/cdr_release.py +161 -8
  11. roms_tools/setup/grid.py +150 -141
  12. roms_tools/setup/initial_conditions.py +113 -48
  13. roms_tools/setup/{datasets.py → lat_lon_datasets.py} +443 -938
  14. roms_tools/setup/mask.py +63 -7
  15. roms_tools/setup/nesting.py +314 -117
  16. roms_tools/setup/river_datasets.py +527 -0
  17. roms_tools/setup/river_forcing.py +46 -20
  18. roms_tools/setup/surface_forcing.py +7 -9
  19. roms_tools/setup/tides.py +2 -3
  20. roms_tools/setup/topography.py +8 -10
  21. roms_tools/setup/utils.py +396 -23
  22. roms_tools/tests/test_analysis/test_cdr_analysis.py +144 -0
  23. roms_tools/tests/test_analysis/test_cdr_ensemble.py +202 -0
  24. roms_tools/tests/test_analysis/test_roms_output.py +61 -3
  25. roms_tools/tests/test_setup/test_boundary_forcing.py +54 -52
  26. roms_tools/tests/test_setup/test_cdr_forcing.py +54 -0
  27. roms_tools/tests/test_setup/test_cdr_release.py +118 -1
  28. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/ALK_ALT_CO2_east/c/0/0/0 +0 -0
  29. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/ALK_ALT_CO2_north/c/0/0/0 +0 -0
  30. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/ALK_ALT_CO2_west/c/0/0/0 +0 -0
  31. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/ALK_east/c/0/0/0 +0 -0
  32. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/ALK_north/c/0/0/0 +0 -0
  33. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/ALK_west/c/0/0/0 +0 -0
  34. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DIC_ALT_CO2_east/c/0/0/0 +0 -0
  35. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DIC_ALT_CO2_north/c/0/0/0 +0 -0
  36. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DIC_ALT_CO2_west/c/0/0/0 +0 -0
  37. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DIC_east/c/0/0/0 +0 -0
  38. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DIC_north/c/0/0/0 +0 -0
  39. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DIC_west/c/0/0/0 +0 -0
  40. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DOC_east/c/0/0/0 +0 -0
  41. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DOC_north/c/0/0/0 +0 -0
  42. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DOC_west/c/0/0/0 +0 -0
  43. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DOCr_east/c/0/0/0 +0 -0
  44. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DOCr_north/c/0/0/0 +0 -0
  45. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DOCr_west/c/0/0/0 +0 -0
  46. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DON_east/c/0/0/0 +0 -0
  47. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DON_north/c/0/0/0 +0 -0
  48. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DON_west/c/0/0/0 +0 -0
  49. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DONr_east/c/0/0/0 +0 -0
  50. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DONr_north/c/0/0/0 +0 -0
  51. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DONr_west/c/0/0/0 +0 -0
  52. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DOP_east/c/0/0/0 +0 -0
  53. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DOP_north/c/0/0/0 +0 -0
  54. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DOP_west/c/0/0/0 +0 -0
  55. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DOPr_east/c/0/0/0 +0 -0
  56. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DOPr_north/c/0/0/0 +0 -0
  57. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/DOPr_west/c/0/0/0 +0 -0
  58. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/Fe_east/c/0/0/0 +0 -0
  59. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/Fe_north/c/0/0/0 +0 -0
  60. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/Fe_west/c/0/0/0 +0 -0
  61. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/Lig_east/c/0/0/0 +0 -0
  62. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/Lig_north/c/0/0/0 +0 -0
  63. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/Lig_west/c/0/0/0 +0 -0
  64. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/NH4_east/c/0/0/0 +0 -0
  65. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/NH4_north/c/0/0/0 +0 -0
  66. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/NH4_west/c/0/0/0 +0 -0
  67. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/NO3_east/c/0/0/0 +0 -0
  68. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/NO3_north/c/0/0/0 +0 -0
  69. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/NO3_west/c/0/0/0 +0 -0
  70. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/O2_east/c/0/0/0 +0 -0
  71. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/O2_north/c/0/0/0 +0 -0
  72. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/O2_west/c/0/0/0 +0 -0
  73. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/PO4_east/c/0/0/0 +0 -0
  74. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/PO4_north/c/0/0/0 +0 -0
  75. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/PO4_west/c/0/0/0 +0 -0
  76. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/SiO3_east/c/0/0/0 +0 -0
  77. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/SiO3_north/c/0/0/0 +0 -0
  78. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/SiO3_west/c/0/0/0 +0 -0
  79. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatC_east/c/0/0/0 +0 -0
  80. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatC_north/c/0/0/0 +0 -0
  81. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatC_west/c/0/0/0 +0 -0
  82. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatChl_east/c/0/0/0 +0 -0
  83. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatChl_north/c/0/0/0 +0 -0
  84. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatChl_west/c/0/0/0 +0 -0
  85. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatFe_east/c/0/0/0 +0 -0
  86. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatFe_north/c/0/0/0 +0 -0
  87. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatFe_west/c/0/0/0 +0 -0
  88. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatP_east/c/0/0/0 +0 -0
  89. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatP_north/c/0/0/0 +0 -0
  90. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatP_west/c/0/0/0 +0 -0
  91. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatSi_east/c/0/0/0 +0 -0
  92. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatSi_north/c/0/0/0 +0 -0
  93. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diatSi_west/c/0/0/0 +0 -0
  94. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazC_east/c/0/0/0 +0 -0
  95. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazC_north/c/0/0/0 +0 -0
  96. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazC_west/c/0/0/0 +0 -0
  97. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazChl_east/c/0/0/0 +0 -0
  98. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazChl_north/c/0/0/0 +0 -0
  99. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazChl_west/c/0/0/0 +0 -0
  100. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazFe_east/c/0/0/0 +0 -0
  101. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazFe_north/c/0/0/0 +0 -0
  102. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazFe_west/c/0/0/0 +0 -0
  103. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazP_east/c/0/0/0 +0 -0
  104. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazP_north/c/0/0/0 +0 -0
  105. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/diazP_west/c/0/0/0 +0 -0
  106. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spC_east/c/0/0/0 +0 -0
  107. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spC_north/c/0/0/0 +0 -0
  108. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spC_west/c/0/0/0 +0 -0
  109. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spCaCO3_east/c/0/0/0 +0 -0
  110. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spCaCO3_north/c/0/0/0 +0 -0
  111. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spCaCO3_west/c/0/0/0 +0 -0
  112. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spChl_east/c/0/0/0 +0 -0
  113. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spChl_north/c/0/0/0 +0 -0
  114. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spChl_west/c/0/0/0 +0 -0
  115. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spFe_east/c/0/0/0 +0 -0
  116. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spFe_north/c/0/0/0 +0 -0
  117. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spFe_west/c/0/0/0 +0 -0
  118. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spP_east/c/0/0/0 +0 -0
  119. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spP_north/c/0/0/0 +0 -0
  120. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/spP_west/c/0/0/0 +0 -0
  121. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/zarr.json +406 -406
  122. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/zooC_east/c/0/0/0 +0 -0
  123. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/zooC_north/c/0/0/0 +0 -0
  124. roms_tools/tests/test_setup/test_data/bgc_boundary_forcing_from_climatology.zarr/zooC_west/c/0/0/0 +0 -0
  125. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/salt_east/c/0/0/0 +0 -0
  126. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/salt_north/c/0/0/0 +0 -0
  127. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/salt_south/c/0/0/0 +0 -0
  128. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/salt_west/c/0/0/0 +0 -0
  129. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/temp_east/c/0/0/0 +0 -0
  130. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/temp_north/c/0/0/0 +0 -0
  131. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/temp_south/c/0/0/0 +0 -0
  132. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/temp_west/c/0/0/0 +0 -0
  133. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/u_east/c/0/0/0 +0 -0
  134. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/u_north/c/0/0/0 +0 -0
  135. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/u_south/c/0/0/0 +0 -0
  136. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/u_west/c/0/0/0 +0 -0
  137. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/ubar_east/c/0/0 +0 -0
  138. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/ubar_north/c/0/0 +0 -0
  139. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/ubar_south/c/0/0 +0 -0
  140. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/ubar_west/c/0/0 +0 -0
  141. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/v_east/c/0/0/0 +0 -0
  142. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/v_north/c/0/0/0 +0 -0
  143. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/v_south/c/0/0/0 +0 -0
  144. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/v_west/c/0/0/0 +0 -0
  145. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/vbar_east/c/0/0 +0 -0
  146. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/vbar_north/c/0/0 +0 -0
  147. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/vbar_south/c/0/0 +0 -0
  148. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/vbar_west/c/0/0 +0 -0
  149. roms_tools/tests/test_setup/test_data/boundary_forcing.zarr/zarr.json +182 -182
  150. roms_tools/tests/test_setup/test_data/grid.zarr/h/c/0/0 +0 -0
  151. roms_tools/tests/test_setup/test_data/grid.zarr/zarr.json +191 -191
  152. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/h/c/0/0 +0 -0
  153. roms_tools/tests/test_setup/test_data/grid_that_straddles_dateline.zarr/zarr.json +210 -210
  154. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/ALK/c/0/0/0/0 +0 -0
  155. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/ALK_ALT_CO2/c/0/0/0/0 +0 -0
  156. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/DIC/c/0/0/0/0 +0 -0
  157. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/DIC_ALT_CO2/c/0/0/0/0 +0 -0
  158. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/DOC/c/0/0/0/0 +0 -0
  159. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/DOCr/c/0/0/0/0 +0 -0
  160. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/DON/c/0/0/0/0 +0 -0
  161. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/DONr/c/0/0/0/0 +0 -0
  162. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/DOP/c/0/0/0/0 +0 -0
  163. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/DOPr/c/0/0/0/0 +0 -0
  164. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/Fe/c/0/0/0/0 +0 -0
  165. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/Lig/c/0/0/0/0 +0 -0
  166. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/NH4/c/0/0/0/0 +0 -0
  167. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/NO3/c/0/0/0/0 +0 -0
  168. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/O2/c/0/0/0/0 +0 -0
  169. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/PO4/c/0/0/0/0 +0 -0
  170. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/SiO3/c/0/0/0/0 +0 -0
  171. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/diatC/c/0/0/0/0 +0 -0
  172. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/diatChl/c/0/0/0/0 +0 -0
  173. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/diatFe/c/0/0/0/0 +0 -0
  174. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/diatP/c/0/0/0/0 +0 -0
  175. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/diatSi/c/0/0/0/0 +0 -0
  176. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/diazC/c/0/0/0/0 +0 -0
  177. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/diazChl/c/0/0/0/0 +0 -0
  178. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/diazFe/c/0/0/0/0 +0 -0
  179. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/diazP/c/0/0/0/0 +0 -0
  180. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/salt/c/0/0/0/0 +0 -0
  181. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/spC/c/0/0/0/0 +0 -0
  182. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/spCaCO3/c/0/0/0/0 +0 -0
  183. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/spChl/c/0/0/0/0 +0 -0
  184. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/spFe/c/0/0/0/0 +0 -0
  185. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/spP/c/0/0/0/0 +0 -0
  186. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/temp/c/0/0/0/0 +0 -0
  187. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/u/c/0/0/0/0 +0 -0
  188. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/ubar/c/0/0/0 +0 -0
  189. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/v/c/0/0/0/0 +0 -0
  190. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/vbar/c/0/0/0 +0 -0
  191. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/zarr.json +182 -182
  192. roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/zooC/c/0/0/0/0 +0 -0
  193. roms_tools/tests/test_setup/test_data/initial_conditions_with_unified_bgc_from_climatology.zarr/salt/c/0/0/0/0 +0 -0
  194. roms_tools/tests/test_setup/test_data/initial_conditions_with_unified_bgc_from_climatology.zarr/temp/c/0/0/0/0 +0 -0
  195. roms_tools/tests/test_setup/test_data/initial_conditions_with_unified_bgc_from_climatology.zarr/u/c/0/0/0/0 +0 -0
  196. roms_tools/tests/test_setup/test_data/initial_conditions_with_unified_bgc_from_climatology.zarr/ubar/c/0/0/0 +0 -0
  197. roms_tools/tests/test_setup/test_data/initial_conditions_with_unified_bgc_from_climatology.zarr/v/c/0/0/0/0 +0 -0
  198. roms_tools/tests/test_setup/test_data/initial_conditions_with_unified_bgc_from_climatology.zarr/vbar/c/0/0/0 +0 -0
  199. roms_tools/tests/test_setup/test_data/initial_conditions_with_unified_bgc_from_climatology.zarr/zarr.json +187 -187
  200. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/u_Im/c/0/0/0 +0 -0
  201. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/u_Re/c/0/0/0 +0 -0
  202. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/v_Im/c/0/0/0 +0 -0
  203. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/v_Re/c/0/0/0 +0 -0
  204. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/zarr.json +66 -66
  205. roms_tools/tests/test_setup/test_grid.py +236 -115
  206. roms_tools/tests/test_setup/test_initial_conditions.py +94 -41
  207. roms_tools/tests/test_setup/{test_datasets.py → test_lat_lon_datasets.py} +409 -100
  208. roms_tools/tests/test_setup/test_nesting.py +119 -31
  209. roms_tools/tests/test_setup/test_river_datasets.py +48 -0
  210. roms_tools/tests/test_setup/test_surface_forcing.py +2 -1
  211. roms_tools/tests/test_setup/test_utils.py +92 -2
  212. roms_tools/tests/test_setup/utils.py +71 -0
  213. roms_tools/tests/test_tiling/test_join.py +241 -0
  214. roms_tools/tests/test_utils.py +139 -17
  215. roms_tools/tiling/join.py +189 -0
  216. roms_tools/utils.py +131 -99
  217. {roms_tools-3.1.2.dist-info → roms_tools-3.3.0.dist-info}/METADATA +12 -2
  218. {roms_tools-3.1.2.dist-info → roms_tools-3.3.0.dist-info}/RECORD +221 -211
  219. {roms_tools-3.1.2.dist-info → roms_tools-3.3.0.dist-info}/WHEEL +0 -0
  220. {roms_tools-3.1.2.dist-info → roms_tools-3.3.0.dist-info}/licenses/LICENSE +0 -0
  221. {roms_tools-3.1.2.dist-info → roms_tools-3.3.0.dist-info}/top_level.txt +0 -0
roms_tools/plot.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from typing import Any, Literal
2
2
 
3
3
  import cartopy.crs as ccrs
4
+ import matplotlib.dates as mdates
4
5
  import matplotlib.pyplot as plt
5
6
  import numpy as np
6
7
  import xarray as xr
@@ -9,18 +10,16 @@ from matplotlib.figure import Figure
9
10
 
10
11
  from roms_tools.regrid import LateralRegridFromROMS, VerticalRegridFromROMS
11
12
  from roms_tools.utils import (
12
- _generate_coordinate_range,
13
- _remove_edge_nans,
13
+ generate_coordinate_range,
14
14
  infer_nominal_horizontal_resolution,
15
15
  normalize_longitude,
16
+ remove_edge_nans,
16
17
  )
17
18
  from roms_tools.vertical_coordinate import compute_depth_coordinates
18
19
 
19
20
  LABEL_COLOR = "k"
20
21
  LABEL_SZ = 10
21
22
  FONT_SZ = 10
22
- EDGE_POS_START = "start"
23
- EDGE_POS_END = "end"
24
23
 
25
24
 
26
25
  def _add_gridlines(ax: Axes) -> None:
@@ -97,12 +96,6 @@ def plot_2d_horizontal_field(
97
96
  lon_deg = field.lon
98
97
  lat_deg = field.lat
99
98
 
100
- # check if North or South pole are in domain
101
- if lat_deg.max().values > 89 or lat_deg.min().values < -89:
102
- raise NotImplementedError(
103
- "Plotting is not implemented for the case that the domain contains the North or South pole."
104
- )
105
-
106
99
  trans = get_projection(lon_deg, lat_deg)
107
100
 
108
101
  if ax is None:
@@ -187,25 +180,8 @@ def plot_nesting(parent_grid_ds, child_grid_ds, parent_straddle, with_dim_names=
187
180
  with_dim_names=with_dim_names,
188
181
  )
189
182
 
190
- vmax = 3
191
- vmin = 0
192
- cmap = plt.colormaps.get_cmap("Blues")
193
- cmap.set_bad(color="gray")
194
- kwargs = {"vmax": vmax, "vmin": vmin, "cmap": cmap}
195
-
196
- field = parent_grid_ds.mask_rho
197
- field = field.where(field)
198
-
199
- _add_field_to_ax(
200
- ax,
201
- parent_lon_deg,
202
- parent_lat_deg,
203
- field,
204
- add_colorbar=False,
205
- kwargs=kwargs,
206
- )
207
-
208
183
  _add_gridlines(ax)
184
+ ax.coastlines()
209
185
 
210
186
  ax.legend(loc="best")
211
187
 
@@ -497,16 +473,16 @@ def line_plot(
497
473
 
498
474
 
499
475
  def _get_edge(
500
- arr: xr.DataArray, dim_name: str, pos: Literal[EDGE_POS_START, EDGE_POS_END]
476
+ arr: xr.DataArray, dim_name: str, pos: Literal["start", "end"]
501
477
  ) -> xr.DataArray:
502
478
  """Extract the first ("start") or last ("end") slice along the given dimension."""
503
- if pos == EDGE_POS_START:
479
+ if pos == "start":
504
480
  return arr.isel({dim_name: 0})
505
481
 
506
- if pos == EDGE_POS_END:
482
+ if pos == "end":
507
483
  return arr.isel({dim_name: -1})
508
484
 
509
- raise ValueError(f"pos must be {EDGE_POS_START} or {EDGE_POS_END}")
485
+ raise ValueError("pos must be `start` or `end`")
510
486
 
511
487
 
512
488
  def _add_boundary_to_ax(
@@ -538,23 +514,23 @@ def _add_boundary_to_ax(
538
514
 
539
515
  edges = [
540
516
  (
541
- _get_edge(lon_deg, xi_dim, EDGE_POS_START),
542
- _get_edge(lat_deg, xi_dim, EDGE_POS_START),
517
+ _get_edge(lon_deg, xi_dim, "start"),
518
+ _get_edge(lat_deg, xi_dim, "start"),
543
519
  r"$\eta$",
544
520
  ), # left
545
521
  (
546
- _get_edge(lon_deg, xi_dim, EDGE_POS_END),
547
- _get_edge(lat_deg, xi_dim, EDGE_POS_END),
522
+ _get_edge(lon_deg, xi_dim, "end"),
523
+ _get_edge(lat_deg, xi_dim, "end"),
548
524
  r"$\eta$",
549
525
  ), # right
550
526
  (
551
- _get_edge(lon_deg, eta_dim, EDGE_POS_START),
552
- _get_edge(lat_deg, eta_dim, EDGE_POS_START),
527
+ _get_edge(lon_deg, eta_dim, "start"),
528
+ _get_edge(lat_deg, eta_dim, "start"),
553
529
  r"$\xi$",
554
530
  ), # bottom
555
531
  (
556
- _get_edge(lon_deg, eta_dim, EDGE_POS_END),
557
- _get_edge(lat_deg, eta_dim, EDGE_POS_END),
532
+ _get_edge(lon_deg, eta_dim, "end"),
533
+ _get_edge(lat_deg, eta_dim, "end"),
558
534
  r"$\xi$",
559
535
  ), # top
560
536
  ]
@@ -660,7 +636,7 @@ def _add_field_to_ax(
660
636
  proj = ccrs.PlateCarree()
661
637
 
662
638
  p = ax.pcolormesh(lon_deg, lat_deg, field, transform=proj, **kwargs)
663
- if hasattr(field, "long_name"):
639
+ if hasattr(field, "long_name") and hasattr(field, "units"):
664
640
  label = f"{field.long_name} [{field.units}]"
665
641
  elif hasattr(field, "Long_name"):
666
642
  # this is the case for matlab generated grids
@@ -675,10 +651,36 @@ def _add_field_to_ax(
675
651
  ax.clabel(cs, inline=True, fontsize=FONT_SZ)
676
652
 
677
653
 
678
- def get_projection(lon, lat):
679
- return ccrs.NearsidePerspective(
680
- central_longitude=lon.mean().values, central_latitude=lat.mean().values
681
- )
654
+ def get_projection(lon: xr.DataArray, lat: xr.DataArray):
655
+ """
656
+ Return a Cartopy projection appropriate for the given lon/lat domain.
657
+
658
+ - Raises an error if the domain includes either pole.
659
+ - Uses PlateCarree if the longitudinal span > 90°.
660
+ - Otherwise uses NearsidePerspective centered on the domain.
661
+
662
+ Parameters
663
+ ----------
664
+ lon, lat : xr.DataArray
665
+ Longitude and latitude values in degrees.
666
+
667
+ Returns
668
+ -------
669
+ cartopy.crs.Projection
670
+ The chosen Cartopy projection.
671
+ """
672
+ # check if North or South pole are in domain
673
+ if lat.max() > 89 or lat.min() < -89:
674
+ raise NotImplementedError(
675
+ "Plotting is not implemented for the case that the domain contains the North or South pole."
676
+ )
677
+
678
+ if lon.max() - lon.min() > 90:
679
+ return ccrs.PlateCarree(central_longitude=lon.mean().values)
680
+ else:
681
+ return ccrs.NearsidePerspective(
682
+ central_longitude=lon.mean().values, central_latitude=lat.mean().values
683
+ )
682
684
 
683
685
 
684
686
  def _validate_plot_inputs(
@@ -817,9 +819,10 @@ def plot(
817
819
  yincrease: bool | None = None,
818
820
  use_coarse_grid: bool = False,
819
821
  with_dim_names: bool = False,
822
+ apply_mask: bool = True,
820
823
  ax: Axes | None = None,
821
824
  save_path: str | None = None,
822
- cmap_name: str | None = "YlOrRd",
825
+ cmap_name: str = "YlOrRd",
823
826
  add_colorbar: bool = True,
824
827
  ) -> None:
825
828
  """Generate a plot of a 2D or 3D ROMS field for a horizontal or vertical slice.
@@ -890,6 +893,9 @@ def plot(
890
893
  with_dim_names : bool, optional
891
894
  Add grid dimension names (`xi`, `eta`) to the outer plot edges. Only for 2D plots. Default is False.
892
895
 
896
+ apply_mask: bool, optional
897
+ Whether to apply the land mask to the field. Default is True.
898
+
893
899
  ax : Axes, optional
894
900
  Matplotlib axes object. If None, a new figure is created. Default is None.
895
901
 
@@ -972,7 +978,8 @@ def plot(
972
978
  field = field.assign_coords({"lon": lon_deg, "lat": lat_deg})
973
979
 
974
980
  # Mask the field
975
- field = field.where(mask)
981
+ if apply_mask:
982
+ field = field.where(mask)
976
983
 
977
984
  # Assign eta and xi as coordinates
978
985
  coords_to_assign = {dim: field[dim] for dim in horizontal_dims.values()}
@@ -1052,7 +1059,7 @@ def plot(
1052
1059
  title = title + f", lat = {lat}°N"
1053
1060
  else:
1054
1061
  resolution = infer_nominal_horizontal_resolution(grid_ds)
1055
- lats = _generate_coordinate_range(
1062
+ lats = generate_coordinate_range(
1056
1063
  field.lat.min().values, field.lat.max().values, resolution
1057
1064
  )
1058
1065
  lats = xr.DataArray(lats, dims=["lat"], attrs={"units": "°N"})
@@ -1062,7 +1069,7 @@ def plot(
1062
1069
  title = title + f", lon = {lon}°E"
1063
1070
  else:
1064
1071
  resolution = infer_nominal_horizontal_resolution(grid_ds, lat)
1065
- lons = _generate_coordinate_range(
1072
+ lons = generate_coordinate_range(
1066
1073
  field.lon.min().values, field.lon.max().values, resolution
1067
1074
  )
1068
1075
  lons = xr.DataArray(lons, dims=["lon"], attrs={"units": "°E"})
@@ -1079,11 +1086,11 @@ def plot(
1079
1086
  field = field.assign_coords({"layer_depth": layer_depth})
1080
1087
 
1081
1088
  if lat is not None:
1082
- field, layer_depth = _remove_edge_nans(
1089
+ field, layer_depth = remove_edge_nans(
1083
1090
  field, "lon", layer_depth if "layer_depth" in locals() else None
1084
1091
  )
1085
1092
  if lon is not None:
1086
- field, layer_depth = _remove_edge_nans(
1093
+ field, layer_depth = remove_edge_nans(
1087
1094
  field, "lat", layer_depth if "layer_depth" in locals() else None
1088
1095
  )
1089
1096
 
@@ -1252,3 +1259,58 @@ def plot_location(
1252
1259
 
1253
1260
  if include_legend:
1254
1261
  ax.legend(loc="center left", bbox_to_anchor=(1.1, 0.5))
1262
+
1263
+
1264
+ def plot_uptake_efficiency(ds: xr.Dataset) -> None:
1265
+ """
1266
+ Plot Carbon Dioxide Removal (CDR) uptake efficiency over time.
1267
+
1268
+ This function plots two estimates of uptake efficiency stored in the dataset:
1269
+ 1. `cdr_efficiency`, computed from CO2 flux differences.
1270
+ 2. `cdr_efficiency_from_delta_diff`, computed from DIC differences.
1271
+
1272
+ The x-axis shows absolute time, formatted as YYYY-MM-DD, and the y-axis shows
1273
+ the uptake efficiency values. The plot includes a legend and grid for clarity.
1274
+
1275
+ Parameters
1276
+ ----------
1277
+ ds : xarray.Dataset
1278
+ Dataset containing the following variables:
1279
+ - "abs_time": array of timestamps (datetime-like)
1280
+ - "cdr_efficiency": uptake efficiency from flux differences
1281
+ - "cdr_efficiency_from_delta_diff": uptake efficiency from DIC differences
1282
+
1283
+ Raises
1284
+ ------
1285
+ ValueError
1286
+ If required variables are missing or empty.
1287
+
1288
+ Returns
1289
+ -------
1290
+ None
1291
+ """
1292
+ required_vars = ["abs_time", "cdr_efficiency", "cdr_efficiency_from_delta_diff"]
1293
+ for var in required_vars:
1294
+ if var not in ds or ds[var].size == 0:
1295
+ raise ValueError(f"Dataset must contain non-empty variable '{var}'.")
1296
+
1297
+ times = ds["abs_time"]
1298
+
1299
+ # Check for monotonically increasing times
1300
+ if not np.all(times[1:] >= times[:-1]):
1301
+ raise ValueError("abs_time must be strictly increasing.")
1302
+
1303
+ fig, ax = plt.subplots(figsize=(10, 4))
1304
+
1305
+ ax.plot(times, ds["cdr_efficiency"], label="from CO2 flux differences", lw=2)
1306
+ ax.plot(
1307
+ times, ds["cdr_efficiency_from_delta_diff"], label="from DIC differences", lw=2
1308
+ )
1309
+ ax.grid()
1310
+ ax.set_title("CDR uptake efficiency")
1311
+ ax.legend()
1312
+
1313
+ # Format x-axis as YYYY-MM-DD
1314
+ ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m-%d"))
1315
+ fig.autofmt_xdate()
1316
+ plt.show()
@@ -13,14 +13,14 @@ from scipy.ndimage import label
13
13
  from roms_tools import Grid
14
14
  from roms_tools.plot import line_plot, section_plot
15
15
  from roms_tools.regrid import LateralRegridToROMS, VerticalRegridToROMS
16
- from roms_tools.setup.datasets import (
16
+ from roms_tools.setup.lat_lon_datasets import (
17
17
  CESMBGCDataset,
18
- Dataset,
19
18
  GLORYSDataset,
20
19
  GLORYSDefaultDataset,
21
20
  UnifiedBGCDataset,
22
21
  )
23
22
  from roms_tools.setup.utils import (
23
+ RawDataSource,
24
24
  add_time_info_to_ds,
25
25
  compute_barotropic_velocity,
26
26
  compute_missing_bgc_variables,
@@ -63,7 +63,7 @@ class BoundaryForcing:
63
63
  If no time filtering is desired, set it to None. Default is None.
64
64
  boundaries : Dict[str, bool], optional
65
65
  Dictionary specifying which boundaries are forced (south, east, north, west). Default is all True.
66
- source : Dict[str, Union[str, Path, List[Union[str, Path]]], bool]
66
+ source : RawDataSource
67
67
  Dictionary specifying the source of the boundary forcing data. Keys include:
68
68
 
69
69
  - "name" (str): Name of the data source (e.g., "GLORYS").
@@ -71,7 +71,9 @@ class BoundaryForcing:
71
71
 
72
72
  - A single string (with or without wildcards).
73
73
  - A single Path object.
74
- - A list of strings or Path objects containing multiple files.
74
+ - A list of strings or Path objects.
75
+ If omitted, the data will be streamed via the Copernicus Marine Toolkit.
76
+ Note: streaming is currently not recommended due to performance limitations.
75
77
  - "climatology" (bool): Indicates if the data is climatology data. Defaults to False.
76
78
 
77
79
  type : str
@@ -124,7 +126,7 @@ class BoundaryForcing:
124
126
  }
125
127
  )
126
128
  """Dictionary specifying which boundaries are forced (south, east, north, west)."""
127
- source: dict[str, str | Path | list[str | Path]]
129
+ source: RawDataSource
128
130
  """Dictionary specifying the source of the boundary forcing data."""
129
131
  type: str = "physics"
130
132
  """Specifies the type of forcing data ("physics", "bgc")."""
@@ -157,7 +159,6 @@ class BoundaryForcing:
157
159
  if self.apply_2d_horizontal_fill:
158
160
  data.choose_subdomain(
159
161
  target_coords,
160
- buffer_points=20, # lateral fill needs good buffer from data margin
161
162
  )
162
163
  # Enforce double precision to ensure reproducibility
163
164
  data.convert_to_float64()
@@ -297,14 +298,12 @@ class BoundaryForcing:
297
298
  zeta_v = zeta_v.isel(**self.bdry_coords["v"][direction])
298
299
 
299
300
  if not self.apply_2d_horizontal_fill and bdry_data.needs_lateral_fill:
300
- logging.info(
301
- f"Applying 1D horizontal fill to {direction}ern boundary."
302
- )
303
- self._validate_1d_fill(
304
- processed_fields,
305
- direction,
306
- bdry_data.dim_names["depth"],
307
- )
301
+ if not self.bypass_validation:
302
+ self._validate_1d_fill(
303
+ processed_fields,
304
+ direction,
305
+ bdry_data.dim_names["depth"],
306
+ )
308
307
  for var_name in processed_fields:
309
308
  processed_fields[var_name] = apply_1d_horizontal_fill(
310
309
  processed_fields[var_name]
@@ -435,7 +434,9 @@ class BoundaryForcing:
435
434
  "Sea surface height will NOT be used to adjust depth coordinates."
436
435
  )
437
436
 
438
- def _get_data(self) -> Dataset:
437
+ def _get_data(
438
+ self,
439
+ ) -> GLORYSDataset | GLORYSDefaultDataset | CESMBGCDataset | UnifiedBGCDataset:
439
440
  """Determine the correct `Dataset` type and return an instance.
440
441
 
441
442
  Returns
@@ -444,7 +445,21 @@ class BoundaryForcing:
444
445
  The `Dataset` instance
445
446
 
446
447
  """
447
- dataset_map: dict[str, dict[str, dict[str, type[Dataset]]]] = {
448
+ dataset_map: dict[
449
+ str,
450
+ dict[
451
+ str,
452
+ dict[
453
+ str,
454
+ type[
455
+ GLORYSDataset
456
+ | GLORYSDefaultDataset
457
+ | CESMBGCDataset
458
+ | UnifiedBGCDataset
459
+ ],
460
+ ],
461
+ ],
462
+ ] = {
448
463
  "physics": {
449
464
  "GLORYS": {
450
465
  "external": GLORYSDataset,
@@ -471,13 +486,16 @@ class BoundaryForcing:
471
486
 
472
487
  data_type = dataset_map[self.type][source_name][variant]
473
488
 
489
+ if isinstance(self.source["path"], bool):
490
+ raise ValueError('source["path"] cannot be a boolean here')
491
+
474
492
  return data_type(
475
493
  filename=self.source["path"],
476
494
  start_time=self.start_time,
477
495
  end_time=self.end_time,
478
- climatology=self.source["climatology"],
496
+ climatology=self.source["climatology"], # type: ignore[arg-type]
479
497
  use_dask=self.use_dask,
480
- ) # type: ignore
498
+ )
481
499
 
482
500
  def _set_variable_info(self, data):
483
501
  """Sets up a dictionary with metadata for variables based on the type of data
@@ -756,6 +774,9 @@ class BoundaryForcing:
756
774
  None
757
775
  If a boundary is divided by land, a warning is issued. No return value is provided.
758
776
  """
777
+ if not hasattr(self, "_warned_directions"):
778
+ self._warned_directions = set()
779
+
759
780
  for var_name in processed_fields.keys():
760
781
  if self.variable_info[var_name]["validate"]:
761
782
  location = self.variable_info[var_name]["location"]
@@ -778,16 +799,20 @@ class BoundaryForcing:
778
799
  wet_nans = xr.where(da.where(mask).isnull(), 1, 0)
779
800
  # Apply label to find connected components of wet NaNs
780
801
  labeled_array, num_features = label(wet_nans)
802
+
781
803
  left_margin = labeled_array[0]
782
804
  right_margin = labeled_array[-1]
783
805
  if left_margin != 0:
784
806
  num_features = num_features - 1
785
807
  if right_margin != 0:
786
808
  num_features = num_features - 1
787
- if num_features > 0:
809
+
810
+ if num_features > 0 and direction not in self._warned_directions:
788
811
  logging.warning(
789
- f"For {var_name}, the {direction}ern boundary is divided by land. It would be safer (but slower) to use `apply_2d_horizontal_fill = True`."
812
+ f"The {direction}ern boundary is divided by land. "
813
+ "It would be safer (but slower and more memory-intensive) to use `apply_2d_horizontal_fill = True`."
790
814
  )
815
+ self._warned_directions.add(direction)
791
816
 
792
817
  def _validate(self, ds):
793
818
  """Validate the dataset for NaN values at the first time step (bry_time=0) for
@@ -2,19 +2,19 @@ import itertools
2
2
  import logging
3
3
  from collections import Counter
4
4
  from collections.abc import Iterator
5
- from datetime import datetime
5
+ from datetime import datetime, timedelta
6
6
  from pathlib import Path
7
7
  from typing import Annotated
8
8
 
9
9
  import matplotlib.gridspec as gridspec
10
10
  import matplotlib.pyplot as plt
11
11
  import numpy as np
12
+ import pandas as pd
12
13
  import xarray as xr
13
14
  from pydantic import (
14
15
  BaseModel,
15
16
  Field,
16
17
  RootModel,
17
- conlist,
18
18
  model_serializer,
19
19
  model_validator,
20
20
  )
@@ -40,6 +40,7 @@ from roms_tools.setup.utils import (
40
40
  from_yaml,
41
41
  gc_dist,
42
42
  get_target_coords,
43
+ get_tracer_metadata_dict,
43
44
  to_dict,
44
45
  validate_names,
45
46
  write_to_yaml,
@@ -103,14 +104,16 @@ class ReleaseSimulationManager(BaseModel):
103
104
  class ReleaseCollector(RootModel):
104
105
  """Collects and validates multiple releases against each other."""
105
106
 
106
- root: conlist(
107
- Annotated[
108
- VolumeRelease | TracerPerturbation, Field(discriminator="release_type")
107
+ root: Annotated[
108
+ list[
109
+ Annotated[
110
+ VolumeRelease | TracerPerturbation, Field(discriminator="release_type")
111
+ ]
109
112
  ],
110
- min_length=1,
111
- ) = Field(alias="releases")
113
+ Field(alias="releases", min_length=1),
114
+ ]
112
115
 
113
- _release_type: ReleaseType = None
116
+ _release_type: ReleaseType | None = None
114
117
 
115
118
  def __iter__(self) -> Iterator[Release]:
116
119
  return iter(self.root)
@@ -126,6 +129,9 @@ class ReleaseCollector(RootModel):
126
129
  else:
127
130
  raise TypeError(f"Invalid key type: {type(item)}. Must be int or str.")
128
131
 
132
+ def __len__(self):
133
+ return len(self.root)
134
+
129
135
  @model_validator(mode="before")
130
136
  @classmethod
131
137
  def unpack_dict(cls, data):
@@ -774,6 +780,62 @@ class CDRForcing(BaseModel):
774
780
  fig.subplots_adjust(hspace=0.45)
775
781
  fig.suptitle(f"Release distribution for: {release_name}")
776
782
 
783
+ def compute_total_cdr_source(self, dt: float) -> pd.DataFrame:
784
+ """
785
+ Compute integrated tracer quantities for all releases and return a DataFrame.
786
+
787
+ Parameters
788
+ ----------
789
+ dt : float
790
+ Time step in seconds for reconstructing ROMS time stamps.
791
+
792
+ Returns
793
+ -------
794
+ pd.DataFrame
795
+ DataFrame with one row per release and one row of units at the top.
796
+ Columns 'temp' and 'salt' are excluded from integrated totals.
797
+ """
798
+ # Reconstruct ROMS time stamps
799
+ _, rel_seconds = _reconstruct_roms_time_stamps(
800
+ self.start_time, self.end_time, dt, self.model_reference_date
801
+ )
802
+
803
+ # Collect accounting results for all releases
804
+ records = []
805
+ release_names = []
806
+ for release in self.releases:
807
+ result = release._do_accounting(rel_seconds, self.model_reference_date)
808
+ records.append(result)
809
+ release_names.append(getattr(release, "name", f"release_{len(records)}"))
810
+
811
+ # Build DataFrame: rows = releases, columns = tracer names
812
+ df = pd.DataFrame(records, index=release_names)
813
+
814
+ # Exclude temp and salt from units row and integrated totals
815
+ integrated_tracers = [col for col in df.columns if col not in ("temp", "salt")]
816
+
817
+ # Add a row of units only for integrated tracers
818
+ tracer_meta = get_tracer_metadata_dict(include_bgc=True, unit_type="integrated")
819
+ units_row = {
820
+ col: tracer_meta.get(col, {}).get("units", "") for col in integrated_tracers
821
+ }
822
+
823
+ df_units = pd.DataFrame([units_row], index=["units"])
824
+
825
+ # Keep only integrated_tracers columns in df, drop temp and salt
826
+ df_integrated = df[integrated_tracers]
827
+
828
+ # Concatenate units row on top
829
+ df_final = pd.concat([df_units, df_integrated])
830
+
831
+ # Store dt as metadata
832
+ df_final.attrs["time_step"] = dt
833
+ df_final.attrs["start_time"] = self.start_time
834
+ df_final.attrs["end_time"] = self.end_time
835
+ df_final.attrs["title"] = "Integrated tracer releases"
836
+
837
+ return df_final
838
+
777
839
  def save(
778
840
  self,
779
841
  filepath: str | Path,
@@ -1051,3 +1113,55 @@ def _map_3d_gaussian(
1051
1113
  distribution_3d /= distribution_3d.sum()
1052
1114
 
1053
1115
  return distribution_3d
1116
+
1117
+
1118
+ def _reconstruct_roms_time_stamps(
1119
+ start_time: datetime,
1120
+ end_time: datetime,
1121
+ dt: float,
1122
+ model_reference_date: datetime,
1123
+ ) -> tuple[list[datetime], np.ndarray]:
1124
+ """
1125
+ Reconstruct ROMS time stamps between `start_time` and `end_time` with step `dt`.
1126
+
1127
+ Parameters
1128
+ ----------
1129
+ start_time : datetime
1130
+ Beginning of the time series.
1131
+ end_time : datetime
1132
+ End of the time series (inclusive if it falls exactly on a step).
1133
+ dt : float
1134
+ Time step in seconds (can be fractional if needed).
1135
+ model_reference_date : datetime
1136
+ The reference date for ROMS time (elapsed time will be relative to this).
1137
+
1138
+ Returns
1139
+ -------
1140
+ times : list of datetime
1141
+ Sequence of datetimes from `start_time` to `end_time`.
1142
+ rel_days : np.ndarray
1143
+ Array of elapsed times in **seconds** relative to `model_reference_date`.
1144
+
1145
+ Raises
1146
+ ------
1147
+ ValueError
1148
+ If `end_time` is not after `start_time` or if `dt` is not positive.
1149
+ """
1150
+ if end_time <= start_time:
1151
+ raise ValueError("end_time must be after start_time")
1152
+ if dt <= 0:
1153
+ raise ValueError("dt must be positive")
1154
+
1155
+ # Generate absolute times
1156
+ delta = timedelta(seconds=dt)
1157
+ times: list[datetime] = []
1158
+ t = start_time
1159
+ while t <= end_time:
1160
+ times.append(t)
1161
+ t += delta
1162
+
1163
+ # Convert to relative ROMS time (days since model_reference_date)
1164
+ rel_days = convert_to_relative_days(times, model_reference_date)
1165
+ rel_seconds = rel_days * 3600 * 24
1166
+
1167
+ return times, rel_seconds