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
@@ -0,0 +1,527 @@
1
+ import logging
2
+ from collections import Counter, defaultdict
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+
7
+ import numpy as np
8
+ import xarray as xr
9
+
10
+ from roms_tools.download import download_river_data
11
+ from roms_tools.setup.utils import (
12
+ assign_dates_to_climatology,
13
+ check_dataset,
14
+ gc_dist,
15
+ select_relevant_times,
16
+ )
17
+ from roms_tools.utils import load_data
18
+
19
+
20
+ @dataclass(kw_only=True)
21
+ class RiverDataset:
22
+ """Represents river data.
23
+
24
+ Parameters
25
+ ----------
26
+ filename : Union[str, Path, List[Union[str, Path]]]
27
+ The path to the data file(s). Can be a single string (with or without wildcards), a single Path object,
28
+ or a list of strings or Path objects containing multiple files.
29
+ start_time : datetime
30
+ The start time for selecting relevant data.
31
+ end_time : datetime
32
+ The end time for selecting relevant data.
33
+ dim_names: Dict[str, str]
34
+ Dictionary specifying the names of dimensions in the dataset.
35
+ Requires "station" and "time" as keys.
36
+ var_names: Dict[str, str]
37
+ Dictionary of variable names that are required in the dataset.
38
+ Requires the keys "latitude", "longitude", "flux", "ratio", and "name".
39
+ opt_var_names: Dict[str, str], optional
40
+ Dictionary of variable names that are optional in the dataset.
41
+ Defaults to an empty dictionary.
42
+ climatology : bool
43
+ Indicates whether the dataset is climatological. Defaults to False.
44
+
45
+ Attributes
46
+ ----------
47
+ ds : xr.Dataset
48
+ The xarray Dataset containing the forcing data on its original grid.
49
+ """
50
+
51
+ filename: str | Path | list[str | Path]
52
+ start_time: datetime
53
+ end_time: datetime
54
+ dim_names: dict[str, str]
55
+ var_names: dict[str, str]
56
+ opt_var_names: dict[str, str] | None = field(default_factory=dict)
57
+ climatology: bool = False
58
+ ds: xr.Dataset = field(init=False, repr=False)
59
+
60
+ def __post_init__(self):
61
+ # Validate start_time and end_time
62
+ if not isinstance(self.start_time, datetime):
63
+ raise TypeError(
64
+ f"start_time must be a datetime object, but got {type(self.start_time).__name__}."
65
+ )
66
+ if not isinstance(self.end_time, datetime):
67
+ raise TypeError(
68
+ f"end_time must be a datetime object, but got {type(self.end_time).__name__}."
69
+ )
70
+
71
+ ds = self.load_data()
72
+ ds = self.clean_up(ds)
73
+ self.check_dataset(ds)
74
+ ds = _deduplicate_river_names(
75
+ ds, self.var_names["name"], self.dim_names["station"]
76
+ )
77
+
78
+ # Select relevant times
79
+ ds = self.add_time_info(ds)
80
+ self.ds = ds
81
+
82
+ def load_data(self) -> xr.Dataset:
83
+ """Load dataset from the specified file.
84
+
85
+ Returns
86
+ -------
87
+ ds : xr.Dataset
88
+ The loaded xarray Dataset containing the forcing data.
89
+ """
90
+ ds = load_data(
91
+ self.filename, self.dim_names, use_dask=False, decode_times=False
92
+ )
93
+
94
+ return ds
95
+
96
+ def clean_up(self, ds: xr.Dataset) -> xr.Dataset:
97
+ """Decodes the 'name' variable (if byte-encoded) and updates the dataset.
98
+
99
+ This method checks if the 'name' variable is of dtype 'object' (i.e., byte-encoded),
100
+ and if so, decodes each byte array to a string and updates the dataset.
101
+ It also ensures that the 'station' dimension is of integer type.
102
+
103
+
104
+ Parameters
105
+ ----------
106
+ ds : xr.Dataset
107
+ The dataset containing the 'name' variable to decode.
108
+
109
+ Returns
110
+ -------
111
+ ds : xr.Dataset
112
+ The dataset with the decoded 'name' variable.
113
+ """
114
+ if ds[self.var_names["name"]].dtype == "object":
115
+ names = []
116
+ for i in range(len(ds[self.dim_names["station"]])):
117
+ byte_array = ds[self.var_names["name"]].isel(
118
+ **{self.dim_names["station"]: i}
119
+ )
120
+ name = _decode_string(byte_array)
121
+ names.append(name)
122
+ ds[self.var_names["name"]] = xr.DataArray(
123
+ data=names, dims=self.dim_names["station"]
124
+ )
125
+
126
+ if ds[self.dim_names["station"]].dtype == "float64":
127
+ ds[self.dim_names["station"]] = ds[self.dim_names["station"]].astype(int)
128
+
129
+ # Drop all variables that have chars dim
130
+ vars_to_drop = ["ocn_name", "stn_name", "ct_name", "cn_name", "chars"]
131
+ existing_vars = [var for var in vars_to_drop if var in ds]
132
+ ds = ds.drop_vars(existing_vars)
133
+
134
+ return ds
135
+
136
+ def check_dataset(self, ds: xr.Dataset) -> None:
137
+ """Validate required variables, dimensions, and uniqueness of river names.
138
+
139
+ Parameters
140
+ ----------
141
+ ds : xr.Dataset
142
+ The xarray Dataset to check.
143
+
144
+ Raises
145
+ ------
146
+ ValueError
147
+ If the dataset does not contain the specified variables or dimensions.
148
+ """
149
+ check_dataset(ds, self.dim_names, self.var_names, self.opt_var_names)
150
+
151
+ def add_time_info(self, ds: xr.Dataset) -> xr.Dataset:
152
+ """Dummy method to be overridden by child classes to add time information to the
153
+ dataset.
154
+
155
+ This method is intended as a placeholder and should be implemented in subclasses
156
+ to provide specific functionality for adding time-related information to the dataset.
157
+
158
+ Parameters
159
+ ----------
160
+ ds : xr.Dataset
161
+ The xarray Dataset to which time information will be added.
162
+
163
+ Returns
164
+ -------
165
+ xr.Dataset
166
+ The xarray Dataset with time information added (as implemented by child classes).
167
+ """
168
+ return ds
169
+
170
+ def select_relevant_times(self, ds) -> xr.Dataset:
171
+ """Select a subset of the dataset based on the specified time range.
172
+
173
+ This method filters the dataset to include all records between `start_time` and `end_time`.
174
+ Additionally, it ensures that one record at or before `start_time` and one record at or
175
+ after `end_time` are included, even if they fall outside the strict time range.
176
+
177
+ If no `end_time` is specified, the method will select the time range of
178
+ [start_time, start_time + 24 hours] and return the closest time entry to `start_time` within that range.
179
+
180
+ Parameters
181
+ ----------
182
+ ds : xr.Dataset
183
+ The input dataset to be filtered. Must contain a time dimension.
184
+
185
+ Returns
186
+ -------
187
+ xr.Dataset
188
+ A dataset filtered to the specified time range, including the closest entries
189
+ at or before `start_time` and at or after `end_time` if applicable.
190
+
191
+ Warns
192
+ -----
193
+ UserWarning
194
+ If no records at or before `start_time` or no records at or after `end_time` are found.
195
+
196
+ UserWarning
197
+ If the dataset does not contain any time dimension or the time dimension is incorrectly named.
198
+ """
199
+ time_dim = self.dim_names["time"]
200
+
201
+ ds = select_relevant_times(ds, time_dim, self.start_time, self.end_time, False)
202
+
203
+ return ds
204
+
205
+ def compute_climatology(self):
206
+ logging.info("Compute climatology for river forcing.")
207
+
208
+ time_dim = self.dim_names["time"]
209
+
210
+ flux = self.ds[self.var_names["flux"]].groupby(f"{time_dim}.month").mean()
211
+ self.ds[self.var_names["flux"]] = flux
212
+
213
+ ds = assign_dates_to_climatology(self.ds, "month")
214
+ ds = ds.swap_dims({"month": "time"})
215
+ self.ds = ds
216
+
217
+ updated_dim_names = {**self.dim_names}
218
+ updated_dim_names["time"] = "time"
219
+ self.dim_names = updated_dim_names
220
+
221
+ self.climatology = True
222
+
223
+ def sort_by_river_volume(self, ds: xr.Dataset) -> xr.Dataset:
224
+ """Sorts the dataset by river volume in descending order (largest rivers first),
225
+ if the volume variable is available.
226
+
227
+ This method uses the river volume to reorder the dataset such that the rivers with
228
+ the largest volumes come first in the `station` dimension. If the volume variable
229
+ is not present in the dataset, a warning is logged.
230
+
231
+ Parameters
232
+ ----------
233
+ ds : xr.Dataset
234
+ The xarray Dataset containing the river data to be sorted by volume.
235
+
236
+ Returns
237
+ -------
238
+ xr.Dataset
239
+ The dataset with rivers sorted by their volume in descending order.
240
+ If the volume variable is not available, the original dataset is returned.
241
+ """
242
+ if self.opt_var_names is not None and "vol" in self.opt_var_names:
243
+ volume_values = ds[self.opt_var_names["vol"]].values
244
+ if isinstance(volume_values, np.ndarray):
245
+ # Check if all volume values are the same
246
+ if np.all(volume_values == volume_values[0]):
247
+ # If all volumes are the same, no need to reverse order
248
+ sorted_indices = np.argsort(
249
+ volume_values
250
+ ) # Sort in ascending order
251
+ else:
252
+ # If volumes differ, reverse order for descending sort
253
+ sorted_indices = np.argsort(volume_values)[
254
+ ::-1
255
+ ] # Reverse for descending order
256
+
257
+ ds = ds.isel(**{self.dim_names["station"]: sorted_indices})
258
+
259
+ else:
260
+ logging.warning("The volume data is not in a valid array format.")
261
+ else:
262
+ logging.warning(
263
+ "Cannot sort rivers by volume. 'vol' is missing in the variable names."
264
+ )
265
+
266
+ return ds
267
+
268
+ def extract_relevant_rivers(self, target_coords, dx):
269
+ """Extracts a subset of the dataset based on the proximity of river mouths to
270
+ target coordinates.
271
+
272
+ This method calculates the distance between each river mouth and the provided target coordinates
273
+ (latitude and longitude) using the `gc_dist` function. It then filters the dataset to include only those
274
+ river stations whose minimum distance from the target is less than a specified threshold distance (`dx`).
275
+
276
+ Parameters
277
+ ----------
278
+ target_coords : dict
279
+ A dictionary containing the target coordinates for the comparison. It should include:
280
+ - "lon" (float): The target longitude in degrees.
281
+ - "lat" (float): The target latitude in degrees.
282
+ - "straddle" (bool): A flag indicating whether to adjust the longitudes for stations that cross the
283
+ International Date Line. If `True`, longitudes greater than 180 degrees are adjusted by subtracting 360,
284
+ otherwise, negative longitudes are adjusted by adding 360.
285
+
286
+ dx : float
287
+ The maximum distance threshold (in meters) for including a river station. Only river mouths that are
288
+ within `dx` meters from the target coordinates will be included in the returned dataset.
289
+
290
+ Returns
291
+ -------
292
+ indices : dict[str, list[tuple]]
293
+ A dictionary containing the indices of the rivers that are within the threshold distance from
294
+ the target coordinates. The dictionary structure consists of river names as keys, and each value is a list of tuples. Each tuple represents
295
+ a pair of indices corresponding to the `eta_rho` and `xi_rho` grid coordinates of the river.
296
+ """
297
+ # Retrieve longitude and latitude of river mouths
298
+ river_lon = self.ds[self.var_names["longitude"]]
299
+ river_lat = self.ds[self.var_names["latitude"]]
300
+
301
+ # Adjust longitude based on whether it crosses the International Date Line (straddle case)
302
+ if target_coords["straddle"]:
303
+ river_lon = xr.where(river_lon > 180, river_lon - 360, river_lon)
304
+ else:
305
+ river_lon = xr.where(river_lon < 0, river_lon + 360, river_lon)
306
+
307
+ # Calculate the distance between the target coordinates and each river mouth
308
+ dist = gc_dist(target_coords["lon"], target_coords["lat"], river_lon, river_lat)
309
+ dist_min = dist.min(dim=["eta_rho", "xi_rho"])
310
+ # Filter the dataset to include only stations within the distance threshold
311
+ if (dist_min < dx).any():
312
+ ds = self.ds.where(dist_min < dx, drop=True)
313
+ ds = self.sort_by_river_volume(ds)
314
+ dist = dist.where(dist_min < dx, drop=True).transpose(
315
+ self.dim_names["station"], "eta_rho", "xi_rho"
316
+ )
317
+
318
+ river_indices = get_indices_of_nearest_grid_cell_for_rivers(dist, self)
319
+ else:
320
+ ds = xr.Dataset()
321
+ river_indices = {}
322
+
323
+ self.ds = ds
324
+
325
+ return river_indices
326
+
327
+ def extract_named_rivers(self, indices):
328
+ """Extracts a subset of the dataset based on the provided river names in the
329
+ indices dictionary.
330
+
331
+ This method filters the dataset to include only the rivers specified in the `indices` dictionary.
332
+ The resulting subset is stored in the `ds` attribute of the class.
333
+
334
+ Parameters
335
+ ----------
336
+ indices : dict
337
+ A dictionary where the keys are river names (strings) and the values are dictionaries
338
+ containing river-related data (e.g., river indices, coordinates).
339
+
340
+ Returns
341
+ -------
342
+ None
343
+ The method modifies the `self.ds` attribute in place, setting it to the filtered dataset
344
+ containing only the data related to the specified rivers.
345
+
346
+ Raises
347
+ ------
348
+ ValueError
349
+ - If `indices` is not a dictionary.
350
+ - If any of the requested river names are not found in the dataset.
351
+ """
352
+ if not isinstance(indices, dict):
353
+ raise ValueError("`indices` must be a dictionary.")
354
+
355
+ river_names = list(indices.keys())
356
+
357
+ # Ensure the dataset is filtered based on the provided river names
358
+ ds_filtered = self.ds.where(
359
+ self.ds[self.var_names["name"]].isin(river_names), drop=True
360
+ )
361
+
362
+ # Check that all requested rivers exist in the dataset
363
+ filtered_river_names = set(ds_filtered[self.var_names["name"]].values)
364
+ missing_rivers = set(river_names) - filtered_river_names
365
+
366
+ if missing_rivers:
367
+ raise ValueError(
368
+ f"The following rivers were not found in the dataset: {missing_rivers}"
369
+ )
370
+
371
+ # Set the filtered dataset as the new `ds`
372
+ self.ds = ds_filtered
373
+
374
+
375
+ @dataclass(kw_only=True)
376
+ class DaiRiverDataset(RiverDataset):
377
+ """Represents river data from the Dai river dataset."""
378
+
379
+ filename: str | Path | list[str | Path] = field(
380
+ default_factory=lambda: download_river_data("dai_trenberth_may2019.nc")
381
+ )
382
+ dim_names: dict[str, str] = field(
383
+ default_factory=lambda: {
384
+ "station": "station",
385
+ "time": "time",
386
+ }
387
+ )
388
+ var_names: dict[str, str] = field(
389
+ default_factory=lambda: {
390
+ "latitude": "lat_mou",
391
+ "longitude": "lon_mou",
392
+ "flux": "FLOW",
393
+ "ratio": "ratio_m2s",
394
+ "name": "riv_name",
395
+ }
396
+ )
397
+ opt_var_names: dict[str, str] = field(
398
+ default_factory=lambda: {
399
+ "vol": "vol_stn",
400
+ }
401
+ )
402
+ climatology: bool = False
403
+
404
+ def add_time_info(self, ds: xr.Dataset) -> xr.Dataset:
405
+ """Adds time information to the dataset based on the climatology flag and
406
+ dimension names.
407
+
408
+ This method processes the dataset to include time information according to the climatology
409
+ setting. If the dataset represents climatology data and the time dimension is labeled as
410
+ "month", it assigns dates to the dataset based on a monthly climatology. Additionally, it
411
+ handles dimension name updates if necessary.
412
+
413
+ Parameters
414
+ ----------
415
+ ds : xr.Dataset
416
+ The input dataset to which time information will be added.
417
+
418
+ Returns
419
+ -------
420
+ xr.Dataset
421
+ The dataset with time information added, including adjustments for climatology and
422
+ dimension names.
423
+ """
424
+ time_dim = self.dim_names["time"]
425
+
426
+ # Extract the 'time' variable as a numpy array
427
+ time_vals = ds[time_dim].values
428
+
429
+ # Handle rounding of the time values
430
+ year = np.round(time_vals * 1e-2).astype(int)
431
+ month = np.round((time_vals * 1e-2 - year) * 1e2).astype(int)
432
+
433
+ # Convert to datetime (assuming the day is always 15th for this example)
434
+ dates = [datetime(year=i, month=m, day=15) for i, m in zip(year, month)]
435
+
436
+ ds[time_dim] = dates
437
+
438
+ return ds
439
+
440
+
441
+ def _decode_string(byte_array):
442
+ # Decode each byte and handle errors with 'ignore'
443
+ decoded_string = "".join(
444
+ [
445
+ x.decode("utf-8", errors="ignore") # Ignore invalid byte sequences
446
+ for x in byte_array.values
447
+ if isinstance(x, bytes) and x != b" " and x is not np.nan
448
+ ]
449
+ )
450
+
451
+ return decoded_string
452
+
453
+
454
+ def get_indices_of_nearest_grid_cell_for_rivers(
455
+ dist: xr.DataArray, data: RiverDataset
456
+ ) -> dict[str, list[tuple[int, int]]]:
457
+ """Get the indices of the nearest grid cell for each river based on distance.
458
+
459
+ Parameters
460
+ ----------
461
+ dist : xr.DataArray
462
+ A 2D or 3D array representing distances from each river to coastal grid cells,
463
+ with dimensions including "eta_rho" and "xi_rho".
464
+ data : RiverDataset
465
+ An instance of RiverDataset containing river names and dimension metadata.
466
+
467
+ Returns
468
+ -------
469
+ dict[str, list[tuple[int, int]]]
470
+ Dictionary mapping each river name to a list containing the (eta_rho, xi_rho) index
471
+ of the closest coastal grid cell.
472
+ """
473
+ # Find indices of the nearest coastal grid cell for each river
474
+ indices = dist.argmin(dim=["eta_rho", "xi_rho"])
475
+
476
+ eta_rho_values = indices["eta_rho"].values
477
+ xi_rho_values = indices["xi_rho"].values
478
+
479
+ # Get the corresponding station indices and river names
480
+ stations = indices["eta_rho"][data.dim_names["station"]].values
481
+ names = (
482
+ data.ds[data.var_names["name"]]
483
+ .sel({data.dim_names["station"]: stations})
484
+ .values
485
+ )
486
+
487
+ # Build dictionary of river name to grid index
488
+ river_indices = {
489
+ str(names[i]): [(int(eta_rho_values[i]), int(xi_rho_values[i]))]
490
+ for i in range(len(stations))
491
+ }
492
+
493
+ return river_indices
494
+
495
+
496
+ def _deduplicate_river_names(
497
+ ds: xr.Dataset, name_var: str, station_dim: str
498
+ ) -> xr.Dataset:
499
+ """Ensure river names are unique by appending _1, _2 to duplicates, excluding non-
500
+ duplicates.
501
+ """
502
+ original = ds[name_var]
503
+
504
+ # Force cast to plain Python strings
505
+ names = [str(name) for name in original.values]
506
+
507
+ # Count all names
508
+ name_counts = Counter(names)
509
+ seen: defaultdict[str, int] = defaultdict(int)
510
+
511
+ unique_names = []
512
+ for name in names:
513
+ if name_counts[name] > 1:
514
+ seen[name] += 1
515
+ unique_names.append(f"{name}_{seen[name]}")
516
+ else:
517
+ unique_names.append(name)
518
+
519
+ # Replace with updated names while preserving dtype, dims, attrs
520
+ updated_array = xr.DataArray(
521
+ data=np.array(unique_names, dtype=f"<U{max(len(n) for n in unique_names)}"),
522
+ dims=original.dims,
523
+ attrs=original.attrs,
524
+ )
525
+ ds[name_var] = updated_array
526
+
527
+ return ds