Source code for edges_analysis.calibration.labcal

"""Module providing routines for calibration of field data."""

from __future__ import annotations

import attr
import numpy as np
from cached_property import cached_property
from edges_cal import CalibrationObservation, Calibrator
from edges_cal import receiver_calibration_func as rcf
from edges_cal import s11
from edges_cal import types as tp
from pathlib import Path
from typing import Callable, Sequence

from .s11 import AntennaS11


[docs] def optional(tp: type) -> Callable: """Define a function that checks if a value is optionally a cetain type.""" return lambda x: None if x is None else tp(x)
[docs] @attr.s(kw_only=True) class LabCalibration: """Lab calibration of field data.""" calobs: Calibrator = attr.ib( converter=lambda x: ( x.to_calibrator() if isinstance(x, CalibrationObservation) else x ) ) _antenna_s11_model: AntennaS11 | Callable = attr.ib()
[docs] @classmethod def from_s11_files( cls, calobs: Calibrator | CalibrationObservation, s11_files: tp.PathLike | Sequence[tp.PathLike], **kwargs, ): """Generate LabCalibration object from files. Parameters ---------- calobs The calibration observation with which to calibrate the receiver. s11_files Either four S1P files that represent the 3 internal standards measurements plus one external match, or a single file in which the S11 has already been calibrated. Other Parameters ---------------- All other parameters are passed to :class:`~s11.AntennaS11`. Includes arguments like ``n_terms``, ``model`` and ``model_delay``. """ if isinstance(calobs, CalibrationObservation): calobs = calobs.to_calibrator() if hasattr(s11_files, "__len__") and len(s11_files) == 4: ant_s11 = AntennaS11.from_s1p_files( files=s11_files, internal_switch=calobs.internal_switch, f_low=calobs.freq.min, f_high=calobs.freq.max, **kwargs, ) else: if not isinstance(s11_files, (str, Path)): s11_files = s11_files[0] ant_s11 = AntennaS11.from_single_file( s11_files, f_low=calobs.freq.min, f_high=calobs.freq.max, internal_switch=calobs.internal_switch, **kwargs, ) return cls( calobs=calobs, antenna_s11_model=ant_s11, )
@property def internal_switch(self) -> s11.InternalSwitch: """The internal switch reflection parameters.""" return self.calobs.internal_switch @property def antenna_s11_model(self) -> Callable[[np.ndarray], np.ndarray]: """Callable S11 model as a function of frequency.""" return ( self._antenna_s11_model.s11_model if isinstance(self._antenna_s11_model, AntennaS11) else self._antenna_s11_model ) @cached_property def antenna_s11(self) -> np.ndarray: """The antenna S11 at the default frequencies.""" return self.antenna_s11_model(self.calobs.freq.freq.to_value("MHz"))
[docs] def get_gamma_coeffs( self, freq: tp.FreqType | None = None, ant_s11: np.ndarray | None = None ): """Get the K-vector for calibration that is dependent on LNA and Antenna S11.""" if freq is None: freq = self.calobs.freq.freq lna = self.calobs.receiver_s11(freq) if ant_s11 is None: ant_s11 = self.antenna_s11_model(freq) return rcf.get_K(lna, ant_s11)
[docs] def get_linear_coefficients( self, freq: np.ndarray | None = None, ant_s11: np.ndarray | None = None ) -> tuple[np.ndarray, np.ndarray]: """Get the linear coeffs that transform uncalibrated to calibrated temp.""" if freq is None: freq = self.calobs.freq.freq coeffs = self.get_gamma_coeffs(freq, ant_s11=ant_s11) a, b = rcf.get_linear_coefficients_from_K( coeffs, self.calobs.C1(freq), self.calobs.C2(freq), self.calobs.Tunc(freq), self.calobs.Tcos(freq), self.calobs.Tsin(freq), t_load=self.calobs.t_load, ) return a, b
[docs] def clone(self, **kwargs): """Create a new instance based off this one.""" return attr.evolve(self, **kwargs)
[docs] def calibrate_q(self, q: np.ndarray, freq: tp.FreqType | None = None) -> np.ndarray: """Convert three-position switch ratio to fully calibrated temperature.""" if freq is None: freq = self.calobs.freq.freq ant_s11 = self.antenna_s11 else: ant_s11 = self.antenna_s11_model(freq) return self.calobs.calibrate_Q(freq, q, ant_s11)
[docs] def calibrate_temp( self, temp: np.ndarray, freq: tp.FreqType | None = None ) -> np.ndarray: """Convert semi-calibrated temperature to fully calibrated temperature.""" if freq is None: freq = self.calobs.freq.freq ant_s11 = self.antenna_s11 else: ant_s11 = self.antenna_s11_model(freq) return self.calobs.calibrate_temp(freq, temp, ant_s11)
[docs] def decalibrate_temp( self, temp: np.ndarray, freq: tp.FreqType | None = None, to_q=False ) -> np.ndarray: """Convert fully-calibrated temp to semi-calibrated temp.""" if freq is None: freq = self.calobs.freq.freq ant_s11 = self.antenna_s11 else: ant_s11 = self.antenna_s11_model(freq) out = self.calobs.decalibrate_temp(freq, temp, ant_s11) return (out - self.calobs.t_load) / self.calobs.t_load_ns if to_q else out
[docs] def with_ant_s11( self, ant_s11: Callable[[np.ndarray], np.ndarray] ) -> LabCalibration: """Clone the instance and add a specific antenna S11 model.""" return attr.evolve(self, antenna_s11_model=ant_s11)