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