disdrodb 0.0.21__py3-none-any.whl → 0.1.1__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 (279) hide show
  1. disdrodb/__init__.py +132 -15
  2. disdrodb/_config.py +4 -2
  3. disdrodb/_version.py +9 -4
  4. disdrodb/api/checks.py +264 -237
  5. disdrodb/api/configs.py +4 -8
  6. disdrodb/api/create_directories.py +235 -290
  7. disdrodb/api/info.py +217 -26
  8. disdrodb/api/io.py +306 -270
  9. disdrodb/api/path.py +597 -173
  10. disdrodb/api/search.py +486 -0
  11. disdrodb/{metadata/scripts → cli}/disdrodb_check_metadata_archive.py +12 -7
  12. disdrodb/{utils/pandas.py → cli/disdrodb_data_archive_directory.py} +9 -18
  13. disdrodb/cli/disdrodb_download_archive.py +86 -0
  14. disdrodb/cli/disdrodb_download_metadata_archive.py +53 -0
  15. disdrodb/cli/disdrodb_download_station.py +84 -0
  16. disdrodb/{api/scripts → cli}/disdrodb_initialize_station.py +22 -10
  17. disdrodb/cli/disdrodb_metadata_archive_directory.py +32 -0
  18. disdrodb/{data_transfer/scripts/disdrodb_download_station.py → cli/disdrodb_open_data_archive.py} +22 -22
  19. disdrodb/cli/disdrodb_open_logs_directory.py +69 -0
  20. disdrodb/{data_transfer/scripts/disdrodb_upload_station.py → cli/disdrodb_open_metadata_archive.py} +22 -24
  21. disdrodb/cli/disdrodb_open_metadata_directory.py +71 -0
  22. disdrodb/cli/disdrodb_open_product_directory.py +74 -0
  23. disdrodb/cli/disdrodb_open_readers_directory.py +32 -0
  24. disdrodb/{l0/scripts → cli}/disdrodb_run_l0.py +38 -31
  25. disdrodb/{l0/scripts → cli}/disdrodb_run_l0_station.py +32 -30
  26. disdrodb/{l0/scripts → cli}/disdrodb_run_l0a.py +30 -21
  27. disdrodb/{l0/scripts → cli}/disdrodb_run_l0a_station.py +24 -33
  28. disdrodb/{l0/scripts → cli}/disdrodb_run_l0b.py +30 -21
  29. disdrodb/{l0/scripts → cli}/disdrodb_run_l0b_station.py +25 -34
  30. disdrodb/cli/disdrodb_run_l0c.py +130 -0
  31. disdrodb/cli/disdrodb_run_l0c_station.py +129 -0
  32. disdrodb/cli/disdrodb_run_l1.py +122 -0
  33. disdrodb/cli/disdrodb_run_l1_station.py +121 -0
  34. disdrodb/cli/disdrodb_run_l2e.py +122 -0
  35. disdrodb/cli/disdrodb_run_l2e_station.py +122 -0
  36. disdrodb/cli/disdrodb_run_l2m.py +122 -0
  37. disdrodb/cli/disdrodb_run_l2m_station.py +122 -0
  38. disdrodb/cli/disdrodb_upload_archive.py +105 -0
  39. disdrodb/cli/disdrodb_upload_station.py +98 -0
  40. disdrodb/configs.py +90 -25
  41. disdrodb/data_transfer/__init__.py +22 -0
  42. disdrodb/data_transfer/download_data.py +87 -90
  43. disdrodb/data_transfer/upload_data.py +64 -37
  44. disdrodb/data_transfer/zenodo.py +15 -18
  45. disdrodb/docs.py +1 -1
  46. disdrodb/issue/__init__.py +17 -4
  47. disdrodb/issue/checks.py +10 -23
  48. disdrodb/issue/reader.py +9 -12
  49. disdrodb/issue/writer.py +14 -17
  50. disdrodb/l0/__init__.py +17 -26
  51. disdrodb/l0/check_configs.py +35 -23
  52. disdrodb/l0/check_standards.py +46 -51
  53. disdrodb/l0/configs/{Thies_LPM → LPM}/bins_diameter.yml +44 -44
  54. disdrodb/l0/configs/{Thies_LPM → LPM}/bins_velocity.yml +40 -40
  55. disdrodb/l0/configs/LPM/l0a_encodings.yml +80 -0
  56. disdrodb/l0/configs/{Thies_LPM → LPM}/l0b_cf_attrs.yml +84 -65
  57. disdrodb/l0/configs/{Thies_LPM → LPM}/l0b_encodings.yml +50 -9
  58. disdrodb/l0/configs/{Thies_LPM → LPM}/raw_data_format.yml +285 -245
  59. disdrodb/l0/configs/{OTT_Parsivel → PARSIVEL}/bins_diameter.yml +66 -66
  60. disdrodb/l0/configs/{OTT_Parsivel → PARSIVEL}/bins_velocity.yml +64 -64
  61. disdrodb/l0/configs/PARSIVEL/l0a_encodings.yml +32 -0
  62. disdrodb/l0/configs/{OTT_Parsivel → PARSIVEL}/l0b_cf_attrs.yml +23 -21
  63. disdrodb/l0/configs/{OTT_Parsivel → PARSIVEL}/l0b_encodings.yml +17 -17
  64. disdrodb/l0/configs/{OTT_Parsivel → PARSIVEL}/raw_data_format.yml +77 -77
  65. disdrodb/l0/configs/{OTT_Parsivel2 → PARSIVEL2}/bins_diameter.yml +64 -64
  66. disdrodb/l0/configs/{OTT_Parsivel2 → PARSIVEL2}/bins_velocity.yml +64 -64
  67. disdrodb/l0/configs/PARSIVEL2/l0a_encodings.yml +39 -0
  68. disdrodb/l0/configs/{OTT_Parsivel2 → PARSIVEL2}/l0b_cf_attrs.yml +28 -26
  69. disdrodb/l0/configs/{OTT_Parsivel2 → PARSIVEL2}/l0b_encodings.yml +20 -20
  70. disdrodb/l0/configs/{OTT_Parsivel2 → PARSIVEL2}/raw_data_format.yml +107 -107
  71. disdrodb/l0/configs/PWS100/bins_diameter.yml +173 -0
  72. disdrodb/l0/configs/PWS100/bins_velocity.yml +173 -0
  73. disdrodb/l0/configs/PWS100/l0a_encodings.yml +19 -0
  74. disdrodb/l0/configs/PWS100/l0b_cf_attrs.yml +76 -0
  75. disdrodb/l0/configs/PWS100/l0b_encodings.yml +176 -0
  76. disdrodb/l0/configs/PWS100/raw_data_format.yml +182 -0
  77. disdrodb/l0/configs/{RD_80 → RD80}/bins_diameter.yml +40 -40
  78. disdrodb/l0/configs/RD80/l0a_encodings.yml +16 -0
  79. disdrodb/l0/configs/{RD_80 → RD80}/l0b_cf_attrs.yml +3 -3
  80. disdrodb/l0/configs/RD80/l0b_encodings.yml +135 -0
  81. disdrodb/l0/configs/{RD_80 → RD80}/raw_data_format.yml +46 -50
  82. disdrodb/l0/l0_reader.py +216 -340
  83. disdrodb/l0/l0a_processing.py +237 -208
  84. disdrodb/l0/l0b_nc_processing.py +227 -80
  85. disdrodb/l0/l0b_processing.py +96 -174
  86. disdrodb/l0/l0c_processing.py +627 -0
  87. disdrodb/l0/readers/{ARM → LPM/ARM}/ARM_LPM.py +36 -58
  88. disdrodb/l0/readers/LPM/AUSTRALIA/MELBOURNE_2007_LPM.py +236 -0
  89. disdrodb/l0/readers/LPM/BRAZIL/CHUVA_LPM.py +185 -0
  90. disdrodb/l0/readers/LPM/BRAZIL/GOAMAZON_LPM.py +185 -0
  91. disdrodb/l0/readers/LPM/ITALY/GID_LPM.py +195 -0
  92. disdrodb/l0/readers/LPM/ITALY/GID_LPM_W.py +210 -0
  93. disdrodb/l0/readers/{BRAZIL/GOAMAZON_LPM.py → LPM/KIT/CHWALA.py} +97 -76
  94. disdrodb/l0/readers/LPM/SLOVENIA/ARSO.py +197 -0
  95. disdrodb/l0/readers/LPM/SLOVENIA/CRNI_VRH.py +197 -0
  96. disdrodb/l0/readers/{UK → LPM/UK}/DIVEN.py +14 -35
  97. disdrodb/l0/readers/PARSIVEL/AUSTRALIA/MELBOURNE_2007_PARSIVEL.py +157 -0
  98. disdrodb/l0/readers/PARSIVEL/CHINA/CHONGQING.py +113 -0
  99. disdrodb/l0/readers/{EPFL → PARSIVEL/EPFL}/ARCTIC_2021.py +40 -57
  100. disdrodb/l0/readers/{EPFL → PARSIVEL/EPFL}/COMMON_2011.py +37 -54
  101. disdrodb/l0/readers/{EPFL → PARSIVEL/EPFL}/DAVOS_2009_2011.py +34 -51
  102. disdrodb/l0/readers/{EPFL → PARSIVEL/EPFL}/EPFL_2009.py +34 -51
  103. disdrodb/l0/readers/{EPFL/PARADISO_2014.py → PARSIVEL/EPFL/EPFL_ROOF_2008.py} +38 -50
  104. disdrodb/l0/readers/PARSIVEL/EPFL/EPFL_ROOF_2010.py +105 -0
  105. disdrodb/l0/readers/{EPFL → PARSIVEL/EPFL}/EPFL_ROOF_2011.py +34 -51
  106. disdrodb/l0/readers/{EPFL → PARSIVEL/EPFL}/EPFL_ROOF_2012.py +33 -51
  107. disdrodb/l0/readers/{EPFL → PARSIVEL/EPFL}/GENEPI_2007.py +25 -44
  108. disdrodb/l0/readers/{EPFL → PARSIVEL/EPFL}/GRAND_ST_BERNARD_2007.py +25 -44
  109. disdrodb/l0/readers/{EPFL → PARSIVEL/EPFL}/GRAND_ST_BERNARD_2007_2.py +25 -44
  110. disdrodb/l0/readers/{EPFL → PARSIVEL/EPFL}/HPICONET_2010.py +34 -51
  111. disdrodb/l0/readers/{EPFL/EPFL_ROOF_2010.py → PARSIVEL/EPFL/HYMEX_LTE_SOP2.py} +37 -50
  112. disdrodb/l0/readers/PARSIVEL/EPFL/HYMEX_LTE_SOP3.py +111 -0
  113. disdrodb/l0/readers/{EPFL → PARSIVEL/EPFL}/HYMEX_LTE_SOP4.py +36 -54
  114. disdrodb/l0/readers/{EPFL → PARSIVEL/EPFL}/LOCARNO_2018.py +34 -52
  115. disdrodb/l0/readers/{EPFL → PARSIVEL/EPFL}/LOCARNO_2019.py +38 -56
  116. disdrodb/l0/readers/PARSIVEL/EPFL/PARADISO_2014.py +105 -0
  117. disdrodb/l0/readers/{EPFL → PARSIVEL/EPFL}/PARSIVEL_2007.py +27 -45
  118. disdrodb/l0/readers/{EPFL → PARSIVEL/EPFL}/PLATO_2019.py +24 -44
  119. disdrodb/l0/readers/PARSIVEL/EPFL/RACLETS_2019.py +140 -0
  120. disdrodb/l0/readers/{EPFL → PARSIVEL/EPFL}/RACLETS_2019_WJF.py +41 -59
  121. disdrodb/l0/readers/{EPFL → PARSIVEL/EPFL}/RIETHOLZBACH_2011.py +34 -51
  122. disdrodb/l0/readers/PARSIVEL/EPFL/SAMOYLOV_2017.py +117 -0
  123. disdrodb/l0/readers/PARSIVEL/EPFL/SAMOYLOV_2019.py +137 -0
  124. disdrodb/l0/readers/{EPFL → PARSIVEL/EPFL}/UNIL_2022.py +42 -55
  125. disdrodb/l0/readers/PARSIVEL/GPM/IFLOODS.py +104 -0
  126. disdrodb/l0/readers/{GPM → PARSIVEL/GPM}/LPVEX.py +29 -48
  127. disdrodb/l0/readers/PARSIVEL/GPM/MC3E.py +184 -0
  128. disdrodb/l0/readers/PARSIVEL/KIT/BURKINA_FASO.py +133 -0
  129. disdrodb/l0/readers/PARSIVEL/NCAR/CCOPE_2015.py +113 -0
  130. disdrodb/l0/readers/{NCAR/VORTEX_SE_2016_P1.py → PARSIVEL/NCAR/OWLES_MIPS.py} +46 -72
  131. disdrodb/l0/readers/PARSIVEL/NCAR/PECAN_MOBILE.py +125 -0
  132. disdrodb/l0/readers/{NCAR/OWLES_MIPS.py → PARSIVEL/NCAR/PLOWS_MIPS.py} +45 -64
  133. disdrodb/l0/readers/PARSIVEL/NCAR/VORTEX2_2009.py +114 -0
  134. disdrodb/l0/readers/PARSIVEL/NCAR/VORTEX2_2010.py +176 -0
  135. disdrodb/l0/readers/PARSIVEL/NCAR/VORTEX2_2010_UF.py +183 -0
  136. disdrodb/l0/readers/PARSIVEL/SLOVENIA/UL_FGG.py +121 -0
  137. disdrodb/l0/readers/{ARM/ARM_LD.py → PARSIVEL2/ARM/ARM_PARSIVEL2.py} +27 -50
  138. disdrodb/l0/readers/PARSIVEL2/BRAZIL/CHUVA_PARSIVEL2.py +163 -0
  139. disdrodb/l0/readers/PARSIVEL2/BRAZIL/GOAMAZON_PARSIVEL2.py +163 -0
  140. disdrodb/l0/readers/{DENMARK → PARSIVEL2/DENMARK}/EROSION_nc.py +14 -35
  141. disdrodb/l0/readers/PARSIVEL2/FRANCE/ENPC_PARSIVEL2.py +189 -0
  142. disdrodb/l0/readers/PARSIVEL2/FRANCE/SIRTA_PARSIVEL2.py +119 -0
  143. disdrodb/l0/readers/PARSIVEL2/GPM/GCPEX.py +104 -0
  144. disdrodb/l0/readers/PARSIVEL2/GPM/NSSTC.py +176 -0
  145. disdrodb/l0/readers/PARSIVEL2/ITALY/GID_PARSIVEL2.py +32 -0
  146. disdrodb/l0/readers/PARSIVEL2/MEXICO/OH_IIUNAM_nc.py +56 -0
  147. disdrodb/l0/readers/PARSIVEL2/NCAR/PECAN_FP3.py +120 -0
  148. disdrodb/l0/readers/{NCAR → PARSIVEL2/NCAR}/PECAN_MIPS.py +45 -64
  149. disdrodb/l0/readers/PARSIVEL2/NCAR/RELAMPAGO_PARSIVEL2.py +181 -0
  150. disdrodb/l0/readers/PARSIVEL2/NCAR/SNOWIE_PJ.py +160 -0
  151. disdrodb/l0/readers/PARSIVEL2/NCAR/SNOWIE_SB.py +160 -0
  152. disdrodb/l0/readers/{NCAR/PLOWS_MIPS.py → PARSIVEL2/NCAR/VORTEX_SE_2016_P1.py} +49 -66
  153. disdrodb/l0/readers/PARSIVEL2/NCAR/VORTEX_SE_2016_P2.py +118 -0
  154. disdrodb/l0/readers/PARSIVEL2/NCAR/VORTEX_SE_2016_PIPS.py +152 -0
  155. disdrodb/l0/readers/PARSIVEL2/NETHERLANDS/DELFT.py +166 -0
  156. disdrodb/l0/readers/PWS100/FRANCE/ENPC_PWS100.py +150 -0
  157. disdrodb/l0/readers/{NCAR/RELAMPAGO_RD80.py → RD80/BRAZIL/CHUVA_RD80.py} +36 -60
  158. disdrodb/l0/readers/{BRAZIL → RD80/BRAZIL}/GOAMAZON_RD80.py +36 -55
  159. disdrodb/l0/readers/{NCAR → RD80/NCAR}/CINDY_2011_RD80.py +35 -54
  160. disdrodb/l0/readers/{BRAZIL/CHUVA_RD80.py → RD80/NCAR/RELAMPAGO_RD80.py} +40 -54
  161. disdrodb/l0/readers/RD80/NOAA/PSL_RD80.py +274 -0
  162. disdrodb/l0/readers/template_reader_raw_netcdf_data.py +62 -0
  163. disdrodb/l0/readers/{reader_template.py → template_reader_raw_text_data.py} +20 -44
  164. disdrodb/l0/routines.py +885 -581
  165. disdrodb/l0/standards.py +77 -238
  166. disdrodb/l0/template_tools.py +105 -110
  167. disdrodb/l1/__init__.py +17 -0
  168. disdrodb/l1/beard_model.py +716 -0
  169. disdrodb/l1/encoding_attrs.py +635 -0
  170. disdrodb/l1/fall_velocity.py +260 -0
  171. disdrodb/l1/filters.py +192 -0
  172. disdrodb/l1/processing.py +202 -0
  173. disdrodb/l1/resampling.py +236 -0
  174. disdrodb/l1/routines.py +358 -0
  175. disdrodb/l1_env/__init__.py +17 -0
  176. disdrodb/l1_env/routines.py +38 -0
  177. disdrodb/l2/__init__.py +17 -0
  178. disdrodb/l2/empirical_dsd.py +1833 -0
  179. disdrodb/l2/event.py +388 -0
  180. disdrodb/l2/processing.py +528 -0
  181. disdrodb/l2/processing_options.py +213 -0
  182. disdrodb/l2/routines.py +868 -0
  183. disdrodb/metadata/__init__.py +9 -2
  184. disdrodb/metadata/checks.py +180 -124
  185. disdrodb/metadata/download.py +81 -0
  186. disdrodb/metadata/geolocation.py +146 -0
  187. disdrodb/metadata/info.py +20 -13
  188. disdrodb/metadata/manipulation.py +3 -3
  189. disdrodb/metadata/reader.py +59 -8
  190. disdrodb/metadata/search.py +77 -144
  191. disdrodb/metadata/standards.py +83 -80
  192. disdrodb/metadata/writer.py +10 -16
  193. disdrodb/psd/__init__.py +38 -0
  194. disdrodb/psd/fitting.py +2146 -0
  195. disdrodb/psd/models.py +774 -0
  196. disdrodb/routines.py +1412 -0
  197. disdrodb/scattering/__init__.py +28 -0
  198. disdrodb/scattering/axis_ratio.py +344 -0
  199. disdrodb/scattering/routines.py +456 -0
  200. disdrodb/utils/__init__.py +17 -0
  201. disdrodb/utils/attrs.py +208 -0
  202. disdrodb/utils/cli.py +269 -0
  203. disdrodb/utils/compression.py +60 -42
  204. disdrodb/utils/dask.py +62 -0
  205. disdrodb/utils/dataframe.py +342 -0
  206. disdrodb/utils/decorators.py +110 -0
  207. disdrodb/utils/directories.py +107 -46
  208. disdrodb/utils/encoding.py +127 -0
  209. disdrodb/utils/list.py +29 -0
  210. disdrodb/utils/logger.py +168 -46
  211. disdrodb/utils/time.py +657 -0
  212. disdrodb/utils/warnings.py +30 -0
  213. disdrodb/utils/writer.py +57 -0
  214. disdrodb/utils/xarray.py +138 -47
  215. disdrodb/utils/yaml.py +0 -1
  216. disdrodb/viz/__init__.py +17 -0
  217. disdrodb/viz/plots.py +17 -0
  218. disdrodb-0.1.1.dist-info/METADATA +294 -0
  219. disdrodb-0.1.1.dist-info/RECORD +232 -0
  220. {disdrodb-0.0.21.dist-info → disdrodb-0.1.1.dist-info}/WHEEL +1 -1
  221. disdrodb-0.1.1.dist-info/entry_points.txt +30 -0
  222. disdrodb/data_transfer/scripts/disdrodb_download_archive.py +0 -53
  223. disdrodb/data_transfer/scripts/disdrodb_upload_archive.py +0 -57
  224. disdrodb/l0/configs/OTT_Parsivel/l0a_encodings.yml +0 -32
  225. disdrodb/l0/configs/OTT_Parsivel2/l0a_encodings.yml +0 -39
  226. disdrodb/l0/configs/RD_80/l0a_encodings.yml +0 -16
  227. disdrodb/l0/configs/RD_80/l0b_encodings.yml +0 -135
  228. disdrodb/l0/configs/Thies_LPM/l0a_encodings.yml +0 -80
  229. disdrodb/l0/io.py +0 -257
  230. disdrodb/l0/l0_processing.py +0 -1091
  231. disdrodb/l0/readers/AUSTRALIA/MELBOURNE_2007_OTT.py +0 -178
  232. disdrodb/l0/readers/AUSTRALIA/MELBOURNE_2007_THIES.py +0 -247
  233. disdrodb/l0/readers/BRAZIL/CHUVA_LPM.py +0 -204
  234. disdrodb/l0/readers/BRAZIL/CHUVA_OTT.py +0 -183
  235. disdrodb/l0/readers/BRAZIL/GOAMAZON_OTT.py +0 -183
  236. disdrodb/l0/readers/CHINA/CHONGQING.py +0 -131
  237. disdrodb/l0/readers/EPFL/EPFL_ROOF_2008.py +0 -128
  238. disdrodb/l0/readers/EPFL/HYMEX_LTE_SOP2.py +0 -127
  239. disdrodb/l0/readers/EPFL/HYMEX_LTE_SOP3.py +0 -129
  240. disdrodb/l0/readers/EPFL/RACLETS_2019.py +0 -158
  241. disdrodb/l0/readers/EPFL/SAMOYLOV_2017.py +0 -136
  242. disdrodb/l0/readers/EPFL/SAMOYLOV_2019.py +0 -158
  243. disdrodb/l0/readers/FRANCE/SIRTA_OTT2.py +0 -138
  244. disdrodb/l0/readers/GPM/GCPEX.py +0 -123
  245. disdrodb/l0/readers/GPM/IFLOODS.py +0 -123
  246. disdrodb/l0/readers/GPM/MC3E.py +0 -123
  247. disdrodb/l0/readers/GPM/NSSTC.py +0 -164
  248. disdrodb/l0/readers/ITALY/GID.py +0 -199
  249. disdrodb/l0/readers/MEXICO/OH_IIUNAM_nc.py +0 -92
  250. disdrodb/l0/readers/NCAR/CCOPE_2015.py +0 -133
  251. disdrodb/l0/readers/NCAR/PECAN_FP3.py +0 -137
  252. disdrodb/l0/readers/NCAR/PECAN_MOBILE.py +0 -144
  253. disdrodb/l0/readers/NCAR/RELAMPAGO_OTT.py +0 -195
  254. disdrodb/l0/readers/NCAR/SNOWIE_PJ.py +0 -172
  255. disdrodb/l0/readers/NCAR/SNOWIE_SB.py +0 -179
  256. disdrodb/l0/readers/NCAR/VORTEX2_2009.py +0 -133
  257. disdrodb/l0/readers/NCAR/VORTEX2_2010.py +0 -188
  258. disdrodb/l0/readers/NCAR/VORTEX2_2010_UF.py +0 -191
  259. disdrodb/l0/readers/NCAR/VORTEX_SE_2016_P2.py +0 -135
  260. disdrodb/l0/readers/NCAR/VORTEX_SE_2016_PIPS.py +0 -170
  261. disdrodb/l0/readers/NETHERLANDS/DELFT.py +0 -187
  262. disdrodb/l0/readers/SPAIN/SBEGUERIA.py +0 -179
  263. disdrodb/l0/scripts/disdrodb_run_l0b_concat.py +0 -93
  264. disdrodb/l0/scripts/disdrodb_run_l0b_concat_station.py +0 -85
  265. disdrodb/utils/netcdf.py +0 -452
  266. disdrodb/utils/scripts.py +0 -102
  267. disdrodb-0.0.21.dist-info/AUTHORS.md +0 -18
  268. disdrodb-0.0.21.dist-info/METADATA +0 -186
  269. disdrodb-0.0.21.dist-info/RECORD +0 -168
  270. disdrodb-0.0.21.dist-info/entry_points.txt +0 -15
  271. /disdrodb/l0/configs/{RD_80 → RD80}/bins_velocity.yml +0 -0
  272. /disdrodb/l0/manuals/{Thies_LPM.pdf → LPM.pdf} +0 -0
  273. /disdrodb/l0/manuals/{ODM_470.pdf → ODM470.pdf} +0 -0
  274. /disdrodb/l0/manuals/{OTT_Parsivel.pdf → PARSIVEL.pdf} +0 -0
  275. /disdrodb/l0/manuals/{OTT_Parsivel2.pdf → PARSIVEL2.pdf} +0 -0
  276. /disdrodb/l0/manuals/{PWS_100.pdf → PWS100.pdf} +0 -0
  277. /disdrodb/l0/manuals/{RD_80.pdf → RD80.pdf} +0 -0
  278. {disdrodb-0.0.21.dist-info → disdrodb-0.1.1.dist-info/licenses}/LICENSE +0 -0
  279. {disdrodb-0.0.21.dist-info → disdrodb-0.1.1.dist-info}/top_level.txt +0 -0
