cloudnetpy 1.95.7__tar.gz → 1.96.1__tar.gz

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 (149) hide show
  1. {cloudnetpy-1.95.7/cloudnetpy.egg-info → cloudnetpy-1.96.1}/PKG-INFO +2 -2
  2. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/cli.py +3 -1
  3. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/disdronator/lpm.py +13 -2
  4. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/disdronator/parsivel.py +4 -1
  5. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/disdronator/process.py +4 -1
  6. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/disdronator/rd80.py +10 -1
  7. cloudnetpy-1.96.1/cloudnetpy/disdronator/utils.py +69 -0
  8. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/disdrometer.py +3 -3
  9. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/mmcr.py +136 -3
  10. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/output.py +12 -1
  11. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/plotting/plot_meta.py +44 -0
  12. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/plotting/plotting.py +39 -2
  13. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/products/__init__.py +1 -0
  14. cloudnetpy-1.96.1/cloudnetpy/products/cod.py +491 -0
  15. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/products/lwc.py +10 -2
  16. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/version.py +2 -2
  17. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1/cloudnetpy.egg-info}/PKG-INFO +2 -2
  18. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy.egg-info/SOURCES.txt +1 -0
  19. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy.egg-info/requires.txt +1 -1
  20. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/pyproject.toml +1 -1
  21. cloudnetpy-1.95.7/cloudnetpy/disdronator/utils.py +0 -38
  22. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/LICENSE +0 -0
  23. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/MANIFEST.in +0 -0
  24. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/README.md +0 -0
  25. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/__init__.py +0 -0
  26. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/arm.py +0 -0
  27. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/__init__.py +0 -0
  28. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/atmos_utils.py +0 -0
  29. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/attenuation.py +0 -0
  30. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/attenuations/__init__.py +0 -0
  31. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/attenuations/gas_attenuation.py +0 -0
  32. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/attenuations/liquid_attenuation.py +0 -0
  33. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/attenuations/melting_attenuation.py +0 -0
  34. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/attenuations/rain_attenuation.py +0 -0
  35. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/categorize.py +0 -0
  36. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/classify.py +0 -0
  37. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/containers.py +0 -0
  38. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/disdrometer.py +0 -0
  39. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/droplet.py +0 -0
  40. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/falling.py +0 -0
  41. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/freezing.py +0 -0
  42. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/insects.py +0 -0
  43. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/lidar.py +0 -0
  44. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/melting.py +0 -0
  45. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/model.py +0 -0
  46. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/mwr.py +0 -0
  47. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/categorize/radar.py +0 -0
  48. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/cloudnetarray.py +0 -0
  49. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/concat_lib.py +0 -0
  50. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/constants.py +0 -0
  51. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/datasource.py +0 -0
  52. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/disdronator/__init__.py +0 -0
  53. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/exceptions.py +0 -0
  54. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/__init__.py +0 -0
  55. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/arm_ceilo.py +0 -0
  56. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/arm_ld.py +0 -0
  57. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/arm_utils.py +0 -0
  58. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/basta.py +0 -0
  59. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/bowtie.py +0 -0
  60. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/ceilo.py +0 -0
  61. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/ceilometer.py +0 -0
  62. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/cl61d.py +0 -0
  63. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/cloudnet_instrument.py +0 -0
  64. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/copernicus.py +0 -0
  65. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/da10.py +0 -0
  66. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/dealias.py +0 -0
  67. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/fd12p.py +0 -0
  68. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/galileo.py +0 -0
  69. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/hatpro.py +0 -0
  70. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/instruments.py +0 -0
  71. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/kazr.py +0 -0
  72. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/lufft.py +0 -0
  73. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/mira.py +0 -0
  74. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/mrr.py +0 -0
  75. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/mwr3c.py +0 -0
  76. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/mwrlos.py +0 -0
  77. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/nc_lidar.py +0 -0
  78. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/nc_radar.py +0 -0
  79. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/pollyxt.py +0 -0
  80. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/radiometrics.py +0 -0
  81. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/rain_e_h3.py +0 -0
  82. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/rpg.py +0 -0
  83. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/rpg_reader.py +0 -0
  84. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/toa5.py +0 -0
  85. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/vaisala.py +0 -0
  86. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/weather_radar.py +0 -0
  87. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/instruments/weather_station.py +0 -0
  88. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/metadata.py +0 -0
  89. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/__init__.py +0 -0
  90. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/file_handler.py +0 -0
  91. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/metadata.py +0 -0
  92. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/model_metadata.py +0 -0
  93. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/plotting/__init__.py +0 -0
  94. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/plotting/plot_meta.py +0 -0
  95. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/plotting/plot_tools.py +0 -0
  96. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/plotting/plotting.py +0 -0
  97. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/products/__init__.py +0 -0
  98. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/products/advance_methods.py +0 -0
  99. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/products/grid_methods.py +0 -0
  100. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/products/model_products.py +0 -0
  101. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/products/observation_products.py +0 -0
  102. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/products/product_resampling.py +0 -0
  103. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/products/tools.py +0 -0
  104. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/statistics/__init__.py +0 -0
  105. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/statistics/statistical_methods.py +0 -0
  106. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/__init__.py +0 -0
  107. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/e2e/__init__.py +0 -0
  108. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/e2e/conftest.py +0 -0
  109. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/e2e/process_cf/__init__.py +0 -0
  110. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/e2e/process_cf/main.py +0 -0
  111. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/e2e/process_cf/tests.py +0 -0
  112. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/__init__.py +0 -0
  113. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/main.py +0 -0
  114. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/tests.py +0 -0
  115. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/__init__.py +0 -0
  116. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/main.py +0 -0
  117. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/tests.py +0 -0
  118. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/unit/__init__.py +0 -0
  119. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/unit/conftest.py +0 -0
  120. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/unit/test_advance_methods.py +0 -0
  121. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/unit/test_grid_methods.py +0 -0
  122. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/unit/test_model_products.py +0 -0
  123. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/unit/test_observation_products.py +0 -0
  124. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/unit/test_plot_tools.py +0 -0
  125. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/unit/test_plotting.py +0 -0
  126. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/unit/test_statistical_methods.py +0 -0
  127. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/tests/unit/test_tools.py +0 -0
  128. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/model_evaluation/utils.py +0 -0
  129. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/plotting/__init__.py +0 -0
  130. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/products/classification.py +0 -0
  131. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/products/der.py +0 -0
  132. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/products/drizzle.py +0 -0
  133. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/products/drizzle_error.py +0 -0
  134. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/products/drizzle_tools.py +0 -0
  135. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/products/epsilon_lidar.py +0 -0
  136. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/products/epsilon_radar.py +0 -0
  137. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/products/ier.py +0 -0
  138. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/products/iwc.py +0 -0
  139. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/products/iwc_weather_radar.py +0 -0
  140. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/products/mie_lu_tables.nc +0 -0
  141. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/products/mwr_tools.py +0 -0
  142. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/products/product_tools.py +0 -0
  143. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/py.typed +0 -0
  144. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy/utils.py +0 -0
  145. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy.egg-info/dependency_links.txt +0 -0
  146. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy.egg-info/entry_points.txt +0 -0
  147. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/cloudnetpy.egg-info/top_level.txt +0 -0
  148. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/docs/source/conf.py +0 -0
  149. {cloudnetpy-1.95.7 → cloudnetpy-1.96.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudnetpy
3
- Version: 1.95.7
3
+ Version: 1.96.1
4
4
  Summary: Python package for Cloudnet processing
5
5
  Author: Simo Tukiainen
6
6
  License: MIT License
@@ -38,7 +38,7 @@ Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
38
38
  Requires-Python: >=3.10
39
39
  Description-Content-Type: text/markdown
40
40
  License-File: LICENSE
41
- Requires-Dist: atmoslib>=2.0.0
41
+ Requires-Dist: atmoslib>=2.4.2
42
42
  Requires-Dist: ceilopyter>=0.2.0
43
43
  Requires-Dist: cloudnet-api-client
44
44
  Requires-Dist: doppy>=0.5.0
@@ -62,9 +62,11 @@ def run(args: argparse.Namespace, tmpdir: str, client: APIClient) -> None:
62
62
  process_categorize = "categorize" in args.products
63
63
  cat_filepath = None
64
64
  if is_arm and cat_products and not process_categorize:
65
- # ARM categorize files are not in the data portal: use a local one
65
+ # Prefer a local file, then the portal; process only if neither exists
66
66
  if not ARM_PRODUCTS & set(args.products):
67
67
  cat_filepath = _find_existing_categorize(args)
68
+ if cat_filepath is None:
69
+ cat_filepath = _fetch_product(args, "categorize", client)
68
70
  process_categorize = cat_filepath is None
69
71
 
70
72
  # Instrument based products
@@ -1,4 +1,5 @@
1
1
  import datetime
2
+ import logging
2
3
  from collections import defaultdict
3
4
  from os import PathLike
4
5
  from typing import TypeAlias
@@ -109,9 +110,18 @@ def read_lpm(filename: str | PathLike) -> tuple[npt.NDArray, dict[int, npt.NDArr
109
110
 
110
111
 
111
112
  def read_lpm_l1(
112
- time: npt.NDArray, l0: dict[int, npt.NDArray], au: int | None = None
113
+ time: npt.NDArray,
114
+ l0: dict[int, npt.NDArray],
115
+ au: int | None = None,
116
+ altitude: float | None = None,
113
117
  ) -> DisdroL1:
114
- area_nom = 4600 * 1000 / au if au is not None else 4560
118
+ if au is None:
119
+ area_nom = 4560.0
120
+ logging.warning(
121
+ "No AU parameter given, assuming nominal sampling area: %d mm2", area_nom
122
+ )
123
+ else:
124
+ area_nom = 4600.0 * 1000.0 / au
115
125
  area_eff = area_nom * (1 - D / (2 * LASER_WIDTH))
116
126
  data_raw = np.stack([l0[i] for i in range(81, 521)], axis=1).reshape(
117
127
  (len(time), len(D), len(V))
@@ -128,6 +138,7 @@ def read_lpm_l1(
128
138
  area_nom=area_nom,
129
139
  area_eff=area_eff,
130
140
  data_raw=data_raw,
141
+ altitude=altitude,
131
142
  )
132
143
 
133
144
 
@@ -522,7 +522,9 @@ def read_parsivel(
522
522
  return np.array(time), convert_to_numpy(data, {}, INT_KEYS, FLOAT_KEYS)
523
523
 
524
524
 
525
- def read_parsivel_l1(time: npt.NDArray, l0: dict[int, npt.NDArray]) -> DisdroL1:
525
+ def read_parsivel_l1(
526
+ time: npt.NDArray, l0: dict[int, npt.NDArray], altitude: float | None = None
527
+ ) -> DisdroL1:
526
528
  if 93 not in l0:
527
529
  msg = "No raw data"
528
530
  raise ValueError(msg)
@@ -544,6 +546,7 @@ def read_parsivel_l1(time: npt.NDArray, l0: dict[int, npt.NDArray]) -> DisdroL1:
544
546
  area_nom=AREA_NOM,
545
547
  area_eff=AREA_EFF,
546
548
  data_raw=data_raw,
549
+ altitude=altitude,
547
550
  )
548
551
 
549
552
 
@@ -20,6 +20,7 @@ class DisdroL1:
20
20
  area_nom: float
21
21
  area_eff: npt.NDArray | None
22
22
  data_raw: npt.NDArray
23
+ altitude: float | None
23
24
 
24
25
 
25
26
  @dataclass
@@ -43,6 +44,7 @@ class DisdroL2:
43
44
  radar_refl: npt.NDArray
44
45
  energy_flux: npt.NDArray
45
46
  visibility: npt.NDArray
47
+ altitude: float | None
46
48
 
47
49
 
48
50
  def process_l2(l1: DisdroL1) -> DisdroL2:
@@ -112,7 +114,7 @@ def process_l2(l1: DisdroL1) -> DisdroL2:
112
114
  fall_velocity = ma.divide(
113
115
  np.sum(l1.velocity * spec, axis=2), np.sum(spec, axis=2)
114
116
  )
115
- is_rain = make_rain_mask(l1.diameter, l1.velocity)
117
+ is_rain = make_rain_mask(l1.diameter, l1.velocity, altitude=l1.altitude)
116
118
  spec_rain = np.copy(spec)
117
119
  spec_rain[:, ~is_rain] = 0
118
120
  rain_amount = (
@@ -190,4 +192,5 @@ def process_l2(l1: DisdroL1) -> DisdroL2:
190
192
  radar_refl=radar_refl_db,
191
193
  energy_flux=energy_flux,
192
194
  visibility=visibility,
195
+ altitude=l1.altitude,
193
196
  )
@@ -1,4 +1,5 @@
1
1
  import datetime
2
+ import logging
2
3
  from os import PathLike
3
4
 
4
5
  import numpy as np
@@ -35,7 +36,14 @@ def read_rd80(filename: str | PathLike) -> tuple[npt.NDArray, dict[str, npt.NDAr
35
36
  return np.array(time), convert_to_numpy(data)
36
37
 
37
38
 
38
- def read_rd80_l1(time: npt.NDArray, l0: dict[str, npt.NDArray]) -> DisdroL1:
39
+ def read_rd80_l1(
40
+ time: npt.NDArray, l0: dict[str, npt.NDArray], altitude: float | None = None
41
+ ) -> DisdroL1:
42
+ if altitude is not None:
43
+ logging.warning(
44
+ "Altitude given but the correction for non-sea-level "
45
+ "conditions is not implemented for RD-80."
46
+ )
39
47
  return DisdroL1(
40
48
  diameter=D,
41
49
  diameter_spread=D_SPREAD,
@@ -48,6 +56,7 @@ def read_rd80_l1(time: npt.NDArray, l0: dict[str, npt.NDArray]) -> DisdroL1:
48
56
  area_nom=AREA_NOM,
49
57
  area_eff=None,
50
58
  data_raw=l0["n"],
59
+ altitude=altitude,
51
60
  )
52
61
 
53
62
 
@@ -0,0 +1,69 @@
1
+ import logging
2
+ from collections.abc import Container
3
+
4
+ import numpy as np
5
+ import numpy.typing as npt
6
+ from atmoslib import isa_air_density
7
+ from atmoslib.constants import RHO_STD
8
+ from numpy import ma
9
+
10
+
11
+ def convert_to_numpy(
12
+ data: dict,
13
+ fill_values: dict | None = None,
14
+ int_keys: Container | None = None,
15
+ float_keys: Container | None = None,
16
+ ) -> dict:
17
+ output = {}
18
+ for key, value in data.items():
19
+ arr = np.array(value)
20
+ if fill_values is not None and key in fill_values:
21
+ arr = ma.masked_where(arr == fill_values[key], arr)
22
+ if int_keys is not None and key in int_keys:
23
+ new_arr = arr.astype(np.int32)
24
+ if not np.all(arr == new_arr):
25
+ msg = "Cannot convert non-integer float to integer"
26
+ raise ValueError(msg)
27
+ arr = new_arr
28
+ elif float_keys is not None and key in float_keys:
29
+ arr = arr.astype(np.float32)
30
+ output[key] = arr
31
+ return output
32
+
33
+
34
+ def make_rain_mask(
35
+ diameter: npt.NDArray[np.floating],
36
+ velocity: npt.NDArray[np.floating],
37
+ threshold: float = 0.5,
38
+ altitude: float | None = None,
39
+ ) -> npt.NDArray[np.bool]:
40
+ """Make mask for rain bins based on theoretical diameter-velocity relation.
41
+
42
+ Args:
43
+ diameter: Diameter (mm)
44
+ velocity: Velocity (m s-1)
45
+ threshold: Maximum error to theoretical relation (1)
46
+ altitude: Site altitude for optional altitude correction (m)
47
+
48
+ References:
49
+ Atlas, D., Srivastava, R. C., & Sekhon, R. S. (1973). Doppler Radar
50
+ Characteristics of Precipitation at Vertical Incidence. Reviews of
51
+ Geophysics and Space Physics, 11(1), 1-35.
52
+ https://doi.org/10.1029/RG011i001p00001
53
+
54
+ Beard, K. V. (1985). Simple Altitude Adjustments to Raindrop Velocities
55
+ for Doppler Radar Analysis. Journal of Atmospheric and Oceanic
56
+ Technology, 2(4), 468-471.
57
+ https://doi.org/10.1175/1520-0426(1985)002%3C0468:SAATRV%3E2.0.CO;2
58
+ """
59
+ v = 9.65 - 10.3 * np.exp(-0.6 * diameter) # Eq. 24, Atlas et al. (1973)
60
+ if altitude is None:
61
+ logging.warning(
62
+ "No altitude given, no correction applied for non-sea-level conditions."
63
+ )
64
+ else:
65
+ rho = isa_air_density(altitude)
66
+ m = 0.375 + 0.025 * diameter # Eq. 3, Beard (1985)
67
+ v = (RHO_STD / rho) ** m * v # Eq. 1, Beard (1985)
68
+ e = np.abs((velocity - v[:, np.newaxis]) / v[:, np.newaxis])
69
+ return e < threshold
@@ -71,7 +71,7 @@ def parsivel2nc(
71
71
  field_separator=field_separator,
72
72
  decimal_separator=decimal_separator,
73
73
  ),
74
- read_parsivel_l1,
74
+ functools.partial(read_parsivel_l1, altitude=site_meta.get("altitude")),
75
75
  ATTRIBUTES,
76
76
  disdrometer_file,
77
77
  output_file,
@@ -117,7 +117,7 @@ def thies2nc(
117
117
  return _process_disdrometer(
118
118
  Thies,
119
119
  read_lpm,
120
- functools.partial(read_lpm_l1, au=au),
120
+ functools.partial(read_lpm_l1, au=au, altitude=site_meta.get("altitude")),
121
121
  ATTRIBUTES,
122
122
  disdrometer_file,
123
123
  output_file,
@@ -160,7 +160,7 @@ def rd802nc(
160
160
  return _process_disdrometer(
161
161
  Rd80,
162
162
  read_rd80,
163
- read_rd80_l1,
163
+ functools.partial(read_rd80_l1, altitude=site_meta.get("altitude")),
164
164
  RD80_ATTRIBUTES,
165
165
  input_file,
166
166
  output_file,
@@ -7,6 +7,7 @@ from uuid import UUID
7
7
 
8
8
  import netCDF4
9
9
  import numpy as np
10
+ from numpy import ma
10
11
 
11
12
  from cloudnetpy import output, utils
12
13
  from cloudnetpy.exceptions import ValidTimeStampError
@@ -19,7 +20,19 @@ from cloudnetpy.instruments.dealias import (
19
20
  )
20
21
  from cloudnetpy.instruments.instruments import MMCR
21
22
  from cloudnetpy.instruments.nc_radar import NcRadar
22
- from cloudnetpy.metadata import COMMON_ATTRIBUTES
23
+ from cloudnetpy.metadata import COMMON_ATTRIBUTES, MetaData
24
+
25
+ CDR_SNR_LIMIT = -5.0 # dB, co-polar SNR required for a valid CDR pixel
26
+ # Conservative fallback: the highest floor observed at SGP (2005). The
27
+ # floor drifted from -11 dB in early 2005 to -16...-19 dB from late 2005 on.
28
+ # Over-subtracting only weakens the depolarization signal, whereas
29
+ # under-subtracting turns rain and cloud into false insects.
30
+ CDR_FLOOR_DEFAULT = -11.0 # dB, cross-talk floor if it cannot be estimated
31
+ CDR_FLOOR_MARGIN = 1.5 # dB, added to the floor before subtraction
32
+ CDR_FLOOR_RANGE = 4000 # m, pixels above this are used to estimate the floor
33
+ CDR_FLOOR_MIN_PIXELS = 200
34
+ CDR_MAX_TIME_DIFF = 15 # s, max distance to the nearest dual-pol profile
35
+ LDR_MIN = -35.0 # dB
23
36
 
24
37
 
25
38
  def mmcr2nc(
@@ -36,8 +49,10 @@ def mmcr2nc(
36
49
  resolutions and sensitivities. Only profiles from a single mode are used.
37
50
  Doppler velocities are folded at the Nyquist velocity of the mode, which
38
51
  is only about 5 m/s in the default general (GE) mode, and are dealiased
39
- using velocity continuity. Depolarization ratio is not available in the
40
- GE mode.
52
+ using velocity continuity. Circular depolarization ratio is measured
53
+ only in the dual-polarization mode (about one profile per 25 s). It is
54
+ mapped to the selected mode by nearest time, corrected for the antenna
55
+ cross-talk floor and stored as `ldr`.
41
56
 
42
57
  Args:
43
58
  raw_file: Daily ARM `mmcrmom` netCDF file, e.g.
@@ -81,6 +96,7 @@ def mmcr2nc(
81
96
  mmcr.init_data(keymap)
82
97
  mmcr.init_mode(mode)
83
98
  mmcr.screen_mode()
99
+ mmcr.add_depolarization()
84
100
  if date is not None:
85
101
  mmcr.check_date(date)
86
102
  mmcr.sort_timestamps()
@@ -95,6 +111,8 @@ def mmcr2nc(
95
111
  mmcr.add_height()
96
112
  mmcr.test_if_all_masked()
97
113
  attributes = output.add_time_attribute(ATTRIBUTES, mmcr.date)
114
+ if mmcr.cdr_floor is not None:
115
+ attributes["ldr"] = _ldr_attributes(mmcr.cdr_floor)
98
116
  output.update_attributes(mmcr.data, attributes)
99
117
  output.save_level1b(mmcr, output_file, uuid)
100
118
  return uuid
@@ -114,6 +132,7 @@ class Mmcr(NcRadar):
114
132
  self.instrument = MMCR
115
133
  self.date = utils.get_epoch(self.dataset["time"].units).date()
116
134
  self.mode_index: int = 0
135
+ self.cdr_floor: float | None = None
117
136
  self._add_geolocation_from_file()
118
137
 
119
138
  def init_mode(self, mode: str) -> None:
@@ -164,6 +183,87 @@ class Mmcr(NcRadar):
164
183
  # MMCR is a fixed vertically pointing radar
165
184
  self.append_data(0.0, "zenith_angle")
166
185
 
186
+ def add_depolarization(self) -> None:
187
+ """Adds cross-talk corrected circular depolarization ratio as ldr.
188
+
189
+ CDR is available only in the dual-polarization mode. Each profile of
190
+ the selected mode gets the CDR of the nearest dual-pol profile in
191
+ time. The antenna cross-talk floor, estimated as the median CDR of
192
+ high-altitude pixels, is subtracted in linear units.
193
+ """
194
+ if "CircularDepolarizationRatio" not in self.dataset.variables:
195
+ return
196
+ try:
197
+ ind_co, ind_cross = self._find_dualpol_modes()
198
+ except ValueError:
199
+ logging.info("No dual-polarization mode found, ldr not available")
200
+ return
201
+ mode_num = np.array(self.dataset["ModeNum"][:])
202
+ time_raw = np.array(self.dataset["time"][:])
203
+ co_ind = np.where(mode_num == ind_co)[0]
204
+ cross_ind = np.where(mode_num == ind_cross)[0]
205
+ if len(co_ind) == 0 or len(cross_ind) == 0:
206
+ logging.info("No dual-polarization profiles found, ldr not available")
207
+ return
208
+ # Pair each cross-pol profile with the co-pol profile of the same pulse
209
+ pair_ind = co_ind[_nearest_index(time_raw[co_ind], time_raw[cross_ind])]
210
+ n_heights = int(self.dataset["NumHeights"][ind_cross])
211
+ cdr = ma.masked_invalid(
212
+ self.dataset["CircularDepolarizationRatio"][cross_ind, :n_heights]
213
+ )
214
+ snr = ma.masked_invalid(
215
+ self.dataset["SignalToNoiseRatio"][pair_ind, :n_heights]
216
+ )
217
+ cdr[ma.filled(snr < CDR_SNR_LIMIT, fill_value=True)] = ma.masked
218
+ altitude = float(self.dataset["alt"][:])
219
+ range_cross = (
220
+ np.array(self.dataset["heights"][ind_cross, :n_heights]) - altitude
221
+ )
222
+ self.cdr_floor = self._estimate_cdr_floor(cdr, range_cross)
223
+ ldr = _remove_cdr_floor(cdr, self.cdr_floor)
224
+ # Map to the profiles and range gates of the selected mode
225
+ time_sel = np.array(self.data["time"][:]) * 3600
226
+ time_ind = _nearest_index(time_raw[cross_ind], time_sel)
227
+ range_ind = _nearest_index(range_cross, self.data["range"][:])
228
+ ldr = ldr[time_ind][:, range_ind]
229
+ too_far = np.abs(time_raw[cross_ind][time_ind] - time_sel) > CDR_MAX_TIME_DIFF
230
+ ldr[too_far, :] = ma.masked
231
+ self.append_data(ldr, "ldr")
232
+
233
+ def _estimate_cdr_floor(
234
+ self, cdr: ma.MaskedArray, range_cross: np.ndarray
235
+ ) -> float:
236
+ is_high = ~ma.getmaskarray(cdr) & (range_cross[np.newaxis, :] > CDR_FLOOR_RANGE)
237
+ if np.count_nonzero(is_high) < CDR_FLOOR_MIN_PIXELS:
238
+ logging.warning(
239
+ "Not enough high-altitude pixels to estimate CDR floor, "
240
+ "using conservative default %s dB",
241
+ CDR_FLOOR_DEFAULT,
242
+ )
243
+ return CDR_FLOOR_DEFAULT
244
+ floor = float(np.median(cdr[is_high].compressed()))
245
+ logging.info("Estimated CDR cross-talk floor: %.1f dB", floor)
246
+ return floor
247
+
248
+ def _find_dualpol_modes(self) -> tuple[int, int]:
249
+ """Returns indices of the co- and cross-polar dual-pol receiver modes.
250
+
251
+ Mode names vary between files (e.g. "PO_Receiver0" in 2005,
252
+ "DualPol_Receiver0" in 2008), so the receiver metadata is used.
253
+ """
254
+ for key in ("NumReceivers", "ReceiverNumber"):
255
+ if key not in self.dataset.variables:
256
+ msg = f"Variable {key} not found"
257
+ raise ValueError(msg)
258
+ n_receivers = ma.filled(self.dataset["NumReceivers"][:], 0)
259
+ receiver = ma.filled(self.dataset["ReceiverNumber"][:], 0)
260
+ co = np.where((n_receivers == 2) & (receiver == 1))[0]
261
+ cross = np.where((n_receivers == 2) & (receiver == 2))[0]
262
+ if len(co) != 1 or len(cross) != 1:
263
+ msg = "Dual-polarization modes not found"
264
+ raise ValueError(msg)
265
+ return int(co[0]), int(cross[0])
266
+
167
267
  def _find_mode_index(self, mode: str) -> int:
168
268
  var = self.dataset["ModeDescription"]
169
269
  var.set_auto_mask(False)
@@ -180,6 +280,39 @@ class Mmcr(NcRadar):
180
280
  self.site_meta = read_geolocation(self.dataset, self.site_meta)
181
281
 
182
282
 
283
+ def _nearest_index(reference: np.ndarray, query: np.ndarray) -> np.ndarray:
284
+ """Returns index of the nearest `reference` value for each `query` value."""
285
+ if len(reference) < 2:
286
+ return np.zeros(len(query), dtype=int)
287
+ order = np.argsort(reference)
288
+ sorted_ref = reference[order]
289
+ pos = np.searchsorted(sorted_ref, query)
290
+ pos = np.clip(pos, 1, len(sorted_ref) - 1)
291
+ use_left = np.abs(query - sorted_ref[pos - 1]) <= np.abs(sorted_ref[pos] - query)
292
+ return order[np.where(use_left, pos - 1, pos)]
293
+
294
+
295
+ def _remove_cdr_floor(cdr: ma.MaskedArray, floor: float) -> ma.MaskedArray:
296
+ """Subtracts the cross-talk floor in linear units, clipping at LDR_MIN."""
297
+ linear = 10 ** (cdr / 10) - 10 ** ((floor + CDR_FLOOR_MARGIN) / 10)
298
+ ldr = ma.masked_where(linear <= 0, linear)
299
+ ldr = 10 * ma.log10(ldr)
300
+ ldr = ldr.filled(LDR_MIN)
301
+ ldr = np.maximum(ldr, LDR_MIN)
302
+ return ma.masked_where(ma.getmaskarray(cdr), ldr)
303
+
304
+
305
+ def _ldr_attributes(floor: float) -> MetaData:
306
+ return COMMON_ATTRIBUTES["ldr"]._replace(
307
+ comment=(
308
+ "Circular depolarization ratio from the dual-polarization mode, "
309
+ "mapped to the selected mode by nearest time. The antenna cross-talk "
310
+ f"floor of {floor:.1f} dB has been subtracted in linear units, "
311
+ f"and values are clipped at {LDR_MIN} dB."
312
+ ),
313
+ )
314
+
315
+
183
316
  ATTRIBUTES = {
184
317
  "correction_bits": CORRECTION_BITS_ATTRIBUTES,
185
318
  "v": DEALIASED_V_ATTRIBUTES,
@@ -181,7 +181,7 @@ def get_references(identifier: str | None = None, extra: list | None = None) ->
181
181
  match identifier:
182
182
  case "der":
183
183
  references += (
184
- ", https://doi.org/10.1175/1520-0426(2002)019<0835:TROSCD>2.0.CO;2"
184
+ ", https://doi.org/10.1175/1520-0426(2002)019%3C0835:TROSCD%3E2.0.CO;2"
185
185
  )
186
186
  case "ier":
187
187
  references += (
@@ -195,6 +195,14 @@ def get_references(identifier: str | None = None, extra: list | None = None) ->
195
195
  references += ", https://doi.org/10.1175/JAM2340.1"
196
196
  case "drizzle":
197
197
  references += ", https://doi.org/10.1175/JAM-2181.1"
198
+ case "cod":
199
+ references += (
200
+ ", https://doi.org/10.1175/BAMS-88-6-883"
201
+ ", https://doi.org/10.1175/1520-0426(2002)019%3C0835:TROSCD%3E2.0.CO;2"
202
+ ", https://doi.org/10.1175/JAM2340.1"
203
+ ", https://doi.org/10.1175/JAM2543.1"
204
+ ", https://doi.org/10.5194/amt-13-5335-2020"
205
+ )
198
206
  case "epsilon-radar":
199
207
  references += (
200
208
  ", https://doi.org/10.5194/amt-13-5335-2020"
@@ -472,6 +480,7 @@ def _get_identifier(short_id: str) -> str:
472
480
  "classification",
473
481
  "der",
474
482
  "ier",
483
+ "cod",
475
484
  "classification-voodoo",
476
485
  "epsilon-radar",
477
486
  )
@@ -486,6 +495,8 @@ def _get_identifier(short_id: str) -> str:
486
495
  return "ice effective radius"
487
496
  if short_id == "der":
488
497
  return "droplet effective radius"
498
+ if short_id == "cod":
499
+ return "cloud optical depth"
489
500
  if short_id == "epsilon-radar":
490
501
  return "dissipation rate of turbulent kinetic energy"
491
502
  return short_id
@@ -170,6 +170,15 @@ _CLABEL = {
170
170
  ("Unquantifiable", _COLORS["seaweed_roll"]),
171
171
  ("Undetected", _COLORS["skyblue"]),
172
172
  ),
173
+ "extinction_retrieval_status": (
174
+ ("_No cloud", _COLORS["white"]),
175
+ ("Liquid", _COLORS["lightblue"]),
176
+ ("Liquid, assumed radius", _COLORS["yellow"]),
177
+ ("Ice", _COLORS["lightsteel"]),
178
+ ("Ice, corrected atten.", _COLORS["skyblue"]),
179
+ ("Liquid & ice", _COLORS["darkpurple"]),
180
+ ("No retrieval", _COLORS["seaweed_roll"]),
181
+ ),
173
182
  "dominant_hydrometeor_type": (
174
183
  ("_Clear sky", _COLORS["white"]),
175
184
  ("Ice", _COLORS["lightgray"]),
@@ -684,5 +693,40 @@ ATTRIBUTES = {
684
693
  "dominant_hydrometeor_type": PlotMeta(
685
694
  clabel=_CLABEL["dominant_hydrometeor_type"],
686
695
  ),
696
+ "extinction_liquid": PlotMeta(
697
+ cmap="Blues",
698
+ plot_range=(1e-4, 1e-1),
699
+ log_scale=True,
700
+ ),
701
+ "extinction_ice": PlotMeta(
702
+ plot_range=(1e-5, 1e-2),
703
+ log_scale=True,
704
+ ),
705
+ "extinction_retrieval_status": PlotMeta(
706
+ clabel=_CLABEL["extinction_retrieval_status"],
707
+ ),
708
+ "extinction_liquid_error": PlotMeta(
709
+ cmap="RdYlGn_r",
710
+ plot_range=(0, 5),
711
+ ),
712
+ "extinction_ice_error": PlotMeta(
713
+ cmap="RdYlGn_r",
714
+ plot_range=(0, 5),
715
+ ),
716
+ "optical_depth_error": PlotMeta(
717
+ zero_line=True,
718
+ ),
719
+ "optical_depth": PlotMeta(
720
+ plot_range=(0.02, 1000),
721
+ log_scale=True,
722
+ ),
723
+ "optical_depth_liquid": PlotMeta(
724
+ plot_range=(0.02, 1000),
725
+ log_scale=True,
726
+ ),
727
+ "optical_depth_ice": PlotMeta(
728
+ plot_range=(0.02, 1000),
729
+ log_scale=True,
730
+ ),
687
731
  },
688
732
  }
@@ -904,6 +904,9 @@ class Plot1D(Plot):
904
904
  units = self._convert_units()
905
905
  if self._plot_meta.mask_zeros:
906
906
  self._mask_zeros()
907
+ is_zero = ~ma.getmaskarray(self._data) & (ma.filled(self._data, 1) == 0)
908
+ if self._is_log:
909
+ self._mask_non_positive()
907
910
  self._mark_gaps(figure_data)
908
911
  self._ax.plot(
909
912
  figure_data.time_including_gaps,
@@ -914,14 +917,34 @@ class Plot1D(Plot):
914
917
  )
915
918
  if self._plot_meta.moving_average:
916
919
  self._plot_moving_average(figure_data, hacky_freq_ind)
917
- if self._plot_meta.zero_line:
920
+ if self._plot_meta.zero_line and not self._is_log:
918
921
  self._ax.axhline(0, color="black", alpha=0.5, label="_nolegend_")
919
922
  self._fill_between_data_gaps(figure_data)
920
- self.sub_plot.set_yax(ylabel=units, y_limits=self._get_y_limits())
923
+ if self._is_log:
924
+ self._ax.set_yscale("log")
925
+ y_limits = self._get_y_limits()
926
+ self.sub_plot.set_yax(ylabel=units, y_limits=y_limits)
927
+ if self._is_log and np.any(is_zero):
928
+ self._plot_zeros(figure_data.time[is_zero], y_limits[0])
921
929
  pos = self._ax.get_position()
922
930
  self._ax.set_position((pos.x0, pos.y0, pos.width * 0.965, pos.height))
923
931
  self._plot_flags(figure_data)
924
932
 
933
+ def _plot_zeros(self, time: ndarray, y_min: float) -> None:
934
+ """Marks zero values along the bottom of a logarithmic axis."""
935
+ is_cloud_variable = self.sub_plot.variable.name.startswith("optical_depth")
936
+ self._ax.plot(
937
+ time,
938
+ np.full(len(time), y_min * 1.3),
939
+ color="lightgrey",
940
+ marker=".",
941
+ lw=0,
942
+ markersize=3,
943
+ label="Clear sky" if is_cloud_variable else "Zero",
944
+ zorder=_get_zorder("data"),
945
+ )
946
+ self._ax.legend(markerscale=3, numpoints=1, frameon=False)
947
+
925
948
  def _plot_flags(self, figure_data: FigureData) -> None:
926
949
  if figure_data.is_mwrpy_product():
927
950
  flags = self._read_flagged_data(figure_data)
@@ -1021,9 +1044,18 @@ class Plot1D(Plot):
1021
1044
  frameon=False,
1022
1045
  )
1023
1046
 
1047
+ def _mask_non_positive(self) -> None:
1048
+ self._data = ma.masked_less_equal(self._data, 0)
1049
+ self._data_orig = ma.masked_less_equal(self._data_orig, 0)
1050
+
1024
1051
  def _get_y_limits(self) -> tuple[float, float]:
1025
1052
  percent_gap = 0.05
1026
1053
  fallback = (-percent_gap, percent_gap)
1054
+ if self._is_log:
1055
+ if self._plot_meta.plot_range is not None:
1056
+ return self._plot_meta.plot_range
1057
+ valid = self._data[~ma.getmaskarray(self._data)]
1058
+ return (valid.min() / 2, valid.max() * 2) if valid.size else (0.1, 10)
1027
1059
  if ma.all(self._data.mask):
1028
1060
  return fallback
1029
1061
  min_data = self._data.min()
@@ -1066,6 +1098,9 @@ class Plot1D(Plot):
1066
1098
  ) -> None:
1067
1099
  time = figure_data.time.copy()
1068
1100
  data = self._data_orig.copy()
1101
+ if self._is_log:
1102
+ # Average in log space so large values do not dominate
1103
+ data = ma.log10(ma.masked_less_equal(data, 0))
1069
1104
 
1070
1105
  if figure_data.is_mwrpy_product() or self.sub_plot.variable.name in (
1071
1106
  "tb",
@@ -1097,6 +1132,8 @@ class Plot1D(Plot):
1097
1132
  )
1098
1133
  else:
1099
1134
  sma = self._calculate_moving_average(data1, time1, window=5)
1135
+ if self._is_log:
1136
+ sma = 10**sma
1100
1137
  gap_time = _get_max_gap_in_minutes(figure_data)
1101
1138
  gaps = self._find_time_gap_indices(time1, max_gap_min=gap_time) + 1
1102
1139
 
@@ -1,4 +1,5 @@
1
1
  from .classification import generate_classification
2
+ from .cod import generate_cod
2
3
  from .der import generate_der
3
4
  from .drizzle import generate_drizzle
4
5
  from .epsilon_lidar import generate_epsilon_from_lidar