disdrodb 0.2.1__py3-none-any.whl → 0.4.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 (313) hide show
  1. disdrodb/__init__.py +3 -1
  2. disdrodb/_config.py +2 -3
  3. disdrodb/_version.py +2 -2
  4. disdrodb/accessor/__init__.py +2 -1
  5. disdrodb/accessor/methods.py +10 -9
  6. disdrodb/api/checks.py +3 -7
  7. disdrodb/api/configs.py +1 -3
  8. disdrodb/api/create_directories.py +4 -6
  9. disdrodb/api/info.py +1 -3
  10. disdrodb/api/io.py +233 -32
  11. disdrodb/api/path.py +3 -7
  12. disdrodb/cli/disdrodb_check_metadata_archive.py +3 -2
  13. disdrodb/cli/disdrodb_check_products_options.py +45 -0
  14. disdrodb/cli/disdrodb_create_summary.py +54 -28
  15. disdrodb/cli/disdrodb_create_summary_station.py +41 -20
  16. disdrodb/cli/disdrodb_data_archive_directory.py +2 -3
  17. disdrodb/cli/disdrodb_download_archive.py +50 -30
  18. disdrodb/cli/disdrodb_download_metadata_archive.py +28 -16
  19. disdrodb/cli/disdrodb_download_station.py +58 -29
  20. disdrodb/cli/disdrodb_initialize_station.py +43 -23
  21. disdrodb/cli/disdrodb_metadata_archive_directory.py +2 -3
  22. disdrodb/cli/disdrodb_open_data_archive.py +17 -13
  23. disdrodb/cli/disdrodb_open_logs_directory.py +31 -21
  24. disdrodb/cli/disdrodb_open_metadata_archive.py +26 -13
  25. disdrodb/cli/disdrodb_open_metadata_directory.py +34 -23
  26. disdrodb/cli/disdrodb_open_product_directory.py +39 -23
  27. disdrodb/cli/disdrodb_open_readers_directory.py +2 -3
  28. disdrodb/cli/disdrodb_run.py +189 -0
  29. disdrodb/cli/disdrodb_run_l0.py +61 -70
  30. disdrodb/cli/disdrodb_run_l0_station.py +50 -55
  31. disdrodb/cli/disdrodb_run_l0a.py +53 -51
  32. disdrodb/cli/disdrodb_run_l0a_station.py +41 -40
  33. disdrodb/cli/disdrodb_run_l0b.py +51 -51
  34. disdrodb/cli/disdrodb_run_l0b_station.py +40 -39
  35. disdrodb/cli/disdrodb_run_l0c.py +56 -53
  36. disdrodb/cli/disdrodb_run_l0c_station.py +44 -41
  37. disdrodb/cli/disdrodb_run_l1.py +55 -51
  38. disdrodb/cli/disdrodb_run_l1_station.py +43 -40
  39. disdrodb/cli/disdrodb_run_l2e.py +56 -51
  40. disdrodb/cli/disdrodb_run_l2e_station.py +44 -40
  41. disdrodb/cli/disdrodb_run_l2m.py +55 -51
  42. disdrodb/cli/disdrodb_run_l2m_station.py +43 -40
  43. disdrodb/cli/disdrodb_run_station.py +184 -0
  44. disdrodb/cli/disdrodb_upload_archive.py +51 -42
  45. disdrodb/cli/disdrodb_upload_station.py +42 -36
  46. disdrodb/configs.py +20 -16
  47. disdrodb/constants.py +5 -2
  48. disdrodb/data_transfer/__init__.py +1 -3
  49. disdrodb/data_transfer/download_data.py +45 -61
  50. disdrodb/data_transfer/upload_data.py +7 -11
  51. disdrodb/data_transfer/zenodo.py +2 -4
  52. disdrodb/docs.py +1 -3
  53. disdrodb/etc/configs/attributes.yaml +52 -2
  54. disdrodb/etc/configs/encodings.yaml +45 -1
  55. disdrodb/etc/products/L0C/ODM470/global.yaml +5 -0
  56. disdrodb/etc/products/L0C/global.yaml +5 -0
  57. disdrodb/etc/products/L1/ODM470/global.yaml +6 -0
  58. disdrodb/etc/products/L1/global.yaml +0 -13
  59. disdrodb/etc/products/L2E/LPM/1MIN.yaml +1 -0
  60. disdrodb/etc/products/L2E/LPM/global.yaml +36 -0
  61. disdrodb/etc/products/L2E/LPM_V0/1MIN.yaml +1 -0
  62. disdrodb/etc/products/L2E/LPM_V0/global.yaml +36 -0
  63. disdrodb/etc/products/L2E/ODM470/1MIN.yaml +1 -0
  64. disdrodb/etc/products/L2E/ODM470/global.yaml +36 -0
  65. disdrodb/etc/products/L2E/PARSIVEL/1MIN.yaml +1 -0
  66. disdrodb/etc/products/L2E/PARSIVEL/global.yaml +36 -0
  67. disdrodb/etc/products/L2E/PARSIVEL2/1MIN.yaml +1 -0
  68. disdrodb/etc/products/L2E/PARSIVEL2/global.yaml +36 -0
  69. disdrodb/etc/products/L2E/PWS100/1MIN.yaml +1 -0
  70. disdrodb/etc/products/L2E/PWS100/global.yaml +36 -0
  71. disdrodb/etc/products/L2E/RD80/1MIN.yaml +19 -0
  72. disdrodb/etc/products/L2E/SWS250/1MIN.yaml +19 -0
  73. disdrodb/etc/products/L2E/global.yaml +16 -2
  74. disdrodb/fall_velocity/__init__.py +47 -0
  75. disdrodb/fall_velocity/graupel.py +484 -0
  76. disdrodb/fall_velocity/hail.py +288 -0
  77. disdrodb/{l1/fall_velocity.py → fall_velocity/rain.py} +265 -44
  78. disdrodb/issue/__init__.py +1 -3
  79. disdrodb/issue/checks.py +2 -3
  80. disdrodb/issue/reader.py +2 -3
  81. disdrodb/issue/writer.py +2 -5
  82. disdrodb/l0/__init__.py +2 -1
  83. disdrodb/l0/check_configs.py +36 -29
  84. disdrodb/l0/check_standards.py +1 -4
  85. disdrodb/l0/configs/LPM/l0a_encodings.yml +17 -17
  86. disdrodb/l0/configs/LPM/l0b_cf_attrs.yml +55 -55
  87. disdrodb/l0/configs/LPM/l0b_encodings.yml +17 -17
  88. disdrodb/l0/configs/LPM/raw_data_format.yml +17 -17
  89. disdrodb/l0/configs/LPM_V0/l0a_encodings.yml +2 -2
  90. disdrodb/l0/configs/LPM_V0/l0b_cf_attrs.yml +2 -2
  91. disdrodb/l0/configs/LPM_V0/l0b_encodings.yml +2 -2
  92. disdrodb/l0/configs/LPM_V0/raw_data_format.yml +2 -2
  93. disdrodb/l0/configs/ODM470/bins_diameter.yml +643 -0
  94. disdrodb/l0/configs/ODM470/bins_velocity.yml +0 -0
  95. disdrodb/l0/configs/ODM470/l0a_encodings.yml +11 -0
  96. disdrodb/l0/configs/ODM470/l0b_cf_attrs.yml +46 -0
  97. disdrodb/l0/configs/ODM470/l0b_encodings.yml +106 -0
  98. disdrodb/l0/configs/ODM470/raw_data_format.yml +111 -0
  99. disdrodb/l0/configs/PARSIVEL/l0b_cf_attrs.yml +1 -1
  100. disdrodb/l0/l0_reader.py +2 -3
  101. disdrodb/l0/l0a_processing.py +6 -8
  102. disdrodb/l0/l0b_nc_processing.py +3 -6
  103. disdrodb/l0/l0b_processing.py +2 -16
  104. disdrodb/l0/l0c_processing.py +29 -12
  105. disdrodb/l0/readers/LPM/ARM/ARM_LPM.py +2 -1
  106. disdrodb/l0/readers/LPM/AUSTRALIA/MELBOURNE_2007_LPM.py +18 -18
  107. disdrodb/l0/readers/LPM/BRAZIL/CHUVA_LPM.py +18 -18
  108. disdrodb/l0/readers/LPM/BRAZIL/GOAMAZON_LPM.py +18 -18
  109. disdrodb/l0/readers/LPM/GERMANY/DWD.py +244 -63
  110. disdrodb/l0/readers/LPM/ITALY/GID_LPM.py +65 -23
  111. disdrodb/l0/readers/LPM/ITALY/GID_LPM_AQ.py +277 -0
  112. disdrodb/l0/readers/LPM/ITALY/GID_LPM_PI.py +19 -18
  113. disdrodb/l0/readers/LPM/ITALY/GID_LPM_T.py +23 -19
  114. disdrodb/l0/readers/LPM/ITALY/GID_LPM_W.py +19 -21
  115. disdrodb/l0/readers/LPM/KIT/CHWALA.py +19 -20
  116. disdrodb/l0/readers/LPM/NETHERLANDS/DELFT_LPM_NC.py +1 -1
  117. disdrodb/l0/readers/LPM/NETHERLANDS/DELFT_RWANDA_LPM_NC.py +18 -18
  118. disdrodb/l0/readers/LPM/NORWAY/HAUKELISETER_LPM.py +19 -20
  119. disdrodb/l0/readers/LPM/NORWAY/NMBU_LPM.py +19 -20
  120. disdrodb/l0/readers/LPM/SLOVENIA/ARSO.py +19 -20
  121. disdrodb/l0/readers/LPM/SLOVENIA/UL.py +19 -20
  122. disdrodb/l0/readers/LPM/SWITZERLAND/INNERERIZ_LPM.py +19 -20
  123. disdrodb/l0/readers/LPM/UK/DIVEN.py +1 -1
  124. disdrodb/l0/readers/LPM/UK/WITHWORTH_LPM.py +19 -20
  125. disdrodb/l0/readers/LPM/USA/CHARLESTON.py +19 -20
  126. disdrodb/l0/readers/LPM/USA/DEVEX.py +255 -0
  127. disdrodb/l0/readers/LPM_V0/BELGIUM/ULIEGE.py +3 -5
  128. disdrodb/l0/readers/LPM_V0/ITALY/GID_LPM_V0.py +4 -3
  129. disdrodb/l0/readers/ODM470/OCEAN/OCEANRAIN.py +124 -0
  130. disdrodb/l0/readers/PARSIVEL/AUSTRALIA/MELBOURNE_2007_PARSIVEL.py +1 -1
  131. disdrodb/l0/readers/PARSIVEL/BASQUECOUNTRY/EUSKALMET_OTT.py +2 -1
  132. disdrodb/l0/readers/PARSIVEL/CHINA/CHONGQING.py +2 -3
  133. disdrodb/l0/readers/PARSIVEL/EPFL/ARCTIC_2021.py +2 -1
  134. disdrodb/l0/readers/PARSIVEL/EPFL/COMMON_2011.py +2 -1
  135. disdrodb/l0/readers/PARSIVEL/EPFL/DAVOS_2009_2011.py +2 -1
  136. disdrodb/l0/readers/PARSIVEL/EPFL/EPFL_2009.py +2 -1
  137. disdrodb/l0/readers/PARSIVEL/EPFL/EPFL_ROOF_2008.py +2 -1
  138. disdrodb/l0/readers/PARSIVEL/EPFL/EPFL_ROOF_2010.py +2 -1
  139. disdrodb/l0/readers/PARSIVEL/EPFL/EPFL_ROOF_2011.py +2 -1
  140. disdrodb/l0/readers/PARSIVEL/EPFL/EPFL_ROOF_2012.py +2 -1
  141. disdrodb/l0/readers/PARSIVEL/EPFL/GENEPI_2007.py +2 -1
  142. disdrodb/l0/readers/PARSIVEL/EPFL/GRAND_ST_BERNARD_2007.py +2 -1
  143. disdrodb/l0/readers/PARSIVEL/EPFL/GRAND_ST_BERNARD_2007_2.py +2 -1
  144. disdrodb/l0/readers/PARSIVEL/EPFL/HPICONET_2010.py +2 -1
  145. disdrodb/l0/readers/PARSIVEL/EPFL/HYMEX_LTE_SOP2.py +2 -1
  146. disdrodb/l0/readers/PARSIVEL/EPFL/HYMEX_LTE_SOP3.py +2 -1
  147. disdrodb/l0/readers/PARSIVEL/EPFL/HYMEX_LTE_SOP4.py +2 -1
  148. disdrodb/l0/readers/PARSIVEL/EPFL/LOCARNO_2018.py +1 -1
  149. disdrodb/l0/readers/PARSIVEL/EPFL/LOCARNO_2019.py +1 -1
  150. disdrodb/l0/readers/PARSIVEL/EPFL/PARADISO_2014.py +2 -1
  151. disdrodb/l0/readers/PARSIVEL/EPFL/PARSIVEL_2007.py +2 -1
  152. disdrodb/l0/readers/PARSIVEL/EPFL/PLATO_2019.py +1 -1
  153. disdrodb/l0/readers/PARSIVEL/EPFL/RACLETS_2019.py +2 -1
  154. disdrodb/l0/readers/PARSIVEL/EPFL/RACLETS_2019_WJF.py +2 -1
  155. disdrodb/l0/readers/PARSIVEL/EPFL/RIETHOLZBACH_2011.py +2 -1
  156. disdrodb/l0/readers/PARSIVEL/EPFL/SAMOYLOV_2017.py +2 -1
  157. disdrodb/l0/readers/PARSIVEL/EPFL/SAMOYLOV_2019.py +2 -1
  158. disdrodb/l0/readers/PARSIVEL/EPFL/UNIL_2022.py +2 -1
  159. disdrodb/l0/readers/PARSIVEL/JAPAN/JMA.py +1 -1
  160. disdrodb/l0/readers/PARSIVEL/KOREA/ICEPOP_MSC.py +159 -0
  161. disdrodb/l0/readers/PARSIVEL/NASA/LPVEX.py +1 -1
  162. disdrodb/l0/readers/PARSIVEL/NASA/MC3E.py +1 -1
  163. disdrodb/l0/readers/PARSIVEL/NCAR/CCOPE_2015.py +1 -1
  164. disdrodb/l0/readers/PARSIVEL/NCAR/OWLES_MIPS.py +1 -1
  165. disdrodb/l0/readers/PARSIVEL/NCAR/PECAN_MOBILE.py +1 -1
  166. disdrodb/l0/readers/PARSIVEL/NCAR/PLOWS_MIPS.py +1 -1
  167. disdrodb/l0/readers/PARSIVEL/NCAR/VORTEX2_2009.py +1 -1
  168. disdrodb/l0/readers/PARSIVEL/NCAR/VORTEX2_2010.py +1 -3
  169. disdrodb/l0/readers/PARSIVEL/NCAR/VORTEX2_2010_UF.py +1 -3
  170. disdrodb/l0/readers/PARSIVEL/SLOVENIA/UL.py +2 -1
  171. disdrodb/l0/readers/PARSIVEL2/ARM/ARM_PARSIVEL2.py +2 -1
  172. disdrodb/l0/readers/PARSIVEL2/BASQUECOUNTRY/EUSKALMET_OTT2.py +2 -1
  173. disdrodb/l0/readers/PARSIVEL2/BELGIUM/ILVO.py +2 -3
  174. disdrodb/l0/readers/PARSIVEL2/BRAZIL/CHUVA_PARSIVEL2.py +1 -1
  175. disdrodb/l0/readers/PARSIVEL2/BRAZIL/GOAMAZON_PARSIVEL2.py +1 -1
  176. disdrodb/l0/readers/PARSIVEL2/CANADA/UQAM_NC.py +1 -1
  177. disdrodb/l0/readers/PARSIVEL2/DENMARK/DTU.py +1 -1
  178. disdrodb/l0/readers/PARSIVEL2/DENMARK/EROSION_nc.py +2 -1
  179. disdrodb/l0/readers/PARSIVEL2/DENMARK/EROSION_raw.py +2 -1
  180. disdrodb/l0/readers/PARSIVEL2/FINLAND/FMI_PARSIVEL2.py +1 -1
  181. disdrodb/l0/readers/PARSIVEL2/FRANCE/ENPC_PARSIVEL2.py +2 -3
  182. disdrodb/l0/readers/PARSIVEL2/FRANCE/OSUG.py +2 -2
  183. disdrodb/l0/readers/PARSIVEL2/FRANCE/SIRTA_PARSIVEL2.py +1 -3
  184. disdrodb/l0/readers/PARSIVEL2/GREECE/NOA.py +4 -3
  185. disdrodb/l0/readers/PARSIVEL2/ITALY/GID_PARSIVEL2.py +1 -3
  186. disdrodb/l0/readers/PARSIVEL2/ITALY/HYDROX.py +6 -3
  187. disdrodb/l0/readers/PARSIVEL2/JAPAN/PRECIP.py +1 -1
  188. disdrodb/l0/readers/PARSIVEL2/KIT/BURKINA_FASO.py +1 -1
  189. disdrodb/l0/readers/PARSIVEL2/KIT/TEAMX.py +1 -1
  190. disdrodb/l0/readers/PARSIVEL2/KOREA/ICEPOP_MSC.py +161 -0
  191. disdrodb/l0/readers/PARSIVEL2/KOREA/ICEPOP_UCLM.py +126 -0
  192. disdrodb/l0/readers/PARSIVEL2/MEXICO/OH_IIUNAM_nc.py +2 -1
  193. disdrodb/l0/readers/PARSIVEL2/MPI/BCO_PARSIVEL2.py +1 -1
  194. disdrodb/l0/readers/PARSIVEL2/MPI/BOWTIE.py +1 -1
  195. disdrodb/l0/readers/PARSIVEL2/NASA/APU.py +3 -1
  196. disdrodb/l0/readers/PARSIVEL2/NASA/NSSTC.py +1 -1
  197. disdrodb/l0/readers/PARSIVEL2/NCAR/FARM_PARSIVEL2.py +2 -1
  198. disdrodb/l0/readers/PARSIVEL2/NCAR/PECAN_FP3.py +1 -1
  199. disdrodb/l0/readers/PARSIVEL2/NCAR/PECAN_MIPS.py +1 -1
  200. disdrodb/l0/readers/PARSIVEL2/NCAR/PERILS_MIPS.py +2 -1
  201. disdrodb/l0/readers/PARSIVEL2/NCAR/PERILS_PIPS.py +2 -1
  202. disdrodb/l0/readers/PARSIVEL2/NCAR/RELAMPAGO_PARSIVEL2.py +1 -1
  203. disdrodb/l0/readers/PARSIVEL2/NCAR/SNOWIE_PJ.py +1 -1
  204. disdrodb/l0/readers/PARSIVEL2/NCAR/SNOWIE_SB.py +1 -1
  205. disdrodb/l0/readers/PARSIVEL2/NCAR/VORTEX_SE_2016_P1.py +2 -3
  206. disdrodb/l0/readers/PARSIVEL2/NCAR/VORTEX_SE_2016_P2.py +1 -1
  207. disdrodb/l0/readers/PARSIVEL2/NCAR/VORTEX_SE_2016_PIPS.py +2 -1
  208. disdrodb/l0/readers/PARSIVEL2/NETHERLANDS/DELFT_NC.py +1 -1
  209. disdrodb/l0/readers/PARSIVEL2/NORWAY/UIB.py +10 -2
  210. disdrodb/l0/readers/PARSIVEL2/PHILIPPINES/PAGASA.py +2 -3
  211. disdrodb/l0/readers/PARSIVEL2/SPAIN/CENER.py +1 -1
  212. disdrodb/l0/readers/PARSIVEL2/SPAIN/CR1000DL.py +1 -1
  213. disdrodb/l0/readers/PARSIVEL2/SPAIN/GRANADA.py +2 -3
  214. disdrodb/l0/readers/PARSIVEL2/SPAIN/LIAISE.py +1 -1
  215. disdrodb/l0/readers/PARSIVEL2/SWEDEN/SMHI.py +2 -1
  216. disdrodb/l0/readers/PARSIVEL2/USA/CSU.py +1 -1
  217. disdrodb/l0/readers/PARSIVEL2/USA/CW3E.py +2 -1
  218. disdrodb/l0/readers/PWS100/AUSTRIA/HOAL.py +2 -3
  219. disdrodb/l0/readers/PWS100/FRANCE/ENPC_PWS100.py +2 -3
  220. disdrodb/l0/readers/PWS100/FRANCE/ENPC_PWS100_SIRTA.py +2 -1
  221. disdrodb/l0/readers/RD80/BRAZIL/ATTO_RD80.py +1 -3
  222. disdrodb/l0/readers/RD80/BRAZIL/CHUVA_RD80.py +1 -3
  223. disdrodb/l0/readers/RD80/BRAZIL/GOAMAZON_RD80.py +1 -3
  224. disdrodb/l0/readers/RD80/NCAR/CINDY_2011_RD80.py +1 -3
  225. disdrodb/l0/readers/RD80/NCAR/RELAMPAGO_RD80.py +1 -3
  226. disdrodb/l0/readers/RD80/NOAA/PSL_RD80.py +2 -3
  227. disdrodb/l0/readers/SWS250/BELGIUM/KMI.py +2 -3
  228. disdrodb/l0/readers/template_reader_raw_netcdf_data.py +2 -3
  229. disdrodb/l0/readers/template_reader_raw_text_data.py +2 -3
  230. disdrodb/l0/standards.py +4 -5
  231. disdrodb/l0/template_tools.py +7 -11
  232. disdrodb/l1/__init__.py +2 -1
  233. disdrodb/l1/classification.py +914 -0
  234. disdrodb/l1/processing.py +36 -106
  235. disdrodb/l1/resampling.py +13 -3
  236. disdrodb/l1_env/__init__.py +1 -1
  237. disdrodb/l1_env/routines.py +7 -6
  238. disdrodb/l2/__init__.py +2 -1
  239. disdrodb/l2/empirical_dsd.py +58 -31
  240. disdrodb/l2/processing.py +327 -61
  241. disdrodb/metadata/checks.py +10 -13
  242. disdrodb/metadata/download.py +5 -4
  243. disdrodb/metadata/geolocation.py +3 -4
  244. disdrodb/metadata/info.py +3 -5
  245. disdrodb/metadata/manipulation.py +1 -3
  246. disdrodb/metadata/reader.py +1 -3
  247. disdrodb/metadata/search.py +1 -4
  248. disdrodb/metadata/standards.py +1 -3
  249. disdrodb/metadata/writer.py +1 -3
  250. disdrodb/physics/__init__.py +17 -0
  251. disdrodb/physics/atmosphere.py +273 -0
  252. disdrodb/physics/water.py +131 -0
  253. disdrodb/physics/wrappers.py +63 -0
  254. disdrodb/psd/__init__.py +1 -2
  255. disdrodb/psd/fitting.py +23 -9
  256. disdrodb/psd/models.py +2 -1
  257. disdrodb/routines/__init__.py +6 -1
  258. disdrodb/routines/l0.py +39 -25
  259. disdrodb/routines/l1.py +23 -16
  260. disdrodb/routines/l2.py +12 -9
  261. disdrodb/routines/options.py +117 -73
  262. disdrodb/routines/options_validation.py +728 -0
  263. disdrodb/routines/wrappers.py +460 -40
  264. disdrodb/scattering/__init__.py +1 -2
  265. disdrodb/scattering/axis_ratio.py +6 -6
  266. disdrodb/scattering/permittivity.py +9 -8
  267. disdrodb/scattering/routines.py +33 -15
  268. disdrodb/summary/__init__.py +1 -1
  269. disdrodb/summary/routines.py +95 -30
  270. disdrodb/utils/__init__.py +1 -1
  271. disdrodb/utils/archiving.py +18 -10
  272. disdrodb/utils/attrs.py +7 -5
  273. disdrodb/utils/cli.py +8 -10
  274. disdrodb/utils/compression.py +10 -13
  275. disdrodb/utils/coords.py +45 -0
  276. disdrodb/utils/dask.py +7 -5
  277. disdrodb/utils/dataframe.py +5 -6
  278. disdrodb/utils/decorators.py +3 -4
  279. disdrodb/utils/dict.py +1 -1
  280. disdrodb/utils/directories.py +5 -7
  281. disdrodb/utils/encoding.py +4 -5
  282. disdrodb/utils/event.py +1 -1
  283. disdrodb/utils/list.py +1 -3
  284. disdrodb/utils/logger.py +1 -3
  285. disdrodb/utils/manipulations.py +175 -4
  286. disdrodb/utils/pydantic.py +81 -0
  287. disdrodb/utils/routines.py +2 -3
  288. disdrodb/utils/subsetting.py +1 -1
  289. disdrodb/utils/time.py +6 -4
  290. disdrodb/utils/warnings.py +2 -3
  291. disdrodb/utils/writer.py +5 -3
  292. disdrodb/utils/xarray.py +31 -3
  293. disdrodb/utils/yaml.py +1 -3
  294. disdrodb/viz/__init__.py +1 -1
  295. disdrodb/viz/plots.py +193 -18
  296. {disdrodb-0.2.1.dist-info → disdrodb-0.4.0.dist-info}/METADATA +5 -4
  297. disdrodb-0.4.0.dist-info/RECORD +361 -0
  298. {disdrodb-0.2.1.dist-info → disdrodb-0.4.0.dist-info}/entry_points.txt +3 -0
  299. disdrodb/etc/products/L1/1MIN.yaml +0 -13
  300. disdrodb/etc/products/L1/LPM/1MIN.yaml +0 -13
  301. disdrodb/etc/products/L1/LPM_V0/1MIN.yaml +0 -13
  302. disdrodb/etc/products/L1/PARSIVEL/1MIN.yaml +0 -13
  303. disdrodb/etc/products/L1/PARSIVEL2/1MIN.yaml +0 -13
  304. disdrodb/etc/products/L1/PWS100/1MIN.yaml +0 -13
  305. disdrodb/etc/products/L1/RD80/1MIN.yaml +0 -13
  306. disdrodb/etc/products/L1/SWS250/1MIN.yaml +0 -13
  307. disdrodb/etc/products/L2M/10MIN.yaml +0 -12
  308. disdrodb/l1/beard_model.py +0 -662
  309. disdrodb/l1/filters.py +0 -205
  310. disdrodb-0.2.1.dist-info/RECORD +0 -329
  311. {disdrodb-0.2.1.dist-info → disdrodb-0.4.0.dist-info}/WHEEL +0 -0
  312. {disdrodb-0.2.1.dist-info → disdrodb-0.4.0.dist-info}/licenses/LICENSE +0 -0
  313. {disdrodb-0.2.1.dist-info → disdrodb-0.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,914 @@
1
+ # -----------------------------------------------------------------------------.
2
+ """DISDRODB hydrometeor classification and QC module."""
3
+
4
+ import numpy as np
5
+ import pandas as pd
6
+ import xarray as xr
7
+
8
+ from disdrodb.constants import DIAMETER_DIMENSION, VELOCITY_DIMENSION
9
+ from disdrodb.fall_velocity import get_hail_fall_velocity
10
+ from disdrodb.fall_velocity.graupel import retrieve_graupel_heymsfield2014_fall_velocity
11
+ from disdrodb.fall_velocity.rain import get_rain_fall_velocity
12
+ from disdrodb.l2.empirical_dsd import get_effective_sampling_area, get_rain_rate_from_drop_number
13
+ from disdrodb.l2.processing import define_rain_spectrum_mask
14
+ from disdrodb.utils.manipulations import filter_diameter_bins, filter_velocity_bins
15
+ from disdrodb.utils.time import ensure_sample_interval_in_seconds
16
+ from disdrodb.utils.xarray import xr_remap_numeric_array
17
+
18
+ # Define possible variable available and corresponding snow_temperature_limit
19
+ DICT_TEMPERATURES = {
20
+ "air_temperature": 6, # generic and PWS100
21
+ "air_temperature_min": 6, # PWS100
22
+ "temperature_ambient": 6, # LPM
23
+ "temperature_interior": 10, # LPM
24
+ "sensor_temperature": 10, # PARSIVEL and SWS250
25
+ }
26
+ TEMPERATURE_VARIABLES = list(DICT_TEMPERATURES)
27
+
28
+
29
+ def get_temperature(ds):
30
+ """Retrieve temperature variable from L0C product."""
31
+ # Check temperature variable is available, otherwise return None
32
+ if not any(var in ds.data_vars for var in DICT_TEMPERATURES):
33
+ return None, None
34
+
35
+ # Define temperature available
36
+ for var, thr in DICT_TEMPERATURES.items():
37
+ if var in ds:
38
+ temperature = ds[var]
39
+ snow_temperature_limit = thr
40
+ break
41
+
42
+ # Fill NaNs
43
+ temperature = temperature.ffill("time").bfill("time")
44
+ return temperature, snow_temperature_limit
45
+
46
+
47
+ def define_qc_temperature(temperature, sample_interval, threshold_minutes=360):
48
+ """Define segment-based QC rule for temperature.
49
+
50
+ Return a qc array with 1 when temperature is constant for over threshold_minutes.
51
+ Return a qc of 2 if temperature is not available.
52
+ """
53
+ # If all NaN, return flag equal to 2
54
+ if np.all(np.isnan(temperature)):
55
+ return xr.full_like(temperature, 2)
56
+
57
+ # Fill NaNs
58
+ temperature = temperature.ffill("time").bfill("time")
59
+
60
+ # Round temperature
61
+ temperature = temperature.round(0)
62
+
63
+ # Initialize flag
64
+ qc_flag = xr.zeros_like(temperature, dtype=int)
65
+
66
+ # Assign 1 when temperature changes, 0 otherwise
67
+ change_points = np.concatenate(([True], np.diff(temperature.values) != 0))
68
+
69
+ # Assign segment IDs
70
+ segment_id = np.cumsum(change_points)
71
+
72
+ # Compute duration per segment
73
+ df = pd.DataFrame(
74
+ {
75
+ "segment": segment_id,
76
+ "time": temperature["time"].to_numpy(),
77
+ },
78
+ )
79
+
80
+ # Count samples per segment
81
+ segment_length = df.groupby("segment").size().rename("count").to_frame()
82
+
83
+ # Compute duration based on sample_interval
84
+ segment_length["duration"] = segment_length["count"] * int(sample_interval)
85
+
86
+ # Flag segments that are constant for over threshold_minutes
87
+ threshold_seconds = threshold_minutes * 60
88
+ long_segments = segment_length[segment_length["duration"] >= threshold_seconds].index
89
+
90
+ # Define QC flag: 1 = no variation, 0 = normal
91
+ mask = np.isin(segment_id, long_segments)
92
+ qc_flag.data = xr.where(mask, 1, 0)
93
+ return qc_flag
94
+
95
+
96
+ def define_qc_margin_fallers(spectrum, fall_velocity_upper, above_velocity_fraction=None, above_velocity_tolerance=2):
97
+ """Define QC mask for margin fallers and splashing."""
98
+ if above_velocity_fraction is not None:
99
+ above_fall_velocity = fall_velocity_upper * (1 + above_velocity_fraction)
100
+ elif above_velocity_tolerance is not None:
101
+ above_fall_velocity = fall_velocity_upper + above_velocity_tolerance
102
+ else:
103
+ above_fall_velocity = np.inf
104
+
105
+ # Define mask
106
+ velocity_lower = xr.ones_like(spectrum.isel(time=0, missing_dims="ignore")) * spectrum["velocity_bin_lower"]
107
+ diameter_upper = xr.ones_like(spectrum.isel(time=0, missing_dims="ignore")) * spectrum["diameter_bin_upper"]
108
+
109
+ mask = np.logical_and(diameter_upper <= 5, velocity_lower >= above_fall_velocity)
110
+ return mask
111
+
112
+
113
+ def define_qc_rain_strong_wind_mask(spectrum):
114
+ """Define QC mask for strong wind artefacts in heavy rainfall.
115
+
116
+ Based on Katia Friedrich et al. 2013.
117
+ """
118
+ diameter_lower = xr.ones_like(spectrum.isel(time=0, missing_dims="ignore")) * spectrum["diameter_bin_lower"]
119
+ velocity_upper = xr.ones_like(spectrum.isel(time=0, missing_dims="ignore")) * spectrum["velocity_bin_upper"]
120
+
121
+ # Define mask
122
+ mask = np.logical_and(
123
+ diameter_lower >= 5,
124
+ velocity_upper < 1,
125
+ )
126
+ return mask
127
+
128
+
129
+ def qc_spikes_isolated_precip(hydrometeor_type, sample_interval):
130
+ """
131
+ Identify isolated precipitation spikes based on hydrometeor classification.
132
+
133
+ This quality control (QC) routine flags short, isolated precipitation detections
134
+ (spikes) that are not supported by neighboring precipitating timesteps within
135
+ a defined time window. The test helps remove spurious single-sample precipitation
136
+ detections caused by instrument noise or transient misclassification.
137
+
138
+ The algorithm:
139
+ 1. Identifies potential precipitation timesteps where `hydrometeor_type>= 1`.
140
+ 2. Computes time differences between consecutive potential precipitation samples.
141
+ 3. Flags a timestep as a spike when both the previous and next precipitation
142
+ detections are separated by more than the configured time window.
143
+ 4. Skips the QC test entirely if the temporal resolution exceeds 2 minutes.
144
+
145
+ Parameters
146
+ ----------
147
+ hydrometeor_type: xr.DataArray
148
+ Hydrometeor type classification array with a ``time`` coordinate.
149
+ Precipitation presence is defined where ``hydrometeor_type>= 1``.
150
+ sample_interval : float or int
151
+ Nominal sampling interval of the dataset in **seconds**.
152
+ If ``sample_interval >= 120`` (2 minutes), the QC test is skipped.
153
+
154
+ Returns
155
+ -------
156
+ flag_spikes : xr.DataArray of int
157
+ Binary QC flag array (same dimensions as input) with:
158
+ * 0 : no spike detected
159
+ * 1 : isolated precipitation spike
160
+
161
+ Notes
162
+ -----
163
+ - The time window is currently fixed to ±60 seconds for typical 1-minute
164
+ sampling intervals but can be adapted to scale with `sample_interval`.
165
+ - Designed to work with irregular time coordinates; relies on actual
166
+ timestamp differences instead of fixed rolling windows.
167
+ - For datasets with coarse sampling (> 2 minutes), the function
168
+ returns a zero-valued flag (QC not applied).
169
+ """
170
+ # Define potential precipitating timesteps
171
+ is_potential_precip = xr.where((hydrometeor_type >= 1), 1, 0)
172
+
173
+ # Initialize QC flag
174
+ flag_spikes = xr.zeros_like(is_potential_precip, dtype=int)
175
+ flag_spikes.attrs.update(
176
+ {
177
+ "long_name": "Isolated precipitation spike flag",
178
+ "standard_name": "flag_spikes",
179
+ "units": "1",
180
+ "flag_values": [0, 1],
181
+ "flag_meanings": "no_spike isolated_precipitation_spike",
182
+ "description": (
183
+ "Quality control flag indicating isolated precipitation detections, "
184
+ "without neighboring precipitating timesteps. "
185
+ "If the sampling interval is 2 minutes or coarser, the QC test is skipped."
186
+ ),
187
+ },
188
+ )
189
+
190
+ # Skip QC for coarse temporal data (> 2 min)
191
+ if sample_interval >= 120:
192
+ return flag_spikes
193
+
194
+ # Define time window based on sample interval
195
+ time_window = 60
196
+
197
+ # Extract arrays
198
+ times = pd.to_datetime(is_potential_precip["time"].to_numpy())
199
+ mask_potential_precip = is_potential_precip.to_numpy() == 1
200
+
201
+ # If no precipitation, skip and return 0 array
202
+ if not np.any(mask_potential_precip):
203
+ return flag_spikes
204
+
205
+ # Get potential precipition indices and timestamps
206
+ precip_idx = np.where(mask_potential_precip)[0]
207
+ precip_times = times[precip_idx].astype("datetime64[s]").astype("int64").astype("float64")
208
+
209
+ # Compute Δt to previous and next precip (vectorized)
210
+ dt_prev = np.empty_like(precip_times)
211
+ dt_next = np.empty_like(precip_times)
212
+ dt_prev[0] = np.inf
213
+ dt_prev[1:] = precip_times[1:] - precip_times[:-1]
214
+ dt_next[-1] = np.inf
215
+ dt_next[:-1] = precip_times[1:] - precip_times[:-1]
216
+
217
+ # Create same-size arrays aligned with original time dimension
218
+ # - Fill NaN for non-precip indices
219
+ delta_prev = np.full_like(mask_potential_precip, np.inf, dtype=float)
220
+ delta_next = np.full_like(mask_potential_precip, np.inf, dtype=float)
221
+ delta_prev[precip_idx] = dt_prev
222
+ delta_next[precip_idx] = dt_next
223
+
224
+ # Identify isolated spikes
225
+ isolated = (mask_potential_precip) & (delta_prev > time_window) & (delta_next > time_window)
226
+ flag_spikes.data[isolated] = 1
227
+ return flag_spikes
228
+
229
+
230
+ def define_hail_mask(spectrum, ds_env, minimum_diameter=5):
231
+ """Define hail mask."""
232
+ # Define velocity limits
233
+ fall_velocity_lower = get_hail_fall_velocity(
234
+ spectrum["diameter_bin_lower"],
235
+ model="Heymsfield2018",
236
+ ds_env=ds_env,
237
+ minimum_diameter=minimum_diameter - 1,
238
+ )
239
+ fall_velocity_upper = get_hail_fall_velocity(
240
+ spectrum["diameter_bin_upper"],
241
+ model="Laurie1960",
242
+ ds_env=ds_env,
243
+ minimum_diameter=minimum_diameter - 1,
244
+ )
245
+
246
+ # Define spectrum mask
247
+ diameter_lower = xr.ones_like(spectrum.isel(time=0, missing_dims="ignore")) * spectrum["diameter_bin_lower"]
248
+ velocity_lower = xr.ones_like(spectrum.isel(time=0, missing_dims="ignore")) * spectrum["velocity_bin_lower"]
249
+ velocity_upper = xr.ones_like(spectrum.isel(time=0, missing_dims="ignore")) * spectrum["velocity_bin_upper"]
250
+
251
+ mask_velocity = np.logical_and(
252
+ velocity_lower >= fall_velocity_lower,
253
+ velocity_upper <= fall_velocity_upper,
254
+ )
255
+
256
+ mask_diameter = diameter_lower >= minimum_diameter
257
+ mask = np.logical_and(mask_diameter, mask_velocity)
258
+ return mask
259
+
260
+
261
+ def define_graupel_mask(
262
+ spectrum,
263
+ ds_env,
264
+ minimum_diameter=0.5,
265
+ maximum_diameter=5,
266
+ minimum_density=50,
267
+ maximum_density=600,
268
+ ):
269
+ """Define graupel mask."""
270
+ # Define velocity limits
271
+ fall_velocity_lower = retrieve_graupel_heymsfield2014_fall_velocity(
272
+ diameter=spectrum["diameter_bin_lower"],
273
+ graupel_density=minimum_density,
274
+ ds_env=ds_env,
275
+ )
276
+ fall_velocity_upper = retrieve_graupel_heymsfield2014_fall_velocity(
277
+ diameter=spectrum["diameter_bin_upper"],
278
+ graupel_density=maximum_density,
279
+ ds_env=ds_env,
280
+ )
281
+ # fall_velocity_upper = get_graupel_fall_velocity(
282
+ # diameter=spectrum["diameter_bin_upper"],
283
+ # model="Locatelli1974Lump",
284
+ # ds_env=ds_env,
285
+ # minimum_diameter=minimum_diameter-1,
286
+ # maximum_diameter=maximum_diameter+1,
287
+ # )
288
+ # Define spectrum mask
289
+ diameter_lower = xr.ones_like(spectrum.isel(time=0, missing_dims="ignore")) * spectrum["diameter_bin_lower"]
290
+ diameter_upper = xr.ones_like(spectrum.isel(time=0, missing_dims="ignore")) * spectrum["diameter_bin_upper"]
291
+
292
+ velocity_lower = xr.ones_like(spectrum.isel(time=0, missing_dims="ignore")) * spectrum["velocity_bin_lower"]
293
+ velocity_upper = xr.ones_like(spectrum.isel(time=0, missing_dims="ignore")) * spectrum["velocity_bin_upper"]
294
+ mask_velocity = np.logical_and(
295
+ velocity_lower >= fall_velocity_lower,
296
+ velocity_upper <= fall_velocity_upper,
297
+ )
298
+ mask_diameter = np.logical_and(
299
+ diameter_lower >= minimum_diameter,
300
+ diameter_upper <= maximum_diameter,
301
+ )
302
+ mask = np.logical_and(mask_diameter, mask_velocity)
303
+ return mask
304
+
305
+
306
+ def classify_raw_spectrum(
307
+ ds,
308
+ ds_env,
309
+ sample_interval,
310
+ sensor_name,
311
+ temperature=None,
312
+ rain_temperature_lower_limit=-5,
313
+ snow_temperature_upper_limit=5,
314
+ ):
315
+ """Run precipitation and hydrometeor type classification."""
316
+ # ------------------------------------------------------------------
317
+ # Filter LPM to avoid being impacted by noise in first bins
318
+ if sensor_name == "LPM":
319
+ # Remove first two diameter bins (very noisy)
320
+ ds = filter_diameter_bins(ds=ds, minimum_diameter=0.375)
321
+ # Remove first velocity bin
322
+ ds = filter_velocity_bins(ds=ds, minimum_velocity=0.2)
323
+ if sensor_name == "PWS100":
324
+ # Remove first two bin
325
+ ds = filter_diameter_bins(ds=ds, minimum_diameter=0.2)
326
+ # Remove first two bin
327
+ ds = filter_velocity_bins(ds=ds, minimum_velocity=0.2)
328
+
329
+ # ------------------------------------------------------------------
330
+ # Retrieve raw spectrum
331
+ raw_spectrum = ds["raw_drop_number"]
332
+
333
+ #### Define masks
334
+ spectrum_template = raw_spectrum.isel(time=0, missing_dims="ignore")
335
+ diameter_lower = raw_spectrum["diameter_bin_lower"].broadcast_like(spectrum_template) # [mm]
336
+ diameter_upper = raw_spectrum["diameter_bin_upper"].broadcast_like(spectrum_template)
337
+
338
+ # Define spectrum areas
339
+ B1 = (diameter_lower >= 0.0) & (diameter_upper <= 0.5)
340
+ B2 = (diameter_upper > 0.5) & (diameter_upper <= 5.0)
341
+ B3 = (diameter_upper > 5.0) & (diameter_upper <= 8.0)
342
+ B4 = diameter_upper > 8.0
343
+
344
+ # Define liquid masks
345
+ # - Compute raindrop fall velocity for lower and upper diameter limits
346
+ raindrop_fall_velocity_upper = get_rain_fall_velocity(
347
+ diameter=ds["diameter_bin_upper"],
348
+ model="Beard1976",
349
+ ds_env=ds_env,
350
+ )
351
+ raindrop_fall_velocity_lower = get_rain_fall_velocity(
352
+ diameter=ds["diameter_bin_lower"],
353
+ model="Beard1976",
354
+ ds_env=ds_env,
355
+ )
356
+ liquid_mask = define_rain_spectrum_mask(
357
+ drop_number=raw_spectrum,
358
+ fall_velocity_lower=raindrop_fall_velocity_lower,
359
+ fall_velocity_upper=raindrop_fall_velocity_upper,
360
+ above_velocity_fraction=None,
361
+ above_velocity_tolerance=2,
362
+ below_velocity_fraction=None,
363
+ below_velocity_tolerance=3,
364
+ maintain_drops_smaller_than=1, # 1, # 2
365
+ maintain_drops_slower_than=2.5, # 2.5, # 3
366
+ maintain_smallest_drops=False,
367
+ )
368
+
369
+ drizzle_mask = liquid_mask & B1
370
+ drizzle_rain_mask = liquid_mask & B2
371
+ rain_mask = liquid_mask & B3 # potentially mixed with small hail
372
+
373
+ # Define graupel masks
374
+ graupel_mask = define_graupel_mask(
375
+ raw_spectrum,
376
+ ds_env=ds_env,
377
+ minimum_diameter=0.9,
378
+ maximum_diameter=5.5,
379
+ minimum_density=50,
380
+ maximum_density=900,
381
+ )
382
+ graupel_mask = np.logical_and(graupel_mask, ~liquid_mask)
383
+ graupel_hd_mask = define_graupel_mask(
384
+ raw_spectrum,
385
+ ds_env=ds_env,
386
+ minimum_diameter=0.9,
387
+ maximum_diameter=5.5,
388
+ minimum_density=400,
389
+ maximum_density=900,
390
+ )
391
+ graupel_hd_mask = np.logical_and(graupel_hd_mask, graupel_mask)
392
+ graupel_ld_mask = np.logical_and(graupel_mask, ~graupel_hd_mask)
393
+
394
+ # graupel_mask.plot.pcolormesh(x="diameter_bin_center")
395
+ # liquid_mask.plot.pcolormesh(x="diameter_bin_center")
396
+ # graupel_hd_mask.plot.pcolormesh(x="diameter_bin_center")
397
+ # graupel_ld_mask.plot.pcolormesh(x="diameter_bin_center")
398
+
399
+ # Define hail mask
400
+ hail_mask = define_hail_mask(raw_spectrum, ds_env=ds_env, minimum_diameter=5)
401
+ hail_mask = np.logical_and(hail_mask, ~graupel_mask)
402
+
403
+ small_hail_mask = hail_mask & B3 # [5,8]
404
+ large_hail_mask = hail_mask & B4 # > 8
405
+
406
+ # Define snow masks
407
+ velocity_upper = xr.ones_like(raw_spectrum.isel(time=0, missing_dims="ignore")) * raw_spectrum["velocity_bin_upper"]
408
+ snow_mask_full = velocity_upper <= 6.5
409
+
410
+ # - Without rain and hail
411
+ snow_mask = np.logical_and(snow_mask_full, ~liquid_mask)
412
+ snow_mask = np.logical_and(snow_mask, ~hail_mask)
413
+ snow_mask = np.logical_and(snow_mask, diameter_lower >= 1)
414
+
415
+ # - Without rain, hail and graupel
416
+ # snow_small_mask = snow_mask & (diameter_upper <= 5.0)
417
+ snow_large_mask = snow_mask & (diameter_upper > 5.0)
418
+
419
+ # Define snow grain mask
420
+ snow_grains_mask = (velocity_upper <= 2.5) & (diameter_upper <= 2.2) # ice crystals & prisms (0.1 < D < 1 or 2 mm)
421
+
422
+ # ---------------------------------------------------------------------
423
+ # Check mask cover all space without leaving empty bins
424
+ # FUTURE: CHECK IF THERE ARE CASES WHERE EMPTY BINS STILL OCCURS
425
+
426
+ # from functools import reduce
427
+ # sum_mask = reduce(np.logical_or, [hail_mask, liquid_mask, graupel_mask])
428
+ # sum_mask.plot.pcolormesh(x="diameter_bin_center")
429
+
430
+ # ---------------------------------------------------------------------
431
+ # Estimate rain rate using particles with D <=5 (D > 5 can be contaminated by noise or hail)
432
+ # - Extract sample interval
433
+ sample_interval = ensure_sample_interval_in_seconds(ds["sample_interval"]) # s
434
+ # - Extract diameter in m
435
+ diameter = ds["diameter_bin_center"] / 1000 # m
436
+ # - Compute sampling area [m2]
437
+ sampling_area = get_effective_sampling_area(sensor_name=sensor_name, diameter=diameter) # m2
438
+ # - Compute dummy rainfall rate (on D from 0 to 5) to avoid 'hail' contamination
439
+ rainfall_rate_mask = drizzle_mask + drizzle_rain_mask
440
+ rainfall_rate = get_rain_rate_from_drop_number(
441
+ drop_number=raw_spectrum.where(rainfall_rate_mask, 0), # if any NaN --> return NaN
442
+ sampling_area=sampling_area,
443
+ diameter=diameter,
444
+ sample_interval=sample_interval,
445
+ )
446
+ # ---------------------------------------------------------------------
447
+ # Estimate gross snowfall rate
448
+ # FUTURE:
449
+ # - Compute over snow mask area with and without rainy area
450
+ # - Use Lempio lump parametrization
451
+ # - Required to define weather codes
452
+
453
+ # ---------------------------------------------------------------------
454
+ # Define wind artefacts mask (Friedrich et al., 2013)
455
+ strong_wind_mask = define_qc_rain_strong_wind_mask(spectrum_template)
456
+
457
+ # Define margin fallers mask
458
+ margin_fallers_mask = define_qc_margin_fallers(
459
+ spectrum_template,
460
+ fall_velocity_upper=raindrop_fall_velocity_upper,
461
+ # above_velocity_fraction=0.6,
462
+ above_velocity_tolerance=2,
463
+ )
464
+
465
+ # Define splash mask
466
+ splash_mask = (diameter_lower >= 0.0) & (diameter_upper <= 6) & (velocity_upper <= 0.6)
467
+
468
+ # ---------------------------------------------------------------------
469
+ # Define liquid, snow, and graupel mask (robust to splash)
470
+ liquid_mask_without_splash = liquid_mask & ~splash_mask
471
+ snow_mask_without_splash = snow_mask & ~splash_mask
472
+ # graupel_mask_without_splash = graupel_mask & ~splash_mask
473
+ graupel_ld_mask_without_splash = graupel_ld_mask & ~splash_mask
474
+
475
+ # ---------------------------------------------------------------------
476
+ #### Compute statistics
477
+ dims = [DIAMETER_DIMENSION, VELOCITY_DIMENSION]
478
+ n_particles = raw_spectrum.sum(dim=dims)
479
+ # n_particles_1 = raw_spectrum.where(B1).sum(dim=dims)
480
+ # n_particles_2 = raw_spectrum.where(B2).sum(dim=dims)
481
+ # n_particles_3 = raw_spectrum.where(B3).sum(dim=dims)
482
+ # n_particles_4 = raw_spectrum.where(B4).sum(dim=dims)
483
+
484
+ ## ----
485
+ # Rain
486
+ n_drizzle = raw_spectrum.where(drizzle_mask).sum(dim=dims)
487
+ n_drizzle_rain = raw_spectrum.where(drizzle_rain_mask).sum(dim=dims)
488
+ n_rain = raw_spectrum.where(rain_mask).sum(dim=dims)
489
+ n_liquid = n_drizzle + n_drizzle_rain + n_rain
490
+ n_liquid_robust = raw_spectrum.where(liquid_mask_without_splash).sum(dim=dims)
491
+
492
+ ## ----
493
+ # Hail
494
+ # n_hail = raw_spectrum.where(hail_mask).sum(dim=dims)
495
+ n_small_hail = raw_spectrum.where(small_hail_mask).sum(dim=dims)
496
+ n_large_hail = raw_spectrum.where(large_hail_mask).sum(dim=dims)
497
+
498
+ ## ----
499
+ # Graupel
500
+ n_graupel = raw_spectrum.where(graupel_mask).sum(dim=dims)
501
+ # n_graupel_robust = raw_spectrum.where(graupel_mask_without_splash).sum(dim=dims)
502
+ n_graupel_ld = raw_spectrum.where(graupel_ld_mask_without_splash).sum(dim=dims)
503
+ n_graupel_hd = raw_spectrum.where(graupel_hd_mask).sum(dim=dims)
504
+
505
+ ## ----
506
+ # Snow
507
+ n_snow = raw_spectrum.where(snow_mask).sum(dim=dims)
508
+ n_snow_robust = raw_spectrum.where(snow_mask_without_splash).sum(dim=dims)
509
+
510
+ # n_snow_small = raw_spectrum.where(snow_small_mask).sum(dim=dims)
511
+ n_snow_large = raw_spectrum.where(snow_large_mask).sum(dim=dims)
512
+ n_snow_grains = raw_spectrum.where(snow_grains_mask).sum(dim=dims)
513
+
514
+ ## ----
515
+ # Auxiliary
516
+ n_wind_artefacts = raw_spectrum.where(strong_wind_mask).sum(dim=dims)
517
+ n_margin_fallers = raw_spectrum.where(margin_fallers_mask).sum(dim=dims)
518
+ n_splashing = raw_spectrum.where(splash_mask).sum(dim=dims)
519
+
520
+ ## ----
521
+ # Bins statistics
522
+ # n_bins = (raw_spectrum.where((~splash_mask) > 0)).sum(dim=dims)
523
+ n_liquid_bins = (raw_spectrum.where(liquid_mask_without_splash) > 0).sum(dim=dims)
524
+ n_snow_bins = (raw_spectrum.where(snow_mask_without_splash) > 0).sum(dim=dims) # without rainy area
525
+ # n_snow_large_bins = (raw_spectrum.where(snow_large_mask) > 0).sum(dim=dims) # only > 5 mm
526
+ # n_graupel_bins = (raw_spectrum.where(graupel_mask_without_splash) > 0).sum(dim=dims)
527
+
528
+ # Bins fractions
529
+ fraction_rain_bins = xr.where(n_particles == 0, 0, n_liquid_bins / (n_liquid_bins + n_snow_bins))
530
+ # fraction_snow_bins = xr.where(n_particles == 0, 0, n_snow_bins / (n_liquid_bins + n_snow_bins))
531
+
532
+ ## ----
533
+ # Particles fractions
534
+ # fraction_drizzle_rel = xr.where(n_particles_1 == 0, 0, n_drizzle / n_particles_1)
535
+ fraction_drizzle_tot = xr.where(n_particles == 0, 0, n_drizzle / n_particles)
536
+
537
+ # fraction_drizzle_rain_rel = xr.where(n_particles_2 == 0, 0, n_drizzle_rain / n_particles_2)
538
+ fraction_drizzle_rain_tot = xr.where(n_particles == 0, 0, (n_drizzle + n_drizzle_rain) / n_particles)
539
+
540
+ # fraction_rain_rel = xr.where(n_particles_3 == 0, 0, n_rain / n_particles_3)
541
+ fraction_rain_tot = xr.where(n_particles == 0, 0, n_liquid / n_particles)
542
+ # fraction_liquid = fraction_rain_tot
543
+
544
+ # fraction_graupel_only_rel = xr.where(n_particles_2 == 0, 0, n_graupel / n_particles_2)
545
+ fraction_graupel_only_tot = xr.where(n_particles == 0, 0, n_graupel / n_particles)
546
+
547
+ # fraction_hail = xr.where(n_particles_4 == 0, 0, n_hail / n_particles_4)
548
+
549
+ # fraction_snow_large_rel = xr.where((n_particles_3 + n_particles_4) == 0, 0,
550
+ # n_snow_large / (n_particles_3+n_particles_4))
551
+ # fraction_snow_large_tot = xr.where(n_particles == 0, 0, n_snow_large / n_particles)
552
+ fraction_snow_tot = xr.where(n_particles == 0, 0, n_snow / n_particles)
553
+ fraction_snow_grains_tot = xr.where(n_particles == 0, 0, n_snow_grains / n_particles)
554
+
555
+ fraction_splash = xr.where(n_particles == 0, 0, n_splashing / n_particles)
556
+
557
+ # fraction_rain_graupel_tot = xr.where(n_particles == 0, 0, (n_liquid + n_graupel) / n_particles)
558
+
559
+ # graupel_liquid_ratio = xr.where(n_particles == 0, 0, n_graupel_robust/n_liquid_robust)
560
+ solid_liquid_ratio = xr.where(n_particles == 0, 0, n_snow_robust / n_liquid_robust)
561
+
562
+ # -----------------------------------------------------------------------------------------------.
563
+ #### Classification logic
564
+ # Class |Conditions | WMO 4680
565
+ # -------------------------------------
566
+ # Drizzle |D < 0.5 mm |
567
+ # Rain |D > 0.5, D < 10 |
568
+ # Snow |D > 1, V < 6 | 71-73
569
+ # Snow grains |D < 1 | 77
570
+ # Ice Crystals |0 < D < 2 |
571
+ # Graupel |D >1 , D < 5 | 74-76
572
+
573
+ # Snow grains are within the drizzle class (<0.5 mm)!
574
+ # --> Temperature required to classify them
575
+
576
+ # Graupel class
577
+ # - Ice pellets / Sleets (frozen raindrops, T<0) (1 <= D <= 5 mm)
578
+ # - Graupel (GS) (Snow pellet coated with ice, T > 0) (2 <= D <= 5 mm)
579
+ # # WMO 4680
580
+ # --------------------------------------------------------------
581
+ # Initialize label
582
+ label = xr.ones_like(ds["time"], dtype=float) * -1 # [mm]
583
+
584
+ # No precipitation
585
+ label = xr.where(n_particles == 0, 0, label)
586
+
587
+ # Graupel only
588
+ cond = (fraction_graupel_only_tot > 0.7) & (n_snow_large < 1)
589
+ label = xr.where(cond & (label == -1), 8, label)
590
+
591
+ # Liquid only
592
+ # - Drizzle (D < 0.5 mm)
593
+ cond = (fraction_drizzle_tot > 0.1) & (n_graupel < 1) & (n_snow_large < 1) & (n_drizzle_rain < 1)
594
+ label = xr.where(cond & (label == -1), 1, label)
595
+
596
+ # - Drizzle + Rain (0.5-5 mm)
597
+ cond = (fraction_drizzle_rain_tot > 0.1) & (n_graupel < 1) & (n_snow_large < 1) & (n_rain < 1)
598
+ label = xr.where(cond & (label == -1), 2, label)
599
+
600
+ # - Rain (D > 5 mm)
601
+ cond = (fraction_rain_tot > 0.1) & (n_graupel < 1) & (n_snow_large < 1)
602
+ label = xr.where(cond & (label == -1), 3, label)
603
+
604
+ # Snow only
605
+ cond = fraction_snow_tot > 0.6 # TODO: extend to use snow_mask_full
606
+ label = xr.where(cond & (label == -1), 5, label)
607
+
608
+ # ---------------------------------
609
+ # Rain (R > 3 mm/hr) with some graupel
610
+ cond = (fraction_rain_bins >= 0.75) & (rainfall_rate > 3)
611
+ label = xr.where(cond & (label == -1), 31, label) # mixed
612
+
613
+ # ---------------------------------
614
+ # (label == -1).sum()
615
+ # (cond & (label == -1)).sum()
616
+
617
+ # ---------------------------------
618
+ # Mixed
619
+ # --> FUTURE: Better clarified the meaning
620
+ # --> FUTURE: R computed with particles only above 3 m/s would help disentagle snow from mixed !
621
+ # --> When R > 1 mm/hr and no splash - solid_liquid_ratio > XXX
622
+ n_snow_bins_thr = 6
623
+ fraction_splash_thr = 0.1
624
+ solid_liquid_ratio_thr = 0.05
625
+ cond = (
626
+ (solid_liquid_ratio >= solid_liquid_ratio_thr)
627
+ & (rainfall_rate > 1)
628
+ & (n_snow_bins > n_snow_bins_thr)
629
+ & (fraction_splash < fraction_splash_thr)
630
+ )
631
+ label = xr.where(cond & (label == -1), 4, label) # mixed
632
+
633
+ cond = (
634
+ (solid_liquid_ratio >= solid_liquid_ratio_thr)
635
+ & (rainfall_rate > 1)
636
+ & (n_snow_bins <= n_snow_bins_thr)
637
+ & (fraction_splash < fraction_splash_thr)
638
+ )
639
+ label = xr.where(cond & (label == -1), 21, label) # Set as rain !
640
+
641
+ # - When R > 1 mm/hr and no splash - solid_liquid_ratio < XXX
642
+ cond = (solid_liquid_ratio < solid_liquid_ratio_thr) & (rainfall_rate > 1) & (fraction_splash < fraction_splash_thr)
643
+ label = xr.where(cond & (label == -1), 21, label) # Set as rain !
644
+
645
+ # ---------------------------------
646
+ # Non-hydrometeors class
647
+ cond = (fraction_splash >= 0.5) & (rainfall_rate < 1.5)
648
+ label = xr.where(cond & (label == -1), -2, label)
649
+
650
+ cond = (fraction_splash >= 0.4) & (fraction_splash <= 0.5) & (rainfall_rate <= 0.2)
651
+ label = xr.where(cond & (label == -1), -2, label)
652
+
653
+ # ---------------------------------
654
+ # - When R > 1mm/hr, with splash
655
+ cond = (rainfall_rate > 1) & (fraction_splash >= 0.1) & (solid_liquid_ratio >= 0.2)
656
+ label = xr.where(cond & (label == -1), 41, label) # mixed
657
+
658
+ cond = (rainfall_rate > 1) & (fraction_splash >= 0.1) & (solid_liquid_ratio < 0.2)
659
+ label = xr.where(cond & (label == -1), 23, label) # rainfall
660
+
661
+ # ---------------------------------
662
+ # - Noisy Rain (solid_liquid_ratio < 0.03)
663
+ cond = (solid_liquid_ratio <= 0.05) & (n_snow_robust <= 2)
664
+ label = xr.where(cond & (label == -1), 22, label) # Set noisy rain
665
+
666
+ # ---------------------------------
667
+ # Ice Crystals
668
+ cond = fraction_snow_grains_tot >= 0.95
669
+ label = xr.where(cond & (label == -1), 6, label)
670
+
671
+ # Remaining snow
672
+ label = xr.where(label == -1, 51, label)
673
+
674
+ # ------------------------------------------------------------------------.
675
+ # Improve classification using temperature information if available
676
+ if temperature is not None:
677
+ temperature = temperature.compute()
678
+ qc_temperature = define_qc_temperature(temperature, sample_interval=sample_interval, threshold_minutes=1440)
679
+
680
+ is_surely_rain = (temperature >= snow_temperature_upper_limit) & (qc_temperature == 0)
681
+ is_surely_snow = (temperature <= rain_temperature_lower_limit) & (qc_temperature == 0)
682
+ is_mixed = label.isin([4, 41])
683
+ is_snow = label.isin([5, 51])
684
+ is_drizzle = label.isin([1])
685
+ is_snow_grain = label.isin([6])
686
+ is_rain = label.isin([2, 21, 22, 23, 3])
687
+ is_graupel = label == 8
688
+
689
+ # Improve mixed classification (4, 41)
690
+ # - If T > snow_temperature_upper_limit --> rain
691
+ # - If T < -5 rain_temperature_lower_limit --> snow
692
+ label = xr.where(is_surely_rain & is_mixed, 24, label)
693
+ label = xr.where(is_surely_snow & is_mixed, 52, label)
694
+
695
+ # Improve snow classification
696
+ # - If T > snow_temperature_upper_limit --> No hydrometeors
697
+ label = xr.where(is_surely_rain & is_snow, -21, label)
698
+
699
+ # Improve drizzle classification
700
+ label = xr.where(is_surely_snow & is_drizzle, 61, label)
701
+
702
+ # Improve snow grains classification
703
+ # --> If T > snow_temperature_upper_limit --> No hydrometeors
704
+ label = xr.where(is_surely_rain & is_snow_grain, -21, label)
705
+
706
+ # Improve rain classification
707
+ # If T < rain_temperature_lower_limit --> No hydrometeors
708
+ label = xr.where(is_surely_snow & is_rain, -21, label)
709
+
710
+ # Improve graupel classification
711
+ # If T < rain_temperature_lower_limit --> Ice pellets / Sleets
712
+ label = xr.where(is_surely_snow & is_graupel, 7, label)
713
+
714
+ # ------------------------------------------------------------------------.
715
+ # Define hydrometeor_typevariable
716
+ # -2 No hydrometeor
717
+ # -1 Undefined
718
+ # 0 No precipitation
719
+ # 1 Drizzle
720
+ # 2 Drizzle+Rain
721
+ # 3 Rain
722
+ # 4 Mixed (when no only graupel, and rain)
723
+ # 5 Snow (when not only graupel, and no rain)
724
+ # 6 Snow grains / ice crystals / needles (only if temperature is available <-- drizzle)
725
+ # 7 Ice pellets / Sleets (only if temperature is available)
726
+ # 8 Graupel --> flag_graupel
727
+ # 9 Hail --> flag_hail
728
+
729
+ hydrometeor_type = label.copy()
730
+ # No hydrometeor
731
+ hydrometeor_type = xr.where(label.isin([-2, -21]), -2, hydrometeor_type)
732
+ # Drizzle
733
+ hydrometeor_type = xr.where(hydrometeor_type.isin([1]), 1, hydrometeor_type)
734
+ # Drizzle+Rain
735
+ hydrometeor_type = xr.where(hydrometeor_type.isin([2, 21, 22, 23, 24]), 2, hydrometeor_type)
736
+ # Rain
737
+ hydrometeor_type = xr.where(hydrometeor_type.isin([3, 31]), 3, hydrometeor_type)
738
+ # Mixed
739
+ hydrometeor_type = xr.where(hydrometeor_type.isin([4]), 4, hydrometeor_type)
740
+ # Snow
741
+ hydrometeor_type = xr.where(hydrometeor_type.isin([5, 51, 52]), 5, hydrometeor_type)
742
+ # Snow grains
743
+ hydrometeor_type = xr.where(hydrometeor_type.isin([6]), 6, hydrometeor_type)
744
+ # Ice Pellets
745
+ hydrometeor_type = xr.where(hydrometeor_type.isin([7]), 7, hydrometeor_type)
746
+ # Graupel
747
+ hydrometeor_type = xr.where(hydrometeor_type.isin([8]), 8, hydrometeor_type)
748
+ # Add CF-attributes
749
+ hydrometeor_type.attrs.update(
750
+ {
751
+ "long_name": "hydrometeor type classification",
752
+ "standard_name": "hydrometeor_classification",
753
+ "units": "1",
754
+ "flag_values": [-2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
755
+ "flag_meanings": (
756
+ "no_hydrometeor undefined no_precipitation "
757
+ "drizzle drizzle_and_rain rain mixed "
758
+ "snow snow_grains ice_pellets graupel hail"
759
+ ),
760
+ },
761
+ )
762
+
763
+ # ------------------------------------------------------------------------.
764
+ #### Define precipitation type variable
765
+ precipitation_type = xr.ones_like(ds["time"], dtype=float) * -1
766
+ precipitation_type = xr.where(hydrometeor_type.isin([0]), 0, precipitation_type)
767
+ precipitation_type = xr.where(hydrometeor_type.isin([1, 2, 3]), 0, precipitation_type)
768
+ precipitation_type = xr.where(hydrometeor_type.isin([5, 6, 7, 8]), 1, precipitation_type)
769
+ precipitation_type = xr.where(hydrometeor_type.isin([4]), 2, precipitation_type)
770
+ precipitation_type.attrs.update(
771
+ {
772
+ "long_name": "precipitation phase classification",
773
+ "standard_name": "precipitation_phase",
774
+ "units": "1",
775
+ "flag_values": [-2, -1, 0, 1, 2],
776
+ "flag_meanings": "undefined no_precipitation rainfall snowfall mixed_phase",
777
+ },
778
+ )
779
+
780
+ # ------------------------------------------------------------------------.
781
+ #### Define flag graupel
782
+ flag_graupel = xr.ones_like(ds["time"], dtype=float) * 0
783
+ flag_graupel = xr.where(
784
+ (((precipitation_type == 0) & (n_graupel_ld > 2)) | ((hydrometeor_type == 8) & (n_graupel_ld > 0))),
785
+ 1,
786
+ flag_graupel,
787
+ )
788
+ flag_graupel = xr.where(
789
+ (((precipitation_type == 0) & (n_graupel_hd > 2)) | ((hydrometeor_type == 8) & (n_graupel_hd > 0))),
790
+ 2,
791
+ flag_graupel,
792
+ )
793
+ flag_graupel.attrs.update(
794
+ {
795
+ "long_name": "graupel occurrence flag",
796
+ "standard_name": "graupel_flag",
797
+ "units": "1",
798
+ "flag_values": [0, 1, 2],
799
+ "flag_meanings": "no_graupel low_density_graupel high_density_graupel",
800
+ "description": (
801
+ "Flag indicating the presence of graupel. "
802
+ "The flag is set when hydrometeor classification identifies graupel (class=8) or "
803
+ "rainfall with graupel particles. "
804
+ "Low-density graupel (value = 1) corresponds to density < 400 kg/m3 "
805
+ "while high-density graupel corresponds to density > 400 kg/m3."
806
+ ),
807
+ },
808
+ )
809
+
810
+ # ------------------------------------------------------------------------.
811
+ #### Define flag hail
812
+ # FUTURE:
813
+ # - Small hail: check if attached to rain body or not
814
+ # - Check how much is detached
815
+ flag_hail = xr.ones_like(ds["time"], dtype=float) * 0
816
+ flag_hail = xr.where(((precipitation_type == 0) & (n_small_hail >= 1) & (rainfall_rate > 1)), 1, flag_hail)
817
+ flag_hail = xr.where(((precipitation_type == 0) & (n_large_hail >= 1) & (rainfall_rate > 1)), 2, flag_hail)
818
+ flag_hail.attrs.update(
819
+ {
820
+ "long_name": "hail occurrence and size flag",
821
+ "standard_name": "hail_flag",
822
+ "units": "1",
823
+ "flag_values": [0, 1, 2],
824
+ "flag_meanings": "no_hail small_hail large_hail",
825
+ "description": (
826
+ "Flag indicating the presence and estimated size of hail. "
827
+ "Set to 1 for small hail when precipitation type indicates rain. "
828
+ "Set to 2 for large hail (>8 mm) under similar conditions."
829
+ ),
830
+ },
831
+ )
832
+ # ------------------------------------------------------------------------.
833
+ #### Define WMO codes
834
+ # FUTURE: Use hydrometeor_typeand flag_hail values [1,2]
835
+ # Require snowfall rate estimate
836
+
837
+ # ------------------------------------------------------------------------
838
+ #### Define QC splashing, strong_wind, margin_fallers, spikes
839
+ # FUTURE: flag_spikes can be used for non hydrometeor classification,
840
+ # --> But caution because observing the below show true rainfall signature
841
+ # --> raw_spectrum.isel(time=(flag_spikes == 0) & (precipitation_type == 0)).disdrodb.plot_spectrum()
842
+
843
+ flag_splashing = xr.where((precipitation_type == 0) & (fraction_splash >= 0.1), 1, 0)
844
+ flag_wind_artefacts = xr.where((precipitation_type == 0) & (n_wind_artefacts >= 1), 1, 0)
845
+ flag_noise = xr.where((hydrometeor_type == -2), 1, 0)
846
+ flag_spikes = qc_spikes_isolated_precip(hydrometeor_type, sample_interval=sample_interval)
847
+
848
+ # ------------------------------------------------------------------------.
849
+ #### Define n_particles_<hydro_class>
850
+ n_graupel_ld_final = xr.where(flag_graupel == 1, n_graupel_ld, 0)
851
+ n_graupel_hd_final = xr.where(flag_graupel == 2, n_graupel_hd, 0)
852
+
853
+ n_small_hail_final = xr.where(flag_hail == 1, n_small_hail, 0)
854
+ n_large_hail_final = xr.where(flag_hail == 2, n_large_hail, 0)
855
+ n_margin_fallers_final = xr.where(precipitation_type == 0, n_margin_fallers, 0)
856
+ n_splashing_final = xr.where(flag_splashing == 1, n_splashing, 0)
857
+
858
+ # ------------------------------------------------------------------------.
859
+ # Create HC and QC dataset
860
+ ds_class = ds[["time"]]
861
+
862
+ # ds_class["label"] = label
863
+ ds_class["precipitation_type"] = precipitation_type
864
+ ds_class["hydrometeor_type"] = hydrometeor_type
865
+
866
+ ds_class["n_particles"] = n_particles
867
+
868
+ ds_class["n_low_density_graupel"] = n_graupel_ld_final
869
+ ds_class["n_high_density_graupel"] = n_graupel_hd_final
870
+
871
+ ds_class["n_small_hail"] = n_small_hail_final
872
+ ds_class["n_large_hail"] = n_large_hail_final
873
+ ds_class["n_margin_fallers"] = n_margin_fallers_final
874
+ ds_class["n_splashing"] = n_splashing_final
875
+
876
+ # fraction_splash
877
+ # fraction_margin_fallers
878
+
879
+ # ds_class["mask_graupel"] = graupel_mask_without_splash
880
+ # ds_class["mask_splashing"] = mask_splashing
881
+
882
+ ds_class["flag_hail"] = flag_hail
883
+ ds_class["flag_graupel"] = flag_graupel
884
+
885
+ ds_class["flag_noise"] = flag_noise
886
+ ds_class["flag_spikes"] = flag_spikes
887
+ ds_class["flag_splashing"] = flag_splashing
888
+ ds_class["flag_wind_artefacts"] = flag_wind_artefacts
889
+ return ds_class
890
+
891
+
892
+ ####--------------------------------------------------------------
893
+ #### Other utilities
894
+ def map_precip_flag_to_precipitation_type(precip_flag):
895
+ """Map OCEANRAIN precip_flag to DISDRODB precipitation_type."""
896
+ mapping_dict = {
897
+ 0: 0, # rain → rainfall
898
+ 1: 1, # snow → snowfall
899
+ 2: 2, # mixed_phase → mixed
900
+ -1: -1, # true_zero_value → no_precipitation
901
+ 4: -2, # inoperative → undefined
902
+ 5: -2, # harbor_time_no_data → undefined
903
+ }
904
+ precipitation_type = xr_remap_numeric_array(precip_flag, mapping_dict)
905
+ precipitation_type.attrs.update(
906
+ {
907
+ "long_name": "precipitation phase classification",
908
+ "standard_name": "precipitation_phase",
909
+ "units": "1",
910
+ "flag_values": [-2, -1, 0, 1, 2],
911
+ "flag_meanings": "undefined no_precipitation rainfall snowfall mixed_phase",
912
+ },
913
+ )
914
+ return precipitation_type