disdrodb/psd/models.py ADDED
@@ -0,0 +1,774 @@
1
+ # -----------------------------------------------------------------------------.
2
+ # Copyright (c) 2021-2023 DISDRODB developers
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+ # -----------------------------------------------------------------------------.
17
+ """Definition of PSD models.
18
+
19
+ The class implementation is inspired by pytmatrix.psd and pyradsim.psd modules
20
+ and adapted to allow efficient vectorized computations with xarray.
21
+
22
+ Source code:
23
+ - https://github.com/jleinonen/pytmatrix/blob/master/pytmatrix/psd.py
24
+ - https://github.com/wolfidan/pyradsim/blob/master/pyradsim/psd.py
25
+
26
+ """
27
+ import importlib
28
+
29
+ import dask.array
30
+ import numpy as np
31
+ import xarray as xr
32
+ from scipy.interpolate import PchipInterpolator, interp1d
33
+ from scipy.special import gamma as gamma_f
34
+
35
+ from disdrodb import DIAMETER_DIMENSION
36
+ from disdrodb.utils.warnings import suppress_warnings
37
+
38
+ # Check if pytmatrix is available
39
+ # - We import pytmatrix.PSD class to pass isinstance(obj, PSD) checks in pytmatrix
40
+ if importlib.util.find_spec("pytmatrix") is not None:
41
+ from pytmatrix.psd import PSD
42
+ else:
43
+
44
+ class PSD:
45
+ """Dummy."""
46
+
47
+ pass
48
+
49
+
50
+ def available_psd_models():
51
+ """Return a list of available PSD models."""
52
+ return list(PSD_MODELS_DICT)
53
+
54
+
55
+ def check_psd_model(psd_model):
56
+ """Check validity of a PSD model."""
57
+ available_models = available_psd_models()
58
+ if psd_model not in available_models:
59
+ raise ValueError(f"{psd_model} is an invalid PSD model. Valid models are: {available_models}.")
60
+ return psd_model
61
+
62
+
63
+ def check_input_parameters(parameters):
64
+ """Check valid input parameters."""
65
+ for param, value in parameters.items():
66
+ if not (is_scalar(value) or isinstance(value, xr.DataArray)):
67
+ raise TypeError(f"Parameter {param} must be a scalar or xarray.DataArray, not {type(value)}")
68
+ return parameters
69
+
70
+
71
+ def check_diameter_inputs(D):
72
+ """Check valid diameter input."""
73
+ if isinstance(D, xr.DataArray) or is_scalar(D):
74
+ return D
75
+ if isinstance(D, (tuple, list)):
76
+ D = np.asanyarray(D)
77
+ if isinstance(D, (np.ndarray, dask.array.Array)):
78
+ if D.ndim != 1:
79
+ raise ValueError("Expecting a 1-dimensional diameter array.")
80
+ if D.size == 0:
81
+ raise ValueError("Expecting a non-empty diameter array.")
82
+ return xr.DataArray(D, dims=DIAMETER_DIMENSION)
83
+ raise TypeError(f"Invalid diameter type: {type(D)}")
84
+
85
+
86
+ def get_psd_model(psd_model):
87
+ """Retrieve the PSD Class."""
88
+ return PSD_MODELS_DICT[psd_model]
89
+
90
+
91
+ def get_psd_model_formula(psd_model):
92
+ """Retrieve the PSD formula."""
93
+ return PSD_MODELS_DICT[psd_model].formula
94
+
95
+
96
+ def create_psd(psd_model, parameters): # TODO: check name around
97
+ """Define a PSD from a dictionary or xr.Dataset of parameters."""
98
+ psd_class = get_psd_model(psd_model)
99
+ psd = psd_class.from_parameters(parameters)
100
+ return psd
101
+
102
+
103
+ def get_required_parameters(psd_model):
104
+ """Retrieve the list of parameters required by a PSD model."""
105
+ psd_class = get_psd_model(psd_model)
106
+ return psd_class.required_parameters()
107
+
108
+
109
+ def is_scalar(value):
110
+ """Determines if the input value is a scalar."""
111
+ return isinstance(value, (float, int)) or (isinstance(value, (np.ndarray, xr.DataArray)) and value.size == 1)
112
+
113
+
114
+ class XarrayPSD(PSD):
115
+ """PSD class template allowing vectorized computations with xarray.
116
+
117
+ We currently inherit from pytmatrix PSD to allow scattering simulations:
118
+ --> https://github.com/ltelab/pytmatrix-lte/blob/880170b4ca62a04e8c843619fa1b8713b9e11894/pytmatrix/psd.py#L321
119
+ """
120
+
121
+ def __call__(self, D):
122
+ """Compute the PSD."""
123
+ D = check_diameter_inputs(D)
124
+ with suppress_warnings():
125
+ return self.formula(D=D, **self.parameters)
126
+
127
+ def has_scalar_parameters(self):
128
+ """Check if the PSD object contains only a single set of parameters."""
129
+ return np.all([is_scalar(value) for value in self.parameters.values()])
130
+
131
+ def has_xarray_parameters(self):
132
+ """Check if the PSD object contains at least one xarray parameter."""
133
+ return any(isinstance(value, xr.DataArray) for param, value in self.parameters.items())
134
+
135
+ def isel(self, **kwargs):
136
+ """Subset the parameters by index using xarray.isel.
137
+
138
+ If the PSD has xarray parameters, returns a new PSD with subset parameters.
139
+ Otherwise raises an error.
140
+ """
141
+ if not self.has_xarray_parameters():
142
+ raise ValueError("isel() can only be used when PSD model parameters are xarray DataArrays")
143
+
144
+ # Subset each xarray parameter
145
+ new_params = {}
146
+ for param, value in self.parameters.items():
147
+ if isinstance(value, xr.DataArray):
148
+ new_params[param] = value.isel(**kwargs)
149
+ else:
150
+ new_params[param] = value
151
+
152
+ # Create new PSD instance
153
+ return self.__class__.from_parameters(new_params)
154
+
155
+ def sel(self, **kwargs):
156
+ """Subset the parameters by label using xarray.sel.
157
+
158
+ If the PSD has xarray parameters, returns a new PSD with subset parameters.
159
+ Otherwise raises an error.
160
+ """
161
+ if not self.has_xarray_parameters():
162
+ raise ValueError("sel() can only be used when PSD model parameters are xarray DataArrays")
163
+
164
+ # Subset each xarray parameter
165
+ new_params = {}
166
+ for param, value in self.parameters.items():
167
+ if isinstance(value, xr.DataArray):
168
+ new_params[param] = value.sel(**kwargs)
169
+ else:
170
+ new_params[param] = value
171
+
172
+ # Create new PSD instance
173
+ return self.__class__.from_parameters(new_params)
174
+
175
+ def __eq__(self, other):
176
+ """Check if two objects are equal."""
177
+ # Check class equality
178
+ if not isinstance(other, self.__class__):
179
+ return False
180
+ # Get required parameters
181
+ params = self.required_parameters()
182
+ # Check scalar parameters case
183
+ if self.has_scalar_parameters() and other.has_scalar_parameters():
184
+ return all(self.parameters[param] == other.parameters[param] for param in params)
185
+ # Check array parameters case
186
+ return all(np.all(self.parameters[param] == other.parameters[param]) for param in params)
187
+
188
+ # def moment(self, D, dD, order):
189
+ # """
190
+ # Compute the moments of the Particle Size Distribution (PSD).
191
+
192
+ # Parameters
193
+ # ----------
194
+ # D: array-like
195
+ # Diameter bin center in m.
196
+ # dD: array-like
197
+ # Diameter bin width in mm.
198
+ # order : int
199
+ # The order of the moment to compute.
200
+
201
+ # Returns
202
+ # -------
203
+ # float
204
+ # The computed moment of the PSD.
205
+
206
+ # Notes
207
+ # -----
208
+ # The method uses numerical integration (trapezoidal rule) to compute the moment.
209
+ # """
210
+ # return np.trapezoid(D**order * self.__call__(D), x=D, dx=dD)
211
+
212
+
213
+ class LognormalPSD(XarrayPSD):
214
+ """Lognormal drop size distribution (DSD).
215
+
216
+ Callable class to provide a lognormal PSD with the given parameters.
217
+
218
+ The PSD form is:
219
+
220
+ N(D) = Nt/(sqrt(2*pi)*sigma*D)) * exp(-(ln(D)-mu)**2 / (2*sigma**2))
221
+
222
+ # g = sigma
223
+ # theta = 0
224
+
225
+ Attributes
226
+ ----------
227
+ Nt:
228
+ g:
229
+ theta:
230
+ mu:
231
+ sigma:
232
+
233
+ """
234
+
235
+ def __init__(self, Nt=1.0, mu=0.0, sigma=1.0):
236
+ self.Nt = Nt
237
+ self.mu = mu
238
+ self.sigma = sigma
239
+ self.parameters = {"Nt": self.Nt, "mu": self.mu, "sigma": self.sigma}
240
+ check_input_parameters(self.parameters)
241
+
242
+ @property
243
+ def name(self):
244
+ """Return name of the PSD."""
245
+ return "LognormalPSD"
246
+
247
+ @staticmethod
248
+ def formula(D, Nt, mu, sigma):
249
+ """Calculates the Lognormal PSD values."""
250
+ coeff = Nt / (np.sqrt(2.0 * np.pi) * sigma * (D))
251
+ return coeff * np.exp(-((np.log(D) - mu) ** 2) / (2.0 * sigma**2))
252
+
253
+ @staticmethod
254
+ def from_parameters(parameters):
255
+ """Initialize LognormalPSD from a dictionary or xr.Dataset.
256
+
257
+ Args:
258
+ parameters (dict or xr.Dataset): Parameters to initialize the class.
259
+
260
+ Returns
261
+ -------
262
+ LognormalPSD: An instance of LognormalPSD initialized with the parameters.
263
+ """
264
+ Nt = parameters["Nt"]
265
+ mu = parameters["mu"]
266
+ sigma = parameters["sigma"]
267
+ return LognormalPSD(Nt=Nt, mu=mu, sigma=sigma)
268
+
269
+ @staticmethod
270
+ def required_parameters():
271
+ """Return the required parameters of the PSD."""
272
+ return ["Nt", "mu", "sigma"]
273
+
274
+ def parameters_summary(self):
275
+ """Return a string with the parameter summary."""
276
+ if self.has_scalar_parameters():
277
+ summary = "".join(
278
+ [
279
+ f"{self.name}\n",
280
+ f"$Nt = {self.Nt:.2f}$\n",
281
+ f"$\\sigma = {self.sigma:.2f}$\n" f"$\\mu = {self.mu:.2f}$\n\n",
282
+ ],
283
+ )
284
+ else:
285
+ summary = "" f"{self.name} with N-d parameters \n"
286
+ return summary
287
+
288
+
289
+ class ExponentialPSD(XarrayPSD):
290
+ """Exponential particle size distribution (PSD).
291
+
292
+ Callable class to provide an exponential PSD with the given
293
+ parameters. The attributes can also be given as arguments to the
294
+ constructor.
295
+
296
+ The PSD form is:
297
+ N(D) = N0 * exp(-Lambda*D)
298
+
299
+ Attributes
300
+ ----------
301
+ N0: the intercept parameter.
302
+ Lambda: the inverse scale parameter
303
+
304
+ Args (call):
305
+ D: the particle diameter.
306
+
307
+ Returns (call):
308
+ The PSD value for the given diameter.
309
+ """
310
+
311
+ def __init__(self, N0=1.0, Lambda=1.0):
312
+ # Define parameters
313
+ self.N0 = N0
314
+ self.Lambda = Lambda
315
+ self.parameters = {"N0": self.N0, "Lambda": self.Lambda}
316
+ check_input_parameters(self.parameters)
317
+
318
+ @property
319
+ def name(self):
320
+ """Return name of the PSD."""
321
+ return "ExponentialPSD"
322
+
323
+ @staticmethod
324
+ def formula(D, N0, Lambda):
325
+ """Calculates the Exponential PSD values."""
326
+ return N0 * np.exp(-Lambda * D)
327
+
328
+ @staticmethod
329
+ def from_parameters(parameters):
330
+ """Initialize ExponentialPSD from a dictionary or xr.Dataset.
331
+
332
+ Args:
333
+ parameters (dict or xr.Dataset): Parameters to initialize the class.
334
+
335
+ Returns
336
+ -------
337
+ ExponentialPSD: An instance of ExponentialPSD initialized with the parameters.
338
+ """
339
+ N0 = parameters["N0"]
340
+ Lambda = parameters["Lambda"]
341
+ return ExponentialPSD(N0=N0, Lambda=Lambda)
342
+
343
+ @staticmethod
344
+ def required_parameters():
345
+ """Return the required parameters of the PSD."""
346
+ return ["N0", "Lambda"]
347
+
348
+ def parameters_summary(self):
349
+ """Return a string with the parameter summary."""
350
+ if self.has_scalar_parameters():
351
+ summary = "".join(
352
+ [
353
+ f"{self.name}\n",
354
+ f"$N0 = {self.N0:.2f}$\n",
355
+ f"$\\lambda = {self.Lambda:.2f}$\n\n",
356
+ ],
357
+ )
358
+ else:
359
+ summary = "" f"{self.name} with N-d parameters \n"
360
+ return summary
361
+
362
+
363
+ class GammaPSD(ExponentialPSD):
364
+ """Gamma particle size distribution (PSD).
365
+
366
+ Callable class to provide an gamma PSD with the given
367
+ parameters. The attributes can also be given as arguments to the
368
+ constructor.
369
+
370
+ The PSD form is:
371
+ N(D) = N0 * D**mu * exp(-Lambda*D)
372
+
373
+ Attributes
374
+ ----------
375
+ N0: the intercept parameter [mm**(-1-mu) m**-3] (scale parameter)
376
+ Lambda: the inverse scale parameter [mm-1] (slope parameter)
377
+ mu: the shape parameter [-]
378
+
379
+ Args (call):
380
+ D: the particle diameter.
381
+
382
+ Returns (call):
383
+ The PSD value for the given diameter.
384
+
385
+ References
386
+ ----------
387
+ Ulbrich, C. W., 1985: The Effects of Drop Size Distribution Truncation on
388
+ Rainfall Integral Parameters and Empirical Relations.
389
+ J. Appl. Meteor. Climatol., 24, 580-590, https://doi.org/10.1175/1520-0450(1985)024<0580:TEODSD>2.0.CO;2
390
+ """
391
+
392
+ def __init__(self, N0=1.0, mu=0.0, Lambda=1.0):
393
+ # Define parameters
394
+ self.N0 = N0
395
+ self.Lambda = Lambda
396
+ self.mu = mu
397
+ self.parameters = {"N0": self.N0, "mu": self.mu, "Lambda": self.Lambda}
398
+ check_input_parameters(self.parameters)
399
+
400
+ @property
401
+ def name(self):
402
+ """Return name of the PSD."""
403
+ return "GammaPSD"
404
+
405
+ @staticmethod
406
+ def formula(D, N0, Lambda, mu):
407
+ """Calculates the Gamma PSD values."""
408
+ return N0 * np.exp(mu * np.log(D) - Lambda * D)
409
+
410
+ @staticmethod
411
+ def from_parameters(parameters):
412
+ """Initialize GammaPSD from a dictionary or xr.Dataset.
413
+
414
+ Args:
415
+ parameters (dict or xr.Dataset): Parameters to initialize the class.
416
+
417
+ Returns
418
+ -------
419
+ GammaPSD: An instance of GammaPSD initialized with the parameters.
420
+ """
421
+ N0 = parameters["N0"]
422
+ Lambda = parameters["Lambda"]
423
+ mu = parameters["mu"]
424
+ return GammaPSD(N0=N0, Lambda=Lambda, mu=mu)
425
+
426
+ @staticmethod
427
+ def required_parameters():
428
+ """Return the required parameters of the PSD."""
429
+ return ["N0", "mu", "Lambda"]
430
+
431
+ def parameters_summary(self):
432
+ """Return a string with the parameter summary."""
433
+ if self.has_scalar_parameters():
434
+ summary = "".join(
435
+ [
436
+ f"{self.name}\n",
437
+ f"$\\mu = {self.mu:.2f}$\n",
438
+ f"$N0 = {self.N0:.2f}$\n",
439
+ f"$\\lambda = {self.Lambda:.2f}$\n\n",
440
+ ],
441
+ )
442
+ else:
443
+ summary = "" f"{self.name} with N-d parameters \n"
444
+ return summary
445
+
446
+
447
+ class NormalizedGammaPSD(XarrayPSD):
448
+ """Normalized gamma particle size distribution (PSD).
449
+
450
+ Callable class to provide a normalized gamma PSD with the given
451
+ parameters. The attributes can also be given as arguments to the
452
+ constructor.
453
+
454
+ The PSD form is:
455
+
456
+ N(D) = Nw * f(mu) * (D/D50)**mu * exp(-(mu+3.67)*D/D50)
457
+ f(mu) = 6/(3.67**4) * (mu+3.67)**(mu+4)/Gamma(mu+4)
458
+
459
+ An alternative formulation as function of Dm:
460
+ # Testud (2001), Bringi (2001), Williams et al., 2014, Dolan 2018
461
+ # --> Normalized with respect to liquid water content (mass) --> Nx=D3/Dm4
462
+ N(D) = Nw * f1(mu) * (D/Dm)**mu * exp(-(mu+4)*D/Dm) # Nw * f(D; Dm, mu)
463
+ f1(mu) = 6/(4**4) * (mu+4)**(mu+4)/Gamma(mu+4)
464
+
465
+ Note: gamma(4) = 6
466
+
467
+ An alternative formulation as function of Dm:
468
+ # Tokay et al., 2010
469
+ # Illingworth et al., 2002 (see eq10 to derive full formulation!)
470
+ # --> Normalized with respect to total concentration --> Nx = #/Dm
471
+ N(D) = Nt* * f2(mu) * (D/Dm)**mu * exp(-(mu+4)*D/Dm)
472
+ f2(mu) = (mu+4)**(mu+1)/Gamma(mu+1)
473
+
474
+ Attributes
475
+ ----------
476
+ D50: the median volume diameter.
477
+ Nw: the intercept parameter.
478
+ mu: the shape parameter.
479
+
480
+ Args (call):
481
+ D: the particle diameter.
482
+
483
+ Returns (call):
484
+ The PSD value for the given diameter.
485
+
486
+ References
487
+ ----------
488
+ Willis, P. T., 1984: Functional Fits to Some Observed Drop Size Distributions and Parameterization of Rain.
489
+ J. Atmos. Sci., 41, 1648-1661, https://doi.org/10.1175/1520-0469(1984)041<1648:FFTSOD>2.0.CO;2
490
+
491
+ Testud, J., S. Oury, R. A. Black, P. Amayenc, and X. Dou, 2001: The Concept of “Normalized” Distribution
492
+ to Describe Raindrop Spectra: A Tool for Cloud Physics and Cloud Remote Sensing.
493
+ J. Appl. Meteor. Climatol., 40, 1118-1140, https://doi.org/10.1175/1520-0450(2001)040<1118:TCONDT>2.0.CO;2
494
+
495
+ Illingworth, A. J., and T. M. Blackman, 2002:
496
+ The Need to Represent Raindrop Size Spectra as Normalized Gamma Distributions for
497
+ the Interpretation of Polarization Radar Observations.
498
+ J. Appl. Meteor. Climatol., 41, 286-297, https://doi.org/10.1175/1520-0450(2002)041<0286:TNTRRS>2.0.CO;2
499
+
500
+ Bringi, V. N., G. Huang, V. Chandrasekar, and E. Gorgucci, 2002:
501
+ A Methodology for Estimating the Parameters of a Gamma Raindrop Size Distribution Model from
502
+ Polarimetric Radar Data: Application to a Squall-Line Event from the TRMM/Brazil Campaign.
503
+ J. Atmos. Oceanic Technol., 19, 633-645, https://doi.org/10.1175/1520-0426(2002)019<0633:AMFETP>2.0.CO;2
504
+
505
+ Bringi, V. N., V. Chandrasekar, J. Hubbert, E. Gorgucci, W. L. Randeu, and M. Schoenhuber, 2003:
506
+ Raindrop Size Distribution in Different Climatic Regimes from Disdrometer and Dual-Polarized Radar Analysis.
507
+ J. Atmos. Sci., 60, 354-365, https://doi.org/10.1175/1520-0469(2003)060<0354:RSDIDC>2.0.CO;2
508
+
509
+ Tokay, A., and P. G. Bashor, 2010: An Experimental Study of Small-Scale Variability of Raindrop Size Distribution.
510
+ J. Appl. Meteor. Climatol., 49, 2348-2365, https://doi.org/10.1175/2010JAMC2269.1
511
+
512
+ """
513
+
514
+ def __init__(self, Nw=1.0, D50=1.0, mu=0.0):
515
+ self.D50 = D50
516
+ self.mu = mu
517
+ self.Nw = Nw
518
+ self.parameters = {"Nw": Nw, "D50": D50, "mu": mu}
519
+ check_input_parameters(self.parameters)
520
+
521
+ @property
522
+ def name(self):
523
+ """Return the PSD name."""
524
+ return "NormalizedGammaPSD"
525
+
526
+ @staticmethod
527
+ def formula(D, Nw, D50, mu):
528
+ """Calculates the NormalizedGamma PSD values."""
529
+ d_ratio = D / D50
530
+ nf = Nw * 6.0 / 3.67**4 * (3.67 + mu) ** (mu + 4) / gamma_f(mu + 4)
531
+ # return nf * d_ratio ** mu * np.exp(-(mu + 3.67) * d_ratio)
532
+ return nf * np.exp(mu * np.log(d_ratio) - (3.67 + mu) * d_ratio)
533
+
534
+ @staticmethod
535
+ def from_parameters(parameters):
536
+ """Initialize NormalizedGammaPSD from a dictionary or xr.Dataset.
537
+
538
+ Args:
539
+ parameters (dict or xr.Dataset): Parameters to initialize the class.
540
+
541
+ Returns
542
+ -------
543
+ NormalizedGammaPSD: An instance of NormalizedGammaPSD initialized with the parameters.
544
+ """
545
+ D50 = parameters["D50"]
546
+ Nw = parameters["Nw"]
547
+ mu = parameters["mu"]
548
+ return NormalizedGammaPSD(D50=D50, Nw=Nw, mu=mu)
549
+
550
+ @staticmethod
551
+ def required_parameters():
552
+ """Return the required parameters of the PSD."""
553
+ return ["Nw", "D50", "mu"]
554
+
555
+ def parameters_summary(self):
556
+ """Return a string with the parameter summary."""
557
+ if self.has_scalar_parameters():
558
+ summary = "".join(
559
+ [
560
+ f"{self.name}\n",
561
+ f"$\\mu = {self.mu:.2f}$\n",
562
+ f"$Nw = {self.Nw:.2f}$\n",
563
+ f"$D50 = {self.D50:.2f}$\n",
564
+ ],
565
+ )
566
+ else:
567
+ summary = "" f"{self.name} with N-d parameters \n"
568
+ return summary
569
+
570
+
571
+ PSD_MODELS_DICT = {
572
+ "LognormalPSD": LognormalPSD,
573
+ "ExponentialPSD": ExponentialPSD,
574
+ "GammaPSD": GammaPSD,
575
+ "NormalizedGammaPSD": NormalizedGammaPSD,
576
+ }
577
+
578
+
579
+ def define_interpolator(bin_edges, bin_values, interp_method):
580
+ """
581
+ Returns an interpolation function that takes one argument D.
582
+
583
+ Parameters
584
+ ----------
585
+ interp_method (str): Interpolation method: 'step_left', 'step_right', 'linear' or 'pchip'.
586
+ bin_edges (array-like): Sorted array of bin edge values.
587
+ bin_values (array-like): Array of bin values corresponding to each bin.
588
+
589
+ Returns
590
+ -------
591
+ callable
592
+ A function f(D) that returns the interpolated values.
593
+ """
594
+ # Ensure bin_edges and bin_values are NumPy arrays
595
+ bin_edges = np.asarray(bin_edges)
596
+ bin_values = np.asarray(bin_values)
597
+ bin_center = bin_edges[:-1] + np.diff(bin_edges) / 2
598
+ # Define a dictionary of lambda functions for each method.
599
+ # - Each lambda accepts only the variable D.
600
+ methods = {
601
+ # 'linear': Linear interpolation between bin values.
602
+ "linear": lambda D: interp1d(
603
+ bin_center,
604
+ bin_values,
605
+ kind="linear",
606
+ bounds_error=False,
607
+ fill_value="extrapolate",
608
+ )(D),
609
+ # 'pchip': Uses the PCHIP interpolator which preserves monotonicity.
610
+ "pchip": lambda D: PchipInterpolator(
611
+ bin_center,
612
+ bin_values,
613
+ extrapolate="extrapolate",
614
+ )(D),
615
+ # 'binary': Uses np.searchsorted for a vectorized direct bin lookup.
616
+ "step_left": lambda D: _stepwise_interpolator(bin_edges, bin_values, D, side="left"),
617
+ "step_right": lambda D: _stepwise_interpolator(bin_edges, bin_values, D, side="right"),
618
+ }
619
+ return methods[interp_method]
620
+
621
+
622
+ def _stepwise_interpolator(bin_edges, bin_values, D, side="left"):
623
+ # Use np.searchsorted binary search to determine the insertion indices.
624
+ # With side='right', it returns the index of the first element greater than D
625
+ # Subtracting by 1 gives the bin to the left of D.
626
+ indices = np.searchsorted(bin_edges, D, side=side) - 1
627
+ indices = np.minimum(indices, len(bin_values) - 1) # enable left inclusion of bin edge max
628
+ # Prepare an array for the results. For D outside the valid range the value is 0.
629
+ result = np.zeros_like(D, dtype=bin_values.dtype)
630
+ # Define valid indices
631
+ valid = (bin_edges[0] < D) & (bin_edges[-1] >= D)
632
+ # For valid entries, assign the corresponding bin value from self.bin_psd.
633
+ result[valid] = bin_values[indices[valid]]
634
+ return result
635
+
636
+
637
+ class BinnedPSD(PSD):
638
+ """Binned Particle Size Distribution (PSD).
639
+
640
+ This class represents a binned particle size distribution (PSD) that computes PSD values
641
+ based on provided bin edges and corresponding PSD values. The PSD is evaluated via interpolation
642
+ using one of several available methods.
643
+
644
+ Parameters
645
+ ----------
646
+ bin_edges : array_like
647
+ A sequence of n+1 bin edge values defining the bins. The edges must be monotonically increasing.
648
+ bin_psd : array_like
649
+ A sequence of n PSD values corresponding to the intervals defined by bin_edges.
650
+ interp_method : {'step_left', 'step_right', 'linear', 'pchip'}, optional
651
+ The interpolation method used to compute the PSD values. The default is 'step_left'.
652
+
653
+ For any input diameter (or diameters) D:
654
+ - If D lies outside the range (bin_edges[0], bin_edges[-1]), the PSD value is set to 0.
655
+ - The interpolation function is defined internally based on the chosen method.
656
+ - PSD values are clipped to ensure they are non-negative.
657
+
658
+ Examples
659
+ --------
660
+ >>> import numpy as np
661
+ >>> bin_edges = [0.0, 1.0, 2.0, 3.0, 4.0]
662
+ >>> bin_psd = [10.0, 20.0, 30.0, 0.0]
663
+ >>> D = np.linspace(0, 3.5, 100)
664
+ >>>
665
+ >>> # Using linear interpolation
666
+ >>> psd_linear = BinnedPSD(bin_edges, bin_psd, interp_method="linear")
667
+ >>> psd_values = psd_linear(D)
668
+ >>>
669
+ >>> # Values for D outside (bin_edges[0], bin_edges[-1]) are set to 0
670
+ """
671
+
672
+ def __init__(self, bin_edges, bin_psd, interp_method="step_left"):
673
+ # Check array size
674
+ if len(bin_edges) != (len(bin_psd) + 1):
675
+ raise ValueError("There must be n+1 bin edges for n bins.")
676
+ # Assign psd values and edges
677
+ self.bin_edges = np.asanyarray(bin_edges)
678
+ self.bin_psd = np.asanyarray(bin_psd)
679
+ self.interp_method = interp_method
680
+
681
+ def __call__(self, D):
682
+ """Compute the PSD.
683
+
684
+ Parameters
685
+ ----------
686
+ D : float
687
+ The diameter for which to calculate the PSD.
688
+
689
+ Returns
690
+ -------
691
+ array-like
692
+ The PSD value(s) corresponding to the given diameter(s) D.
693
+ if D values are outside the range of bin edges, 0 values are returned.
694
+
695
+ """
696
+ # Ensure D is numpy array of correct dimension
697
+ D = np.asanyarray(check_diameter_inputs(D))
698
+ # Define interpolator
699
+ interpolator = define_interpolator(
700
+ bin_edges=self.bin_edges,
701
+ bin_values=self.bin_psd,
702
+ interp_method=self.interp_method,
703
+ )
704
+ # Interpolate
705
+ values = interpolator(D)
706
+ # Mask outside bin edges
707
+ values[~(self.bin_edges[0] < D) & (self.bin_edges[-1] >= D)] = 0
708
+ # Clip values above 0
709
+ # - Extrapolation of some interpolator
710
+ values = np.clip(values, a_min=0, a_max=None)
711
+ if D.size == 1:
712
+ return values.item()
713
+ return values
714
+
715
+ def __eq__(self, other):
716
+ """Check Binned PSD equality."""
717
+ if other is None:
718
+ return False
719
+ if not isinstance(other, self.__class__):
720
+ return False
721
+ return (
722
+ len(self.bin_edges) == len(other.bin_edges)
723
+ and (self.bin_edges == other.bin_edges).all()
724
+ and (self.bin_psd == other.bin_psd).all()
725
+ )
726
+
727
+
728
+ ####-----------------------------------------------------------------.
729
+ #### Moments Computations from PSD parameters
730
+
731
+
732
+ def get_exponential_moment(N0, Lambda, moment):
733
+ """Compute exponential distribution moments."""
734
+ return N0 * gamma_f(moment + 1) / Lambda ** (moment + 1)
735
+
736
+
737
+ def get_gamma_moment_v1(N0, mu, Lambda, moment):
738
+ """Compute gamma distribution moments.
739
+
740
+ References
741
+ ----------
742
+ Kozu, T., and K. Nakamura, 1991:
743
+ Rainfall Parameter Estimation from Dual-Radar Measurements
744
+ Combining Reflectivity Profile and Path-integrated Attenuation.
745
+ J. Atmos. Oceanic Technol., 8, 259-270, https://doi.org/10.1175/1520-0426(1991)008<0259:RPEFDR>2.0.CO;2
746
+ """
747
+ # Zhang et al 2001: N0 * gamma_f(mu + moment + 1) * Lambda ** (-(mu + moment + 1))
748
+ return N0 * gamma_f(mu + moment + 1) / Lambda ** (mu + moment + 1)
749
+
750
+
751
+ def get_gamma_moment_v2(Nt, mu, Lambda, moment):
752
+ """Compute gamma distribution moments.
753
+
754
+ References
755
+ ----------
756
+ Kozu, T., and K. Nakamura, 1991:
757
+ Rainfall Parameter Estimation from Dual-Radar Measurements
758
+ Combining Reflectivity Profile and Path-integrated Attenuation.
759
+ J. Atmos. Oceanic Technol., 8, 259-270, https://doi.org/10.1175/1520-0426(1991)008<0259:RPEFDR>2.0.CO;2
760
+ """
761
+ return Nt * gamma_f(mu + moment + 1) / gamma_f(mu + 1) / Lambda**moment
762
+
763
+
764
+ def get_lognormal_moment(Nt, sigma, mu, moment):
765
+ """Compute lognormal distribution moments.
766
+
767
+ References
768
+ ----------
769
+ Kozu, T., and K. Nakamura, 1991:
770
+ Rainfall Parameter Estimation from Dual-Radar Measurements
771
+ Combining Reflectivity Profile and Path-integrated Attenuation.
772
+ J. Atmos. Oceanic Technol., 8, 259-270, https://doi.org/10.1175/1520-0426(1991)008<0259:RPEFDR>2.0.CO;2
773
+ """
774
+ return Nt * np.exp(moment * mu + 1 / 2 * moment * sigma**2)