The Python package calibration provides Rayleigh (molecular) and liquid-cloud calibration of ceilometers and lidars used in the MeteoSwiss / E-PROFILE network. Includes water-vapour correction for 910 nm instruments, six selectable molecular-window detection methods, and a suite of analysis / validation scripts for the E-PROFILE ALC paper.
- Multi-instrument support: CHM15k, CHM8k, CL51, CL61, Mini-MPL, and more
- Water-vapour correction (mandatory for 910 nm): spectral two-way WV absorption from CAMS monthly means + HITRAN cross-section LUT; nights without matching CAMS are excluded (flag −4), never silently skipped
- Six molecular-window detection methods (selectable via
molecular_methodin options.json):main= E-PROF v1.1 — original E-PROFILE methodimproved= E-PROF v1.2 — aerosol-robust grid search (production default)optimal= E-PROF v2 — temporal aerosol rejection + layer flaggingcalipso,earlinet,bellini— alternative reference methods
- Liquid-cloud calibration (
cloud/): Python port of MATLAB O'Connor method, bit-for-bit validated; reads E-PROFILE L2 or Cloudnet CL61 raw; mandatory WV correction - Kalman smoothing bridge: shells out to
run_kalman_from_matlab.pyto produce a smoothed daily lidar constant - Three input data levels: L1 (
rcs_0), L2 daily, L2 monthly (see Input data level) - CF-compliant output: CSV (
<WMO>_<id>_cl.csv) + optional NetCDF4
apply_wv_correctionmust stay = 1 inoptions.json(production). Never calibrate 910 nm without a valid matching-month CAMS file — no fallback.molecular_methoddefaults to"improved"(E-PROF v1.2) in production. Do not change without re-running the long-run validation.molecular_sourcestays"standard"(US Standard Atmosphere 1976) for production.
This project is managed with uv (PEP 621 / hatchling). From the repo root:
uv pip install -e ".[plotting]" # editable install incl. optional plot deps
# or set up a fully managed environment: uv syncPlain pip works too if you prefer: pip install -e ".[plotting]".
Required: Python ≥ 3.9, NumPy, SciPy, netCDF4, pandas
Optional: matplotlib (plots), scipy.ndimage (smoothing)
from calibration import calibrate_rayleigh, CalibrationOptions, InstrumentInfo
from calibration.config import InstrumentType
options = CalibrationOptions.from_json("options.json")
info = InstrumentInfo(site_name="Payerne", wmo_id="0-20000-0-06610",
identifier="A", instrument_type=InstrumentType.CHM15k,
latitude=46.82, longitude=6.95, altitude=491)
calibrate_rayleigh("20240115", info, options)| Field | Default | Description |
|---|---|---|
molecular_method |
"improved" |
Molecular-window detection method (see Methods section) |
apply_wv_correction |
1 |
Must stay 1 — WV correction for 910 nm |
molecular_source |
"standard" |
Atmospheric profile source ("standard" or "cams") |
cams_folder |
"D:/CAMS/" |
Path to CAMS monthly NetCDF files |
abs_cs_lookup_table |
(path) | HITRAN WV cross-section LUT |
data_level |
"L1" |
Input data level (L1, L2_daily, L2_monthly) |
folder_root |
— | Root of input data archive |
hour_min / hour_max |
20 / 4 | Nighttime window (UTC) |
z_low_cloud |
4000 | Max cloud height for clear-night selection (m) |
LRaer |
52 | Aerosol lidar ratio assumed in molecular window (sr) |
threshold_quality |
15 | Max rel. error (%) to accept a calibration |
data_level selects which product is read; folder_root must point to the matching archive:
data_level |
Variable | Layout | Example root |
|---|---|---|---|
L1 (default) |
rcs_0 |
<WMO>/YYYY/MM/L1_<WMO>_<id><YYYYMMDD>.nc |
D:/E-PROFILE_L1 |
L2_daily |
attenuated_backscatter_0 + calibration_constant_0 |
<WMO>/YYYY/MM/L2_<WMO>_<id><YYYYMMDD>.nc |
D:/E-PROFILE_L2_2021-2025 |
L2_monthly |
attenuated_backscatter_0 + calibration_constant_0 |
<WMO>/YYYY/L2_<WMO>_<id><YYYYMM>.nc |
A:/E-PROFILE_L2_monthly |
RAW |
instrument-native (CL61 beta_att, CHM beta_raw) |
<WMO>/YYYYMMDD/*.nc or <WMO>/YYYYMMDD.nc |
R:/CL61/RAW_cloudnet_dl |
For L2 levels: rcs = attenuated_backscatter_0 × calibration_constant_0 × 1e-6. The 1e-6 matches the MATLAB reference; stored β_att is always micro-scaled (1E-6/(m·sr)) regardless of the units attribute. Height variable can be range, height, or altitude (ASL — subtracted from station_altitude to get AGL).
| Key | Label | Description |
|---|---|---|
main |
E-PROF v1.1 | Original E-PROFILE grid-search method |
improved |
E-PROF v1.2 | Aerosol-robust grid search; production default |
optimal |
E-PROF v2 | Temporal aerosol rejection + per-layer flagging; highest precision, lower yield |
calipso |
CALIPSO-type | Fixed high-altitude molecular window |
earlinet |
EARLINET/SCC-type | SCC-style window search |
bellini |
Bellini/ALICENET | ALICENET gradient-based window |
eprof_v10 |
E-PROF v1.0 | Pre-a4e7140 baseline (sign-error in Rayleigh slope); for historical comparison only — not selectable in production |
METHODS_DISPLAY in validation/compare_molecular_methods.py is the ordered tuple used for display/aggregation (includes eprof_v10). METHODS in calibration/rayleigh/molecular_methods.py lists only the live-selectable methods.
Long-run results (14 sites, full archive): optimal is most precise (robust CV 10–14 %); improved best balances precision and yield; main has the highest yield (57 %) but noisiest nights; calipso highest yield (79 %) but weakest signal-selectivity. See the reports in doc/reports/ (figures under C:\DATA\Projects\202606_E-PROFILE_calibration\figs_paper_validation\molecular_methods_longrun\).
- Data loading: L1
rcs_0, or L2 reconstructed, or RAW instrument signal - Fog exclusion: profiles with
vertical_visibilityset → excluded (ceilometer fog flag) - Nighttime selection: 20:00–04:00 UTC (configurable)
- Clear-night filtering: remove profiles with low clouds or precipitation
- Water-vapour correction (910 nm only): compute two-way T²_wv(r) from CAMS monthly q/T/lnsp + HITRAN LUT (
water_vapor_correction/water_vapor.py); divide RCS; skip night if no CAMS (flag −4) - Atmospheric model: US Standard Atmosphere 1976 (or CAMS for molecular density)
- Molecular calculation: Bucholtz (1995) β_mol(λ, z)
- Molecular window detection: selected method (see Methods section)
- Rayleigh fit: linear regression signal vs β_mol in the window
- Validation: slope method vs Klett cross-check
- Output: append to
<WMO>_<id>_cl.csv(date, flag, lidar_constant, uncertainty, …)
| Flag | Meaning |
|---|---|
| 1 | Successful calibration |
| 0.5 | Partially clear night (some clouds removed) |
| 0 | No data available |
| −1 | Not a clear night |
| −2 | Signal not proportional to molecular scattering (fog night) |
| −3 | Poor agreement between calibration methods |
| −4 | Missing WV data (CAMS); 910 nm night skipped — no fallback |
| −5 | RCS contains only NaN values |
| −6 | Uncertainty exceeds calibration value |
| −7 | Negative Rayleigh fit slope |
| −8 | Rayleigh fit intercept exceeds slope |
calibration/ installable package (public API unchanged)
├── config.py plotting.py main.py shared config, plotting, CLI entry point
├── rayleigh/ molecular calibration: calibration.py, rayleigh_fit.py,
│ atmosphere.py, molecular_methods.py
├── io/ data_loader.py (L1/L2/RAW readers) + output.py (CSV/NetCDF)
├── water_vapor_correction/ water_vapor.py (spectral two-way WV transmission)
├── cloud/ cloud/calibration.py (liquid-cloud O'Connor method)
└── data/ standard_atmosphere_US_1976_50km.csv (package data)
scripts/ runnable calibration runners + data/ (acquisition) + doc_tools/
validation/ comparison / sensitivity / reproduction studies (validation/README.md)
examples/ Jupyter quick-start notebooks (examples/README.md)
tests/ pytest suite
doc/ reference papers, method descriptions, reports/ (doc/README.md)
lost_and_found/ one-off R&D scratch + old logs (unmaintained)
options.json and instruments.json stay at the repo root (read from the working
directory — run scripts from the repo root). Figures and result archives are written
outside the repo to C:\DATA\Projects\202606_E-PROFILE_calibration.
| Module | Purpose |
|---|---|
rayleigh/calibration.py |
Main calibration pipeline; calls WV correction, molecular calc, window detection |
rayleigh/molecular_methods.py |
Six molecular-window detection methods |
rayleigh/atmosphere.py |
Standard-atmosphere loader + Bucholtz molecular properties |
rayleigh/rayleigh_fit.py |
Molecular-window grid search + lidar-constant fit |
water_vapor_correction/water_vapor.py |
WV two-way transmission from CAMS; port of MATLAB wv_t2eff + compute_wv_transmission |
cloud/calibration.py |
Liquid-cloud calibration (O'Connor method); bit-for-bit vs MATLAB |
io/data_loader.py |
Multi-level reader (L1/L2/RAW/Cloudnet CL61); fog-flag threading |
io/output.py |
CF-compliant CSV + NetCDF writers |
The public API is re-exported from the top level — from calibration import calibrate_rayleigh, CalibrationOptions, InstrumentInfo, ... is unchanged.
| Location | Contents |
|---|---|
scripts/ |
Calibration runners: run_all_l2monthly.py, run_calibration.py, run_targeted_rayleigh.py, calibrate_cloudnet_cl61.py, run_lindenberg_cl61_cal.py |
scripts/data/ |
Data acquisition & prep: Cloudnet downloads, build_l2_manifest.py, build_cop_lookup.py, converters |
validation/ |
Method comparison, MATLAB↔Python parity, long-run & sensitivity studies (see validation/README.md) |
examples/ |
Quick-start notebooks (see examples/README.md) |
| File | Coverage |
|---|---|
test_water_vapor.py |
WV vs MATLAB refs + atmoslib; validates geopotential, level-order invariance |
test_cloud_calibration_vs_matlab.py |
Cloud calib bit-for-bit vs MATLAB (max rel diff 1.4e-5) |
test_calibration.py, test_methods_smoke.py |
Config / atmosphere unit tests; molecular-methods smoke test |
Primary output: CSV at C:\DATA\Projects\202606_E-PROFILE_calibration\E-PROFILE_calibration_rayleigh\fullcal_all\<WMO>_<id>\<WMO>_<id>_cl.csv (the output root is configured per script and via options.folder_output)
| Column | Description |
|---|---|
date |
Night date (YYYYMMDD) |
flag |
Success flag (1 = ok, 0.5 = partial, negative = failure) |
lidar_constant |
Daily lidar constant C_L |
uncertainty |
Calibration uncertainty (same units as C_L) |
bottom_height |
Bottom of molecular window (m AGL) |
top_height |
Top of molecular window (m AGL) |
message |
Human-readable status message |
- Bucholtz, A. (1995). Rayleigh-scattering calculations for the terrestrial atmosphere. Applied Optics, 34(15), 2765–2773.
- Wiegner, M., & Geiß, A. (2012). Aerosol profiling with the Jenoptik ceilometer CHM15kx. AMT, 5(8), 1953–1964.
- Wiegner, M., & Gasteiger, J. (2015). Correction of water vapor absorption for aerosol remote sensing with ceilometers. AMT, 8(9), 3971–3984. [Spectral WV correction basis]
- Hopkin, E., et al. (2019). A robust automated technique for operational calibration of ceilometers using the integrated backscatter from overcast stratocumulus. AMT, 12(7), 4131–4147. [Cloud calibration O'Connor method]
- E-PROFILE Programme: https://e-profile.eu
E-PROFILE aerosol-processing recommendation, applied to the OmB diagnostic (calibration/omb/):
- 5-min pre-average of the calibrated attenuated backscatter (reproduces the L2 cadence), then a top-2 km SNR screen (drop gates with signal <
snr_min·σ; σ = robust 1.4826·MAD of the de-range-corrected signalβ/r²over the topnoise_top_m= 2 km — the instrument noise floor, C_L-invariant), then a morphological opening of the 2-D (5min-time × range) keep mask (scipy.ndimage.binary_opening) to delete isolated/transient speckle. The opening removes the one-sided cut's positive-noise tail, which otherwise biased O−B high and growing with altitude. - CAMS matching is a ±15 min window centred on each CAMS step (was forward 3 h).
- Period statistics keep altitudes valid in ≥
valid_frac_min= 25 % of CAMS steps; interpolation onto the CAMS levels is gap-capped (interp_max_gap_m= 300 m) so large flagged gaps are not bridged →valid_fracis honest and drawn on a second x-axis of the median-profile panel. - Vaisala fog/precip: a non-NaN
vertical_visibility(CL31/CL51/CL61 report it instead of a cloud base under fog/strong precipitation) is treated as a low cloud base inio/l1_window.py(np.fminwith the cloud base); CHM15k (vor) / Mini-MPL unaffected. - The observation pcolor now shows all data with a translucent grey wash where flagged (cloud / SNR / morphology) plus cloud-base markers.
- Files:
omb/omb.py,omb/figures.py,incremental.py,io/l1_window.py. The OmB cache (_omb_cache.npz) schema gainedobsfull__*/cloud_base/full_srcs→ rebuild OmB caches from scratch (cannot append to old caches).
- Switched packaging from Poetry to uv / PEP 621 (
pyproject.toml[project]+ hatchling build backend); removedsetup.py. Install withuv pip install -e ".[plotting]". - Renamed the import package
rayleigh_calibration→calibration(it does both Rayleigh and cloud calibration). Method subpackages are nowrayleigh/andcloud/(the latter wascloud_calibration/; its module iscloud/calibration.py). The distribution and CLI command are noweprofile-calibration. Public imports becomefrom calibration import ….
- Package split into themed subpackages:
rayleigh/(calibration, fit, atmosphere, molecular methods),io/(readers + writers),water_vapor_correction/,cloud/; sharedconfig.py/plotting.py/main.pystay at the package top level. The public API (from calibration import …) is unchanged. - US Standard Atmosphere 1976 table shipped as package data (
calibration/data/);load_standard_atmosphere(None, grid)resolves it regardless of the working directory. - Loose scripts sorted into
scripts/(runners +data/),validation/(studies),examples/(notebooks) andlost_and_found/(R&D scratch + logs); roottest_*.pyconsolidated intotests/. - Figures and result archives moved out of the repo to
C:\DATA\Projects\202606_E-PROFILE_calibration; hardcoded output paths in the scripts updated accordingly.
- WV correction added (
water_vapor.py): spectral two-way T²_wv from CAMS + HITRAN LUT; mandatory for 910 nm (flag −4 on missing CAMS, no fallback). Effect: Payerne CL61 Feb-28 C_L +20 %. - Six molecular methods (
molecular_methods.py):main/improved/optimal/calipso/earlinet/bellini; selectable viamolecular_methodin options.json. - E-PROF versioning:
main→v1.1,improved→v1.2,optimal→v2; historical baselineeprof_v10(pre-sign-error) tracked inMETHODS_DISPLAYfor paper comparison. - Cloud calibration port (
cloud/calibration.py): bit-for-bit vs MATLAB O'Connor; reads E-PROFILE L2 and Cloudnet CL61 RAW; full WV correction included. - Fog exclusion:
vertical_visibilitythreaded through all readers; flagged profiles excluded from Rayleigh + cloud calibration. - RAW data level: native instrument reader for CHM15k β_raw and Cloudnet CL61 β_att day-folders/daily files.
- Cloudnet CL61 pipeline: download → homogenize → calibrate scripts for Lindenberg and Hyytiala (ACTRIS-Cloudnet API).
- Long-run validation: 14 sites (10 CHM15k + 4 Mini-MPL), full archive, 7 methods; results in
figs_paper_validation/molecular_methods_longrun/. - Sign-error fix (commit a4e7140): corrected sign convention in Rayleigh slope; all post-fix CSVs in
fullcal_all/.
- Complete rewrite with AI
- Complete rewrite with modern Python practices; type hints, dataclasses, vectorised calculations.
- Original E-PROFILE implementation by hem.