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