cloudnetpy 1.97.1__tar.gz → 1.97.3__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 (148) hide show
  1. {cloudnetpy-1.97.1/cloudnetpy.egg-info → cloudnetpy-1.97.3}/PKG-INFO +3 -3
  2. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/README.md +1 -1
  3. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/disdronator/parsivel.py +39 -0
  4. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/instruments/ceilo.py +20 -3
  5. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/instruments/instruments.py +8 -0
  6. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/instruments/kazr.py +26 -3
  7. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/instruments/lufft.py +1 -0
  8. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/instruments/vaisala.py +25 -2
  9. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/metadata.py +3 -1
  10. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/products/advance_methods.py +35 -23
  11. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/products/grid_methods.py +23 -15
  12. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/products/observation_products.py +8 -4
  13. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/products/tools.py +11 -0
  14. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/tests/unit/test_advance_methods.py +26 -6
  15. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/tests/unit/test_grid_methods.py +9 -0
  16. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/tests/unit/test_observation_products.py +2 -1
  17. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/tests/unit/test_tools.py +14 -0
  18. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/version.py +1 -1
  19. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3/cloudnetpy.egg-info}/PKG-INFO +3 -3
  20. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy.egg-info/requires.txt +1 -1
  21. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/pyproject.toml +1 -1
  22. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/LICENSE +0 -0
  23. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/MANIFEST.in +0 -0
  24. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/__init__.py +0 -0
  25. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/arm.py +0 -0
  26. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/categorize/__init__.py +0 -0
  27. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/categorize/atmos_utils.py +0 -0
  28. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/categorize/attenuation.py +0 -0
  29. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/categorize/attenuations/__init__.py +0 -0
  30. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/categorize/attenuations/gas_attenuation.py +0 -0
  31. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/categorize/attenuations/liquid_attenuation.py +0 -0
  32. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/categorize/attenuations/melting_attenuation.py +0 -0
  33. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/categorize/attenuations/rain_attenuation.py +0 -0
  34. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/categorize/categorize.py +0 -0
  35. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/categorize/classify.py +0 -0
  36. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/categorize/containers.py +0 -0
  37. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/categorize/disdrometer.py +0 -0
  38. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/categorize/droplet.py +0 -0
  39. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/categorize/falling.py +0 -0
  40. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/categorize/freezing.py +0 -0
  41. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/categorize/insects.py +0 -0
  42. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/categorize/lidar.py +0 -0
  43. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/categorize/melting.py +0 -0
  44. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/categorize/model.py +0 -0
  45. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/categorize/mwr.py +0 -0
  46. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/categorize/radar.py +0 -0
  47. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/cli.py +0 -0
  48. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/cloudnetarray.py +0 -0
  49. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/concat_lib.py +0 -0
  50. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/constants.py +0 -0
  51. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/datasource.py +0 -0
  52. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/disdronator/__init__.py +0 -0
  53. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/disdronator/lpm.py +0 -0
  54. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/disdronator/process.py +0 -0
  55. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/disdronator/rd80.py +0 -0
  56. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/disdronator/utils.py +0 -0
  57. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/exceptions.py +0 -0
  58. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/instruments/__init__.py +0 -0
  59. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/instruments/arm_ceilo.py +0 -0
  60. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/instruments/arm_ld.py +0 -0
  61. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/instruments/arm_utils.py +0 -0
  62. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/instruments/basta.py +0 -0
  63. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/instruments/bowtie.py +0 -0
  64. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/instruments/ceilometer.py +0 -0
  65. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/instruments/cl61d.py +0 -0
  66. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/instruments/cloudnet_instrument.py +0 -0
  67. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/instruments/copernicus.py +0 -0
  68. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/instruments/da10.py +0 -0
  69. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/instruments/dealias.py +0 -0
  70. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/instruments/disdrometer.py +0 -0
  71. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/instruments/fd12p.py +0 -0
  72. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/instruments/galileo.py +0 -0
  73. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/instruments/hatpro.py +0 -0
  74. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/instruments/mira.py +0 -0
  75. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/instruments/mmcr.py +0 -0
  76. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/instruments/mrr.py +0 -0
  77. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/instruments/mwr3c.py +0 -0
  78. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/instruments/mwrlos.py +0 -0
  79. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/instruments/nc_lidar.py +0 -0
  80. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/instruments/nc_radar.py +0 -0
  81. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/instruments/pollyxt.py +0 -0
  82. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/instruments/radiometrics.py +0 -0
  83. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/instruments/rain_e_h3.py +0 -0
  84. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/instruments/rpg.py +0 -0
  85. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/instruments/rpg_reader.py +0 -0
  86. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/instruments/toa5.py +0 -0
  87. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/instruments/weather_radar.py +0 -0
  88. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/instruments/weather_station.py +0 -0
  89. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/metadata.py +0 -0
  90. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/__init__.py +0 -0
  91. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/file_handler.py +0 -0
  92. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/model_metadata.py +0 -0
  93. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/plotting/__init__.py +0 -0
  94. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/plotting/plot_meta.py +0 -0
  95. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/plotting/plot_tools.py +0 -0
  96. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/plotting/plotting.py +0 -0
  97. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/products/__init__.py +0 -0
  98. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/products/model_products.py +0 -0
  99. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/products/product_resampling.py +0 -0
  100. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/statistics/__init__.py +0 -0
  101. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/statistics/statistical_methods.py +0 -0
  102. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/tests/__init__.py +0 -0
  103. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/tests/e2e/__init__.py +0 -0
  104. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/tests/e2e/conftest.py +0 -0
  105. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/tests/e2e/process_cf/__init__.py +0 -0
  106. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/tests/e2e/process_cf/main.py +0 -0
  107. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/tests/e2e/process_cf/tests.py +0 -0
  108. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/__init__.py +0 -0
  109. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/main.py +0 -0
  110. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/tests/e2e/process_iwc/tests.py +0 -0
  111. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/__init__.py +0 -0
  112. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/main.py +0 -0
  113. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/tests/e2e/process_lwc/tests.py +0 -0
  114. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/tests/unit/__init__.py +0 -0
  115. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/tests/unit/conftest.py +0 -0
  116. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/tests/unit/test_model_products.py +0 -0
  117. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/tests/unit/test_plot_tools.py +0 -0
  118. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/tests/unit/test_plotting.py +0 -0
  119. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/tests/unit/test_statistical_methods.py +0 -0
  120. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/model_evaluation/utils.py +0 -0
  121. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/output.py +0 -0
  122. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/plotting/__init__.py +0 -0
  123. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/plotting/plot_meta.py +0 -0
  124. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/plotting/plotting.py +0 -0
  125. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/products/__init__.py +0 -0
  126. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/products/classification.py +0 -0
  127. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/products/cod.py +0 -0
  128. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/products/der.py +0 -0
  129. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/products/drizzle.py +0 -0
  130. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/products/drizzle_error.py +0 -0
  131. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/products/drizzle_tools.py +0 -0
  132. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/products/epsilon_lidar.py +0 -0
  133. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/products/epsilon_radar.py +0 -0
  134. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/products/ier.py +0 -0
  135. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/products/iwc.py +0 -0
  136. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/products/iwc_weather_radar.py +0 -0
  137. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/products/lwc.py +0 -0
  138. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/products/mie_lu_tables.nc +0 -0
  139. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/products/mwr_tools.py +0 -0
  140. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/products/product_tools.py +0 -0
  141. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/py.typed +0 -0
  142. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy/utils.py +0 -0
  143. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy.egg-info/SOURCES.txt +0 -0
  144. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy.egg-info/dependency_links.txt +0 -0
  145. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy.egg-info/entry_points.txt +0 -0
  146. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/cloudnetpy.egg-info/top_level.txt +0 -0
  147. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/docs/source/conf.py +0 -0
  148. {cloudnetpy-1.97.1 → cloudnetpy-1.97.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudnetpy
3
- Version: 1.97.1
3
+ Version: 1.97.3
4
4
  Summary: Python package for Cloudnet processing
5
5
  Author: Simo Tukiainen
6
6
  License: MIT License
@@ -39,7 +39,7 @@ Requires-Python: >=3.11
39
39
  Description-Content-Type: text/markdown
40
40
  License-File: LICENSE
41
41
  Requires-Dist: atmoslib>=2.4.2
42
- Requires-Dist: ceilopyter>=0.2.2
42
+ Requires-Dist: ceilopyter>=0.2.3
43
43
  Requires-Dist: cloudnet-api-client
44
44
  Requires-Dist: doppy>=0.5.0
45
45
  Requires-Dist: matplotlib
@@ -66,7 +66,7 @@ Dynamic: license-file
66
66
 
67
67
  [![CloudnetPy CI](https://github.com/actris-cloudnet/cloudnetpy/actions/workflows/test.yml/badge.svg)](https://github.com/actris-cloudnet/cloudnetpy/actions/workflows/test.yml)
68
68
  [![PyPI version](https://img.shields.io/pypi/v/cloudnetpy.svg)](https://pypi.org/project/cloudnetpy/)
69
- [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.3666030.svg)](https://doi.org/10.5281/zenodo.3666030)
69
+ [![DOI](https://img.shields.io/badge/DOI-10.5281%2Fzenodo.3666030-blue)](https://doi.org/10.5281/zenodo.3666030)
70
70
  [![status](https://joss.theoj.org/papers/959971f196f617dddc0e7d8333ff22b7/status.svg)](https://joss.theoj.org/papers/959971f196f617dddc0e7d8333ff22b7)
71
71
 
72
72
  CloudnetPy is Python software designed for producing vertical profiles of cloud properties from ground-based
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![CloudnetPy CI](https://github.com/actris-cloudnet/cloudnetpy/actions/workflows/test.yml/badge.svg)](https://github.com/actris-cloudnet/cloudnetpy/actions/workflows/test.yml)
4
4
  [![PyPI version](https://img.shields.io/pypi/v/cloudnetpy.svg)](https://pypi.org/project/cloudnetpy/)
5
- [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.3666030.svg)](https://doi.org/10.5281/zenodo.3666030)
5
+ [![DOI](https://img.shields.io/badge/DOI-10.5281%2Fzenodo.3666030-blue)](https://doi.org/10.5281/zenodo.3666030)
6
6
  [![status](https://joss.theoj.org/papers/959971f196f617dddc0e7d8333ff22b7/status.svg)](https://joss.theoj.org/papers/959971f196f617dddc0e7d8333ff22b7)
7
7
 
8
8
  CloudnetPy is Python software designed for producing vertical profiles of cloud properties from ground-based
@@ -237,6 +237,32 @@ LEIPZIG_KEYS = {
237
237
  FLOAT_KEYS = {1, 2, 7, 16, 17, 24, 30, 31, 33, 34, 35}
238
238
  INT_KEYS = {3, 4, 8, 9, 10, 11, 12, 18, 25, 26, 27, 28, 60}
239
239
 
240
+ KOLSASS_TELEGRAM: list[str | int | None] = [
241
+ "%Y-%m-%d %H:%M:%S",
242
+ 18,
243
+ 20,
244
+ 25,
245
+ 1,
246
+ 2,
247
+ 7,
248
+ 9,
249
+ 35,
250
+ 11,
251
+ 90,
252
+ 91,
253
+ 93,
254
+ 13,
255
+ 14,
256
+ 15,
257
+ 16,
258
+ 17,
259
+ 10,
260
+ 12,
261
+ 26,
262
+ 27,
263
+ 28,
264
+ ]
265
+
240
266
 
241
267
  def _read_lines(
242
268
  telegram: Sequence[str | int | None],
@@ -387,6 +413,17 @@ def _read_granada(filename: str | PathLike) -> ParsivelOutput:
387
413
  return _read_lines(telegram, content, b",", b".")
388
414
 
389
415
 
416
+ def _read_kolsass(filename: str | PathLike) -> ParsivelOutput:
417
+ with open(filename, "rb") as f:
418
+ _header = f.readline()
419
+ content = f.read()
420
+ # Fix "0-9" to "-9" (e.g. 2025-01-01)
421
+ content = re.sub(rb",0-(\d),", rb",-\1,", content)
422
+ # Remove decimal part from integers (e.g. 2025-01-02)
423
+ content = re.sub(rb"(\d+)\.0,", rb"\1,", content, flags=re.MULTILINE)
424
+ return _read_lines(KOLSASS_TELEGRAM, content, b",", b".")
425
+
426
+
390
427
  def _read_headerless(
391
428
  filename: str | PathLike,
392
429
  telegram: Sequence[int | str | None],
@@ -516,6 +553,8 @@ def _read_parsivel(
516
553
  head = f.read(50)
517
554
  if head.startswith(b"datetime_utc;"):
518
555
  return _read_pyatmoslogger(filename)
556
+ if head.startswith(b"datetime_utc,sensor status,sensor time,"):
557
+ return _read_kolsass(filename)
519
558
  if head.startswith(b"Date;Time;"):
520
559
  return _read_asdo(filename)
521
560
  if head.startswith(b'"TOA5"'):
@@ -9,18 +9,18 @@ from uuid import UUID
9
9
 
10
10
  import netCDF4
11
11
  import numpy as np
12
- from ceilopyter import read_ct25k
12
+ from ceilopyter import read_ct25k, read_ld40
13
13
  from numpy import ma
14
14
 
15
15
  from cloudnetpy import output
16
16
  from cloudnetpy.instruments.cl61d import Cl61d
17
17
  from cloudnetpy.instruments.da10 import Da10
18
18
  from cloudnetpy.instruments.lufft import LufftCeilo
19
- from cloudnetpy.instruments.vaisala import ClCeilo, Cs135, Ct25k
19
+ from cloudnetpy.instruments.vaisala import ClCeilo, Cs135, Ct25k, Ld40
20
20
  from cloudnetpy.metadata import COMMON_ATTRIBUTES, MetaData
21
21
  from cloudnetpy.utils import get_uuid
22
22
 
23
- CeiloObject = ClCeilo | Ct25k | LufftCeilo | Cl61d | Cs135 | Da10
23
+ CeiloObject = ClCeilo | Ct25k | LufftCeilo | Cl61d | Cs135 | Da10 | Ld40
24
24
 
25
25
 
26
26
  def ceilo2nc(
@@ -90,6 +90,20 @@ def ceilo2nc(
90
90
  c_obj = read_ct25k(full_path, calibration_factor, range_corrected)
91
91
  ceilo_obj.data["beta"] = c_obj.beta
92
92
  ceilo_obj.data["beta_raw"] = c_obj.beta_raw
93
+ ceilo_obj.data["beta_smooth"] = c_obj.beta_smooth
94
+ ceilo_obj.data["time"] = c_obj.time
95
+ ceilo_obj.data["range"] = c_obj.range
96
+ if c_obj.zenith_angle is not None:
97
+ ceilo_obj.data["zenith_angle"] = np.median(c_obj.zenith_angle)
98
+ ceilo_obj.data["calibration_factor"] = c_obj.calibration_factor
99
+ ceilo_obj.sort_time()
100
+ ceilo_obj.screen_date()
101
+ ceilo_obj.convert_to_fraction_hour()
102
+ elif isinstance(ceilo_obj, Ld40):
103
+ c_obj = read_ld40(full_path, calibration_factor)
104
+ ceilo_obj.data["beta"] = c_obj.beta
105
+ ceilo_obj.data["beta_raw"] = c_obj.beta_raw
106
+ ceilo_obj.data["beta_smooth"] = c_obj.beta_smooth
93
107
  ceilo_obj.data["time"] = c_obj.time
94
108
  ceilo_obj.data["range"] = c_obj.range
95
109
  if c_obj.zenith_angle is not None:
@@ -174,6 +188,7 @@ def _initialize_ceilo(
174
188
  "chm15k",
175
189
  "cs135",
176
190
  "da10",
191
+ "ld40",
177
192
  ):
178
193
  msg = f"Invalid ceilometer model: {site_meta['model']}"
179
194
  raise ValueError(msg)
@@ -193,6 +208,8 @@ def _initialize_ceilo(
193
208
  return Da10(full_path, site_meta, date)
194
209
  if model == "cs135":
195
210
  return Cs135(full_path, site_meta, date)
211
+ if model == "ld40":
212
+ return Ld40(full_path, site_meta, date)
196
213
  return LufftCeilo(full_path, site_meta, date)
197
214
 
198
215
 
@@ -51,6 +51,14 @@ CT25K = Instrument(
51
51
  wavelength=905.0,
52
52
  )
53
53
 
54
+ LD40 = Instrument(
55
+ manufacturer="Vaisala",
56
+ domain="lidar",
57
+ category="ceilometer",
58
+ model="LD40",
59
+ wavelength=855.0,
60
+ )
61
+
54
62
  CL61D = Instrument(
55
63
  manufacturer="Vaisala",
56
64
  domain="lidar",
@@ -164,10 +164,10 @@ class Kazr(CloudnetInstrument):
164
164
 
165
165
  def read_files(self) -> None:
166
166
  """Reads and concatenates all input files."""
167
+ with netCDF4.Dataset(self._find_reference_file()) as nc:
168
+ self._init_metadata(nc)
167
169
  for file in self.files:
168
170
  with netCDF4.Dataset(file) as nc:
169
- if not self.keymap:
170
- self._init_metadata(nc)
171
171
  try:
172
172
  self._read_file(nc)
173
173
  except (KeyError, ValueError) as err:
@@ -350,6 +350,24 @@ class Kazr(CloudnetInstrument):
350
350
  msg = "All radar data are masked"
351
351
  raise RadarDataError(msg)
352
352
 
353
+ def _find_reference_file(self) -> Path:
354
+ """Returns the first file using the range grid with the most profiles."""
355
+ n_profiles: dict[int, int] = {}
356
+ first_file: dict[int, Path] = {}
357
+ for file in self.files:
358
+ with netCDF4.Dataset(file) as nc:
359
+ if "range" not in nc.dimensions or "time" not in nc.dimensions:
360
+ continue
361
+ n_range = nc.dimensions["range"].size
362
+ n_profiles[n_range] = (
363
+ n_profiles.get(n_range, 0) + nc.dimensions["time"].size
364
+ )
365
+ first_file.setdefault(n_range, file)
366
+ if not first_file:
367
+ msg = "No valid KAZR files found"
368
+ raise ValidTimeStampError(msg)
369
+ return first_file[max(n_profiles, key=lambda k: n_profiles[k])]
370
+
353
371
  def _init_metadata(self, nc: netCDF4.Dataset) -> None:
354
372
  if "reflectivity" in nc.variables:
355
373
  self.keymap = KEYMAP_CFR
@@ -366,6 +384,7 @@ class Kazr(CloudnetInstrument):
366
384
  if bias is not None:
367
385
  self.offset_applied = _parse_bias(bias)
368
386
  self.serial_number = getattr(nc, "serial_number", None) or None
387
+ self.date = _parse_time_units(nc["time"].units).date()
369
388
  self.append_data(np.array(nc["range"][:], dtype=float), "range")
370
389
  self.append_data(0.0, "zenith_angle")
371
390
  self.append_data(self._read_frequency(nc), "radar_frequency")
@@ -421,7 +440,11 @@ class Kazr(CloudnetInstrument):
421
440
  @staticmethod
422
441
  def _read_frequency(nc: netCDF4.Dataset) -> float:
423
442
  if "frequency" in nc.variables:
424
- return float(np.mean(nc["frequency"][:]) / 1e9) # Hz -> GHz
443
+ frequency = ma.mean(nc["frequency"][:])
444
+ if not ma.is_masked(frequency) and np.isfinite(frequency):
445
+ return float(frequency / 1e9) # Hz -> GHz
446
+ # E.g. bnfkazr2cfrgeqcM1.b1 files have only a fill value
447
+ logging.warning("Radar frequency missing from file, using default")
425
448
  value = _parse_global_attribute(nc, "radar_operating_frequency")
426
449
  if value is not None:
427
450
  return value
@@ -34,6 +34,7 @@ class LufftCeilo(Ceilometer):
34
34
  ceilo = read_chm15k(self.file_name, calibration_factor)
35
35
  self.data["beta"] = ceilo.beta
36
36
  self.data["beta_raw"] = ceilo.beta_raw
37
+ self.data["beta_smooth"] = ceilo.beta_smooth
37
38
  self.data["time"] = ceilo.time
38
39
  self.data["range"] = ceilo.range
39
40
  if ceilo.zenith_angle is not None:
@@ -7,7 +7,7 @@ from os import PathLike
7
7
  import ceilopyter.version
8
8
  import numpy as np
9
9
  import numpy.typing as npt
10
- from ceilopyter import read_cl_file, read_cs_file, read_ct_file
10
+ from ceilopyter import read_cl_file, read_cs_file
11
11
 
12
12
  from cloudnetpy.exceptions import ValidTimeStampError
13
13
  from cloudnetpy.instruments import instruments
@@ -118,13 +118,31 @@ class Ct25k(VaisalaCeilo):
118
118
  site_meta: dict,
119
119
  expected_date: datetime.date | None = None,
120
120
  ) -> None:
121
- super().__init__(read_ct_file, full_path, site_meta, expected_date)
121
+ super().__init__(no_reader, full_path, site_meta, expected_date)
122
122
  self._store_ceilometer_info()
123
123
 
124
124
  def _store_ceilometer_info(self) -> None:
125
125
  self.instrument = instruments.CT25K
126
126
 
127
127
 
128
+ class Ld40(VaisalaCeilo):
129
+ """Class for Vaisala LD40 ceilometer."""
130
+
131
+ noise_param = NoiseParam(noise_min=0.7e-7, noise_smooth_min=1.2e-8)
132
+
133
+ def __init__(
134
+ self,
135
+ full_path: str | PathLike,
136
+ site_meta: dict,
137
+ expected_date: datetime.date | None = None,
138
+ ) -> None:
139
+ super().__init__(no_reader, full_path, site_meta, expected_date)
140
+ self._store_ceilometer_info()
141
+
142
+ def _store_ceilometer_info(self) -> None:
143
+ self.instrument = instruments.LD40
144
+
145
+
128
146
  class Cs135(VaisalaCeilo):
129
147
  """Class for Campbell Scientific CS135 ceilometer."""
130
148
 
@@ -140,3 +158,8 @@ class Cs135(VaisalaCeilo):
140
158
 
141
159
  def _store_ceilometer_info(self) -> None:
142
160
  self.instrument = instruments.CS135
161
+
162
+
163
+ def no_reader() -> None:
164
+ msg = "No ceilometer reader"
165
+ raise RuntimeError(msg)
@@ -171,7 +171,9 @@ REGRID_PRODUCT_ATTRIBUTES = {
171
171
  "This variable is the observed mean ice water content derived from radar\n"
172
172
  "reflectivity factor averaged onto the model grid with height and time.\n"
173
173
  "The formula has been applied where the categorization data has\n"
174
- "diagnosed that the radar echo is due to ice."
174
+ "diagnosed that the radar echo is due to ice; pixels without ice count\n"
175
+ "as zero and unreliable retrievals (e.g. rain) are excluded, so the\n"
176
+ "value is a grid-box mean comparable to the model ice water content."
175
177
  ),
176
178
  dimensions=("time", "level"),
177
179
  ),
@@ -6,6 +6,7 @@ from numpy import ma
6
6
 
7
7
  import cloudnetpy.utils as cl_tools
8
8
  from cloudnetpy.constants import T_FREEZING
9
+ from cloudnetpy.exceptions import InvalidSourceFileError
9
10
  from cloudnetpy.model_evaluation.model_metadata import MODEL_PREFIX
10
11
  from cloudnetpy.model_evaluation.products.model_products import ModelManager
11
12
  from cloudnetpy.model_evaluation.products.observation_products import ObservationManager
@@ -15,6 +16,8 @@ from cloudnetpy.products.product_tools import (
15
16
  z_to_iwc,
16
17
  )
17
18
 
19
+ MAX_WIND_SHEAR = 20.0 # m s-1 km-1
20
+
18
21
 
19
22
  class AdvanceProductMethods:
20
23
  """Class that adds advance methods of product to nc-file.
@@ -53,7 +56,8 @@ class AdvanceProductMethods:
53
56
  t_screened = self.remove_extra_levels(temperature - T_FREEZING)
54
57
  iwc, lwc = (self._model_obj.get_water_content(var) for var in ["iwc", "lwc"])
55
58
  coeffs = self.set_frequency_parameters()
56
- z_sen = self.fit_z_sensitivity(h)
59
+ # Same dielectric factor scaling of Z as in the IWC product.
60
+ z_sen = self.fit_z_sensitivity(h) + 10 * np.log10(coeffs.K2liquid0 / 0.93)
57
61
  cf_filtered = self.filter_high_iwc_low_cf(cf, iwc, lwc)
58
62
  cloud_iwc, ice_ind = self.find_ice_in_clouds(cf_filtered, iwc, lwc)
59
63
  variance_iwc = self.iwc_variance(h, ice_ind)
@@ -102,15 +106,14 @@ class AdvanceProductMethods:
102
106
  if self._obs_obj.z_sensitivity is None:
103
107
  msg = "No z_sensitivity in observation file"
104
108
  raise ValueError(msg)
105
- if self._obs_obj.height is None:
106
- msg = "No height in observation file"
107
- raise ValueError(msg)
109
+ if self._obs_obj.height_agl is None:
110
+ msg = "Observation file is missing height or altitude"
111
+ raise InvalidSourceFileError(msg)
108
112
  # Interpolate at the model level heights: rebinning assumes evenly
109
113
  # spaced levels, which maps stretched model levels to wrong heights.
110
- obs_height = self._obs_obj.height_agl
111
- if obs_height is None:
112
- obs_height = self._obs_obj.height
113
- obs_height = np.asarray(ma.filled(obs_height, np.nan), dtype=float)
114
+ obs_height = np.asarray(
115
+ ma.filled(self._obs_obj.height_agl, np.nan), dtype=float
116
+ )
114
117
  z_obs = np.asarray(ma.filled(self._obs_obj.z_sensitivity, np.nan), dtype=float)
115
118
  is_valid = np.isfinite(obs_height) & np.isfinite(z_obs)
116
119
  model_height = np.asarray(ma.filled(h, np.nan), dtype=float)
@@ -167,36 +170,45 @@ class AdvanceProductMethods:
167
170
  v = self._model_obj.getvar("vwind")
168
171
  u = self.remove_extra_levels(u)
169
172
  v = self.remove_extra_levels(v)
170
- w_shear = self.calculate_wind_shear(self._model_obj.wind, u, v, height)
173
+ w_shear = self.calculate_wind_shear(u, v, height)
171
174
  return self.calculate_variance_iwc(w_shear, ice_ind)
172
175
 
173
176
  def calculate_variance_iwc(
174
177
  self, w_shear: npt.NDArray, ice_ind: tuple
175
178
  ) -> npt.NDArray:
179
+ """Fractional variance of IWC within a grid box (Hogan and Illingworth
180
+ 2003, eq. 11), with the horizontal resolution in km and the wind shear
181
+ in m s-1 km-1. The shear is capped and the constant is -1.03 as in the
182
+ original Cloudnet code (the published equation has -0.93).
183
+ """
184
+ shear = np.minimum(w_shear[ice_ind], MAX_WIND_SHEAR)
176
185
  return 10 ** (
177
- 0.3 * np.log10(self._model_obj.resolution_h)
178
- - 0.04 * w_shear[ice_ind]
179
- - 1.03
186
+ 0.3 * np.log10(self._model_obj.resolution_h) - 0.04 * shear - 1.03
180
187
  )
181
188
 
182
189
  @staticmethod
183
190
  def calculate_wind_shear(
184
- wind: npt.NDArray,
185
191
  u: npt.NDArray,
186
192
  v: npt.NDArray,
187
193
  height: npt.NDArray,
188
194
  ) -> npt.NDArray:
189
- grand_winds = []
190
- for w in (wind, u, v):
195
+ """Magnitude of the vertical shear of the horizontal wind (m s-1 km-1)
196
+ from (time, level) arrays.
197
+ """
198
+ height_km = height / 1000
199
+ gradients = []
200
+ for w in (u, v):
191
201
  grad_w = np.zeros(w.shape)
192
- grad_w[0, :] = (w[1, :] - w[0, :]) / (height[1, :] - height[0, :])
193
- grad_w[1:-1, :] = (w[2:, :] - w[:-2, :]) / (height[2:, :] - height[:-2, :])
194
- grad_w[-1, :] = (w[-1, :] - w[-2, :]) / (height[-1, :] - height[-2, :])
195
- grand_winds.append(grad_w)
196
-
197
- w_shear = np.sqrt(np.power(grand_winds[1], 2) + np.power(grand_winds[-1], 2))
198
- w_shear[grand_winds[0] < 0] = 0 - w_shear[grand_winds[0] < 0]
199
- return w_shear
202
+ grad_w[:, 0] = (w[:, 1] - w[:, 0]) / (height_km[:, 1] - height_km[:, 0])
203
+ grad_w[:, 1:-1] = (w[:, 2:] - w[:, :-2]) / (
204
+ height_km[:, 2:] - height_km[:, :-2]
205
+ )
206
+ grad_w[:, -1] = (w[:, -1] - w[:, -2]) / (
207
+ height_km[:, -1] - height_km[:, -2]
208
+ )
209
+ gradients.append(grad_w)
210
+
211
+ return np.sqrt(np.power(gradients[0], 2) + np.power(gradients[1], 2))
200
212
 
201
213
  @staticmethod
202
214
  def calculate_iwc_distribution(
@@ -2,7 +2,7 @@ import numpy as np
2
2
  import numpy.typing as npt
3
3
  from numpy import ma
4
4
 
5
- from cloudnetpy import utils
5
+ from cloudnetpy.exceptions import InvalidSourceFileError
6
6
  from cloudnetpy.model_evaluation.products import tools as tl
7
7
  from cloudnetpy.model_evaluation.products.model_products import ModelManager
8
8
  from cloudnetpy.model_evaluation.products.observation_products import ObservationManager
@@ -24,7 +24,12 @@ class ProductGrid:
24
24
  self._obs_obj = obs_obj
25
25
  self._date = obs_obj.date
26
26
  self._obs_time = tl.time2datetime(obs_obj.time, self._date)
27
- self._obs_height = obs_obj.data["height"][:]
27
+ # Model heights are above ground; the observation file's height is
28
+ # above mean sea level.
29
+ if obs_obj.height_agl is None:
30
+ msg = "Observation file is missing height or altitude"
31
+ raise InvalidSourceFileError(msg)
32
+ self._obs_height = np.asarray(obs_obj.height_agl)
28
33
  self._obs_data = obs_obj.data[obs_obj.product][:]
29
34
  self.model_obj = model_obj
30
35
  self._model_time = model_obj.time
@@ -34,21 +39,22 @@ class ProductGrid:
34
39
  ma.array(model_obj.wind),
35
40
  1,
36
41
  )
37
- time_steps = utils.binvec(self._model_time)
38
- self._time_steps = tl.time2datetime(time_steps, self._date)
42
+ self._time_windows = [
43
+ tuple(tl.time2datetime(np.array(edges), self._date))
44
+ for edges in tl.time_windows(self._model_time)
45
+ ]
39
46
  self._generate_downsample_product()
40
47
 
41
48
  def _generate_downsample_product(self) -> None:
42
49
  """Downsampling products are generated with different averaging methods
43
50
  for a selected size of model time-height window.
51
+
52
+ Grid cells without any observation pixels stay masked.
44
53
  """
45
54
  product_dict, product_adv_dict = self._get_method_storage()
46
55
  model_t = tl.time2datetime(self._model_time, self._date)
47
- for i in range(len(self._time_steps) - 1):
48
- x_ind = tl.get_1d_indices(
49
- (self._time_steps[i], self._time_steps[i + 1]),
50
- self._obs_time,
51
- )
56
+ for i, window in enumerate(self._time_windows):
57
+ x_ind = tl.get_1d_indices(window, self._obs_time)
52
58
  y_steps = tl.rebin_edges(self._model_height[i])
53
59
  for j in range(len(y_steps) - 1):
54
60
  x_ind_adv = tl.get_adv_indices(
@@ -91,19 +97,21 @@ class ProductGrid:
91
97
 
92
98
  def _cf_method_storage(self) -> tuple[dict, dict]:
93
99
  cf_dict = {
94
- "cf_V": ma.zeros(self._model_height.shape),
95
- "cf_A": ma.zeros(self._model_height.shape),
100
+ "cf_V": ma.masked_all(self._model_height.shape),
101
+ "cf_A": ma.masked_all(self._model_height.shape),
96
102
  }
97
103
  cf_adv_dict = {
98
- "cf_V_adv": ma.zeros(self._model_height.shape),
99
- "cf_A_adv": ma.zeros(self._model_height.shape),
104
+ "cf_V_adv": ma.masked_all(self._model_height.shape),
105
+ "cf_A_adv": ma.masked_all(self._model_height.shape),
100
106
  }
101
107
  return cf_dict, cf_adv_dict
102
108
 
103
109
  def _product_method_storage(self) -> tuple[dict, dict]:
104
- product_dict = {f"{self._obs_obj.product}": ma.zeros(self._model_height.shape)}
110
+ product_dict = {
111
+ f"{self._obs_obj.product}": ma.masked_all(self._model_height.shape)
112
+ }
105
113
  product_adv_dict = {
106
- f"{self._obs_obj.product}_adv": ma.zeros(self._model_height.shape),
114
+ f"{self._obs_obj.product}_adv": ma.masked_all(self._model_height.shape),
107
115
  }
108
116
  return product_dict, product_adv_dict
109
117
 
@@ -103,10 +103,14 @@ class ObservationManager(DataSource):
103
103
  """Keeps only reliable ice water content retrievals.
104
104
 
105
105
  Status 1 is a reliable retrieval and status 3 is a retrieval with the
106
- radar corrected for liquid, rain and melting attenuation; everything
107
- else (uncorrected attenuation, lidar-only, rain) is masked out.
106
+ radar corrected for liquid, rain and melting attenuation. Status 0 (no
107
+ ice) is set to zero so that the regridded value is a grid-box mean,
108
+ comparable to the model. Everything else (uncorrected attenuation,
109
+ lidar-only, rain) is masked out.
108
110
  """
109
111
  iwc = self.getvar("iwc")
110
- iwc_status = self.getvar("iwc_retrieval_status")
111
- iwc[~np.isin(iwc_status, (1, 3))] = ma.masked
112
+ status = ma.filled(self.getvar("iwc_retrieval_status"), -1)
113
+ no_ice = status == 0
114
+ invalid = (ma.getmaskarray(iwc) & ~no_ice) | ~np.isin(status, (0, 1, 3))
115
+ iwc = ma.array(ma.filled(iwc, 0), mask=invalid)
112
116
  self.append_data(iwc, "iwc")
@@ -13,6 +13,17 @@ def time2datetime(time: npt.NDArray, date: datetime.datetime) -> npt.NDArray:
13
13
  return np.asarray([date + timedelta(hours=float(t)) for t in time])
14
14
 
15
15
 
16
+ def time_windows(time: npt.NDArray) -> list[tuple[float, float]]:
17
+ """Averaging window around each model time step.
18
+
19
+ Each window is +/- half the smallest time step, so a gap in the time
20
+ vector (e.g. a missing forecast run) does not widen the windows.
21
+ """
22
+ time = np.asarray(time, dtype=float)
23
+ half = np.min(np.diff(time)) / 2 if time.size > 1 else 0.5
24
+ return [(t - half, t + half) for t in time]
25
+
26
+
16
27
  def rebin_edges(arr: npt.NDArray) -> npt.NDArray:
17
28
  """Rebins array bins by half and adds boundaries."""
18
29
  new_arr = [(arr[i] + arr[i + 1]) / 2 for i in range(len(arr) - 1)]
@@ -229,19 +229,39 @@ def test_calculate_variance_iwc(obs_file, model_file) -> None:
229
229
  testing.assert_array_almost_equal(result, expected)
230
230
 
231
231
 
232
+ def test_calculate_variance_iwc_shear_cap(obs_file, model_file) -> None:
233
+ obs = ObservationManager(PRODUCT, str(obs_file))
234
+ model = ModelManager(str(model_file), MODEL, PRODUCT)
235
+ advance = AdvanceProductMethods(model, obs)
236
+ shear = np.array([[20.0, 50.0]])
237
+ result = advance.calculate_variance_iwc(shear, (np.array([0, 0]), np.array([0, 1])))
238
+ assert result[0] == result[1]
239
+
240
+
232
241
  def test_calculate_wind_shear(obs_file, model_file) -> None:
233
242
  obs = ObservationManager(PRODUCT, str(obs_file))
234
243
  model = ModelManager(str(model_file), MODEL, PRODUCT)
235
244
  advance = AdvanceProductMethods(model, obs)
236
- u = np.array([[1, 2, 0, 1], [-1, 0, 1, -1], [1, 0, 1, -1]])
237
- v = np.array([[1, 0, 1, -1], [1, 2, -1, 0], [1, 2, 0, 1]])
238
- wind = np.sqrt(np.power(u, 2) + np.power(v, 2))
239
- height = np.array([[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3]])
240
- expected = np.array([[2, 2.83, 2.24, -2.24], [0, 1.41, 0.71, 1.41], [2, 0, -1, 1]])
241
- result = advance.calculate_wind_shear(wind, u, v, height)
245
+ # (time, level) arrays; shear is the vertical gradient in m s-1 km-1
246
+ u = np.array([[1, 2, 0, 1], [-1, 0, 1, -1], [1, 0, 1, -1]]).T
247
+ v = np.array([[1, 0, 1, -1], [1, 2, -1, 0], [1, 2, 0, 1]]).T
248
+ height = np.array([[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3]]).T * 1000
249
+ expected = np.array([[2, 2.83, 2.24, 2.24], [0, 1.41, 0.71, 1.41], [2, 0, 1, 1]]).T
250
+ result = advance.calculate_wind_shear(u, v, height)
242
251
  testing.assert_array_almost_equal(np.round(result, 2), expected)
243
252
 
244
253
 
254
+ def test_calculate_wind_shear_constant_height_in_time(obs_file, model_file) -> None:
255
+ obs = ObservationManager(PRODUCT, str(obs_file))
256
+ model = ModelManager(str(model_file), MODEL, PRODUCT)
257
+ advance = AdvanceProductMethods(model, obs)
258
+ u = np.array([[1.0, 3.0, 4.0], [1.0, 3.0, 4.0]])
259
+ v = np.zeros_like(u)
260
+ height = np.array([[0.0, 1000.0, 2000.0], [0.0, 1000.0, 2000.0]])
261
+ result = advance.calculate_wind_shear(u, v, height)
262
+ testing.assert_array_almost_equal(result, [[2, 1.5, 1], [2, 1.5, 1]])
263
+
264
+
245
265
  def test_calculate_iwc_distribution(obs_file, model_file) -> None:
246
266
  obs = ObservationManager(PRODUCT, str(obs_file))
247
267
  model = ModelManager(str(model_file), MODEL, PRODUCT)
@@ -2,6 +2,7 @@ import numpy as np
2
2
  import pytest
3
3
  from numpy import ma, testing
4
4
 
5
+ from cloudnetpy.exceptions import InvalidSourceFileError
5
6
  from cloudnetpy.model_evaluation.products.grid_methods import ProductGrid
6
7
  from cloudnetpy.model_evaluation.products.model_products import ModelManager
7
8
  from cloudnetpy.model_evaluation.products.observation_products import ObservationManager
@@ -42,6 +43,14 @@ def test_generate_regrid_product(model_file, obs_file, product, variables) -> No
42
43
  assert var in model.data
43
44
 
44
45
 
46
+ def test_missing_height_agl_raises(model_file, obs_file) -> None:
47
+ obs = ObservationManager(PRODUCT, str(obs_file))
48
+ model = ModelManager(str(model_file), MODEL, PRODUCT)
49
+ obs.height_agl = None
50
+ with pytest.raises(InvalidSourceFileError):
51
+ ProductGrid(model, obs)
52
+
53
+
45
54
  @pytest.mark.parametrize("key, value", [("iwc", 1), ("lwc", 1), ("cf", 2)])
46
55
  def test_get_method_storage(key, value, model_file, obs_file) -> None:
47
56
  obs = ObservationManager(key, str(obs_file))
@@ -113,6 +113,7 @@ def test_mask_iwc(obs_file) -> None:
113
113
  obs = ObservationManager("iwc", str(obs_file))
114
114
  iwc_status = obs.getvar("iwc_retrieval_status")
115
115
  expected = ma.copy(obs.getvar("iwc"))
116
- expected[~np.isin(iwc_status, (1, 3))] = ma.masked
116
+ expected[iwc_status == 0] = 0
117
+ expected[~np.isin(iwc_status, (0, 1, 3))] = ma.masked
117
118
  obs._mask_iwc()
118
119
  testing.assert_array_almost_equal(expected, obs.data["iwc"][:])
@@ -128,3 +128,17 @@ def test_obs_windows_size_first_index() -> None:
128
128
  result = tools.get_obs_window_size(ind_x, ind_y)
129
129
  assert result is not None
130
130
  testing.assert_almost_equal(result, (1, 1))
131
+
132
+
133
+ def test_time_windows_even_spacing() -> None:
134
+ result = tools.time_windows(np.array([0.0, 1.0, 2.0]))
135
+ testing.assert_array_almost_equal(result, [(-0.5, 0.5), (0.5, 1.5), (1.5, 2.5)])
136
+
137
+
138
+ def test_time_windows_gap() -> None:
139
+ result = tools.time_windows(np.array([0.0, 1.0, 24.0]))
140
+ testing.assert_array_almost_equal(result, [(-0.5, 0.5), (0.5, 1.5), (23.5, 24.5)])
141
+
142
+
143
+ def test_time_windows_single() -> None:
144
+ testing.assert_array_almost_equal(tools.time_windows(np.array([3.0])), [(2.5, 3.5)])
@@ -1,4 +1,4 @@
1
1
  MAJOR = 1
2
2
  MINOR = 97
3
- PATCH = 1
3
+ PATCH = 3
4
4
  __version__ = f"{MAJOR}.{MINOR}.{PATCH}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudnetpy
3
- Version: 1.97.1
3
+ Version: 1.97.3
4
4
  Summary: Python package for Cloudnet processing
5
5
  Author: Simo Tukiainen
6
6
  License: MIT License
@@ -39,7 +39,7 @@ Requires-Python: >=3.11
39
39
  Description-Content-Type: text/markdown
40
40
  License-File: LICENSE
41
41
  Requires-Dist: atmoslib>=2.4.2
42
- Requires-Dist: ceilopyter>=0.2.2
42
+ Requires-Dist: ceilopyter>=0.2.3
43
43
  Requires-Dist: cloudnet-api-client
44
44
  Requires-Dist: doppy>=0.5.0
45
45
  Requires-Dist: matplotlib
@@ -66,7 +66,7 @@ Dynamic: license-file
66
66
 
67
67
  [![CloudnetPy CI](https://github.com/actris-cloudnet/cloudnetpy/actions/workflows/test.yml/badge.svg)](https://github.com/actris-cloudnet/cloudnetpy/actions/workflows/test.yml)
68
68
  [![PyPI version](https://img.shields.io/pypi/v/cloudnetpy.svg)](https://pypi.org/project/cloudnetpy/)
69
- [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.3666030.svg)](https://doi.org/10.5281/zenodo.3666030)
69
+ [![DOI](https://img.shields.io/badge/DOI-10.5281%2Fzenodo.3666030-blue)](https://doi.org/10.5281/zenodo.3666030)
70
70
  [![status](https://joss.theoj.org/papers/959971f196f617dddc0e7d8333ff22b7/status.svg)](https://joss.theoj.org/papers/959971f196f617dddc0e7d8333ff22b7)
71
71
 
72
72
  CloudnetPy is Python software designed for producing vertical profiles of cloud properties from ground-based
@@ -1,5 +1,5 @@
1
1
  atmoslib>=2.4.2
2
- ceilopyter>=0.2.2
2
+ ceilopyter>=0.2.3
3
3
  cloudnet-api-client
4
4
  doppy>=0.5.0
5
5
  matplotlib
@@ -15,7 +15,7 @@ classifiers = [
15
15
  ]
16
16
  dependencies = [
17
17
  "atmoslib>=2.4.2",
18
- "ceilopyter>=0.2.2",
18
+ "ceilopyter>=0.2.3",
19
19
  "cloudnet-api-client",
20
20
  "doppy>=0.5.0",
21
21
  "matplotlib",
File without changes
File without changes
File without changes