Source code for instruments.ceilo

"""Module for reading and processing Vaisala / Lufft ceilometers."""

from itertools import islice

import netCDF4
from numpy import ma

from cloudnetpy import output, utils
from cloudnetpy.instruments.campbell_scientific import Cs135
from cloudnetpy.instruments.cl61d import Cl61d
from cloudnetpy.instruments.lufft import LufftCeilo
from cloudnetpy.instruments.vaisala import ClCeilo, Ct25k
from cloudnetpy.metadata import MetaData


[docs] def ceilo2nc( full_path: str, output_file: str, site_meta: dict, uuid: str | None = None, date: str | None = None, ) -> str: """Converts Vaisala, Lufft and Campbell Scientific ceilometer data into Cloudnet Level 1b netCDF file. This function reads raw Vaisala (CT25k, CL31, CL51, CL61), Lufft (CHM 15k, CHM 15k-x) and Campbell Scientific (CS135) ceilometer files and writes the data into netCDF file. Three variants of the backscatter are saved: 1. Raw backscatter, `beta_raw` 2. Signal-to-noise screened backscatter, `beta` 3. SNR-screened backscatter with smoothed weak background, `beta_smooth` With CL61 two additional depolarisation parameters are saved: 1. Signal-to-noise screened depolarisation, `depolarisation` 2. SNR-screened depolarisation with smoothed weak background, `depolarisation_smooth` CL61 screened backscatter is screened using beta_smooth mask to improve detection of weak aerosol layers and supercooled liquid clouds. Args: full_path: Ceilometer file name. output_file: Output file name, e.g. 'ceilo.nc'. site_meta: Dictionary containing information about the site and instrument. Required key value pairs are `name` and `altitude` (metres above mean sea level). Also, 'calibration_factor' is recommended because the default value is probably incorrect. If the background noise is *not* range-corrected, you must define: {'range_corrected': False}. You can also explicitly set the instrument model with e.g. {'model': 'cl61d'}. uuid: Set specific UUID for the file. date: Expected date as YYYY-MM-DD of all profiles in the file. Returns: UUID of the generated file. Raises: RuntimeError: Failed to read or process raw ceilometer data. Examples: >>> from cloudnetpy.instruments import ceilo2nc >>> site_meta = {'name': 'Mace-Head', 'altitude': 5} >>> ceilo2nc('vaisala_raw.txt', 'vaisala.nc', site_meta) >>> site_meta = {'name': 'Juelich', 'altitude': 108, 'calibration_factor': 2.3e-12} >>> ceilo2nc('chm15k_raw.nc', 'chm15k.nc', site_meta) """ snr_limit = 5 ceilo_obj = _initialize_ceilo(full_path, site_meta, date) calibration_factor = site_meta.get("calibration_factor") range_corrected = site_meta.get("range_corrected", True) ceilo_obj.read_ceilometer_file(calibration_factor) ceilo_obj.check_beta_raw_shape() n_negatives = _get_n_negatives(ceilo_obj) ceilo_obj.data["beta"] = ceilo_obj.calc_screened_product( ceilo_obj.data["beta_raw"], snr_limit, range_corrected=range_corrected, n_negatives=n_negatives, ) ceilo_obj.data["beta_smooth"] = ceilo_obj.calc_beta_smooth( ceilo_obj.data["beta"], snr_limit, range_corrected=range_corrected, n_negatives=n_negatives, ) if ceilo_obj.instrument is None or ceilo_obj.instrument.model is None: msg = "Failed to read ceilometer model" raise RuntimeError(msg) if ( any( model in ceilo_obj.instrument.model.lower() for model in ("cl61", "chm15k", "chm15kx", "cl51", "cl31") ) and range_corrected ): mask = ceilo_obj.data["beta_smooth"].mask ceilo_obj.data["beta"] = ma.masked_where(mask, ceilo_obj.data["beta_raw"]) ceilo_obj.data["beta"][ceilo_obj.data["beta"] <= 0] = ma.masked if "depolarisation" in ceilo_obj.data: ceilo_obj.data["depolarisation"].mask = ceilo_obj.data["beta"].mask ceilo_obj.screen_depol() ceilo_obj.screen_invalid_values() ceilo_obj.prepare_data() ceilo_obj.data_to_cloudnet_arrays() attributes = output.add_time_attribute(ATTRIBUTES, ceilo_obj.date) output.update_attributes(ceilo_obj.data, attributes) for key in ("beta", "beta_smooth"): ceilo_obj.add_snr_info(key, snr_limit) return output.save_level1b(ceilo_obj, output_file, uuid)
def _get_n_negatives(ceilo_obj: ClCeilo | Ct25k | LufftCeilo | Cl61d | Cs135) -> int: is_old_chm_version = ( hasattr(ceilo_obj, "is_old_version") and ceilo_obj.is_old_version ) is_ct25k = ( ceilo_obj.instrument is not None and getattr(ceilo_obj.instrument, "model", "").lower() == "ct25k" ) if is_old_chm_version or is_ct25k: return 20 return 5 def _initialize_ceilo( full_path: str, site_meta: dict, date: str | None = None, ) -> ClCeilo | Ct25k | LufftCeilo | Cl61d | Cs135: if "model" in site_meta: if site_meta["model"] not in ( "cl31", "cl51", "cl61d", "ct25k", "chm15k", "cs135", ): msg = f"Invalid ceilometer model: {site_meta['model']}" raise ValueError(msg) if site_meta["model"] in ("cl31", "cl51"): model = "cl31_or_cl51" else: model = site_meta["model"] else: model = _find_ceilo_model(full_path) if model == "cl31_or_cl51": return ClCeilo(full_path, site_meta, date) if model == "ct25k": return Ct25k(full_path, site_meta, date) if model == "cl61d": return Cl61d(full_path, site_meta, date) if model == "cs135": return Cs135(full_path, site_meta, date) return LufftCeilo(full_path, site_meta, date) def _find_ceilo_model(full_path: str) -> str: model = None try: with netCDF4.Dataset(full_path) as nc: title = nc.title for identifier in ["cl61d", "cl61-d"]: if identifier in title.lower() or identifier in full_path.lower(): model = "cl61d" if model is None: model = "chm15k" except OSError: with open(full_path, "rb") as file: for line in islice(file, 100): if line.startswith(b"\x01CL"): model = "cl31_or_cl51" elif line.startswith(b"\x01CT"): model = "ct25k" if model is None: msg = "Unable to determine ceilometer model" raise RuntimeError(msg) return model ATTRIBUTES = { "depolarisation": MetaData( long_name="Lidar volume linear depolarisation ratio", units="1", comment="SNR-screened lidar volume linear depolarisation ratio at 910.55 nm.", ), "depolarisation_raw": MetaData( long_name="Lidar volume linear depolarisation ratio", units="1", comment="SNR-screened lidar volume linear depolarisation ratio at 910.55 nm.", ), "scale": MetaData(long_name="Scale", units="%", comment="100 (%) is normal."), "software_level": MetaData( long_name="Software level ID", units="1", ), "laser_temperature": MetaData( long_name="Laser temperature", units="C", ), "window_transmission": MetaData( long_name="Window transmission estimate", units="%", ), "laser_energy": MetaData( long_name="Laser pulse energy", units="%", ), "background_light": MetaData( long_name="Background light", units="mV", comment="Measured at internal ADC input.", ), "backscatter_sum": MetaData( long_name="Sum of detected and normalized backscatter", units="sr-1", comment="Multiplied by scaling factor times 1e4.", ), "range_resolution": MetaData( long_name="Range resolution", units="m", ), "number_of_gates": MetaData( long_name="Number of range gates in profile", units="1", ), "unit_id": MetaData( long_name="Ceilometer unit number", units="1", ), "message_number": MetaData( long_name="Message number", units="1", ), "message_subclass": MetaData( long_name="Message subclass number", units="1", ), "detection_status": MetaData( long_name="Detection status", units="1", comment="From the internal software of the instrument.", ), "warning": MetaData( long_name="Warning and Alarm flag", units="1", definition=utils.status_field_definition( { "0": "Self-check OK", "W": "At least one warning on", "A": "At least one error active.", } ), ), "warning_flags": MetaData( long_name="Warning flags", units="1", ), "receiver_sensitivity": MetaData( long_name="Receiver sensitivity", units="%", comment="Expressed as % of nominal factory setting.", ), "window_contamination": MetaData( long_name="Window contamination", units="mV", comment="Measured at internal ADC input.", ), "calibration_factor": MetaData( long_name="Attenuated backscatter calibration factor", units="1", comment="Calibration factor applied.", ), }