Python Bindings#

The cal-py crate provides a thin PyO3 surface for calling the calibration engine from Python. It contains zero logic – all work is dispatched to cal-core and cal-io.

Module: _cal_rs#

Build with maturin:

cd crates/cal-py
maturin develop              # debug
maturin build --release      # release wheel

Exported Functions#

Function

Description

version()

Returns the crate version string

list_scans(store_path)

List scan numbers in an L0 Zarr store

calibrate_store(input, output, atm, config?, foeff?, gain_image?)

Calibrate all scans in a store (vectorized path)

calibrate_scan_py(input, output, atm, scan, config?, foeff?, gain_image?)

Calibrate a single scan (Dask-friendly, vectorized path)

convert_fits_folder(fits_folder, zarr_path, skip?) *

Convert FITS folder to Zarr (* requires fits-ingest)

Calibration Path#

Both calibrate_store and calibrate_scan_py use the vectorized path (calibrate_full), the same single calibration function used by the CLI. There is no per-pixel loop or separate per-pixel path in cal-py. Multi-pixel data is handled by resolve_calibration_load_full which computes all pixels as [C, R, A] arrays simultaneously.

Configuration Dict#

The config parameter accepts a Python dict:

config = {
    "obs_mode": "otf",              # auto, totalpower, otf
    "pwv": 1.2,                     # fixed PWV in mm (omit to fit)
    "physics": "kalibrate_compat",  # exact or kalibrate_compat
    "clip_tsys": 200.0,             # bad channel threshold (K)
    "clip_counts": 0.01,            # min hot-cold ratio
    "eta_fss": 1.0,                 # forward scattering efficiency
    "eta_mb": 1.0,                  # main beam efficiency
    "n_threads": 4,                 # Rayon thread count
}

The foeff and gain_image parameters are passed as separate keyword arguments (default 0.97 and 0.5) and are bundled into a CalibrationParams struct internally.

Dask Integration#

For distributed processing, use calibrate_scan_py as a Dask delayed function:

import dask

tasks = [
    dask.delayed(_cal_rs.calibrate_scan_py)(
        input_path, output_path, atm_table_path, scan_num
    )
    for scan_num in scan_numbers
]
results = dask.compute(*tasks)

Each call processes one scan independently, making this suitable for dask.distributed or multiprocessing.

Design: Zero Logic in cal-py#

cal-py is intentionally thin:

  • No calibration math

  • No data manipulation

  • No error recovery logic

  • All work dispatched to cal-core and cal-io

  • The internal calibrate_one_scan() helper performs the full vectorized pipeline (resolve -> prepare -> atmosphere -> calibrate -> write) using the same code paths as the CLI

This keeps the Python surface stable while the Rust internals evolve.