Source code for edges.cal.load_data

"""Definition of a class that contains all the data required for a calibration load."""

from collections.abc import Callable
from functools import cached_property

import attrs
import numpy as np
from astropy import units as un
from pygsdata.attrs import npfield

from edges import types as tp
from edges.cal.s11.s11model import S11ModelParams
from edges.io import CalObsDefEDGES2, CalObsDefEDGES3, hickleable

from . import reflection_coefficient as rc
from .s11 import CalibratedS11
from .spectra import LoadSpectrum


[docs] @hickleable @attrs.define(kw_only=True) class Load: """Wrapper class containing all relevant information for a given load. Parameters ---------- spectrum : :class:`LoadSpectrum` The spectrum for this particular load. reflections : :class:`SwitchCorrection` The S11 measurements for this particular load. hot_load_correction : :class:`HotLoadCorrection` If this is a hot load, provide a hot load correction. ambient : :class:`LoadSpectrum` If this is a hot load, need to provide an ambient spectrum to correct it. """ spectrum: LoadSpectrum = attrs.field() s11: CalibratedS11 = attrs.field() _raw_s11: CalibratedS11 | None = attrs.field(default=None) ambient_temperature: tp.TemperatureType = npfield( default=298.0 * un.K, unit=un.K, possible_ndims=( 0, 1, ), ) load_name: str = attrs.field(default="") loss: np.ndarray = npfield(dtype=float, possible_ndims=(1,)) @loss.default def _loss_default(self): """Default loss is a flat 1.0.""" return np.ones(len(self.spectrum.freqs))
[docs] @classmethod def from_caldef( cls, caldef: CalObsDefEDGES2 | CalObsDefEDGES3, load_name: str, ambient_temperature: tp.TemperatureType | None = None, f_low: tp.FreqType = 40 * un.MHz, f_high: tp.FreqType = np.inf * un.MHz, s11_kwargs: dict | None = None, spec_kwargs: dict | None = None, loss_model: Callable | None = None, loss_model_params: S11ModelParams = ( S11ModelParams.from_hot_load_cable_defaults() ), restrict_s11_freqs: bool = False, ): """ Define a full :class:`Load` from a path and name. Parameters ---------- path : str or Path Path to the top-level calibration observation. load_name : str Name of a load to define. f_low, f_high : float Min/max frequencies to keep in measurements. reflection_kwargs : dict Extra arguments to pass through to :class:`SwitchCorrection`. spec_kwargs : dict Extra arguments to pass through to :class:`LoadSpectrum`. ambient_temperature The ambient temperature to use for the loss, if required (required for new hot loads). By default, read an ambient load's actual temperature reading from the io object. Returns ------- load : :class:`Load` The load object, containing all info about spectra and S11's for that load. """ if not spec_kwargs: spec_kwargs = {} if not s11_kwargs: s11_kwargs = {} # For the LoadSpectrum, we can specify both f_low/f_high and f_range_keep. # The first pair is what defines what gets read in and smoothed/averaged. # The second pair then selects a part of this range to keep for doing # calibration with. if "f_low" not in spec_kwargs: spec_kwargs["f_low"] = f_low if "f_high" not in spec_kwargs: spec_kwargs["f_high"] = f_high loaddef = getattr(caldef, load_name) spec = LoadSpectrum.from_loaddef( loaddef=loaddef, f_range_keep=(f_low, f_high), **spec_kwargs, ) # Fill up kwargs with keywords from this instance # TODO: here we only use the calkit defined for the FIRST switching_state, # instead of using each calkit for each switching_state. To fix this, we require # having meta information inside the S11/ directory. s11_kwargs["f_low"] = f_low if restrict_s11_freqs else 0 * un.MHz s11_kwargs["f_high"] = f_high if restrict_s11_freqs else np.inf * un.MHz s11_model_params = s11_kwargs.pop( "model_params", S11ModelParams.from_calibration_load_defaults(name=load_name), ) if isinstance(caldef, CalObsDefEDGES2): if "internal_switch_kwargs" not in s11_kwargs: s11_kwargs["internal_switch_kwargs"] = {} if "calkit" not in s11_kwargs["internal_switch_kwargs"]: s11_kwargs["internal_switch_kwargs"]["calkit"] = rc.get_calkit( rc.AGILENT_85033E, resistance_of_match=caldef.male_resistance ) raw_s11 = CalibratedS11.from_edges2_loaddef( caldef, load=load_name, **s11_kwargs ) elif isinstance(caldef, CalObsDefEDGES3): if "calkit" not in s11_kwargs: s11_kwargs["calkit"] = rc.AGILENT_ALAN raw_s11 = CalibratedS11.from_edges3_loaddef( caldef, load=load_name, **s11_kwargs ) # Now, model the S11 s11 = raw_s11.smoothed(s11_model_params, freqs=spec.freqs) if loss_model is not None: if ( hasattr(loss_model, "sparams") and loss_model.sparams.freqs.size != spec.freqs.size ): loss_model = attrs.evolve( loss_model, sparams=loss_model.sparams.smoothed( params=loss_model_params, freqs=spec.freqs ), ) loss = loss_model(spec.freqs, s11.s11) else: loss = np.ones(spec.freqs.shape) return cls( spectrum=spec, raw_s11=raw_s11, s11=s11, loss=loss, ambient_temperature=ambient_temperature, load_name=load_name, )
[docs] def get_temp_with_loss(self): """Calculate the temperature of the load accounting for loss.""" gain = self.loss return gain * self.spectrum.temp_ave + (1 - gain) * self.ambient_temperature
@cached_property def temp_ave(self) -> np.ndarray: """The average temperature of the thermistor (over frequency and time).""" return self.get_temp_with_loss() @property def averaged_q(self) -> np.ndarray: """The average spectrum power ratio, Q (over time).""" return self.spectrum.q.data.squeeze() @property def freqs(self) -> tp.FreqType: """Frequencies of the spectrum.""" return self.spectrum.q.freqs