Source code for edges.cal.loss

"""Models for loss through cables."""

from collections.abc import Sequence

import attrs
import numpy as np
from numpy import typing as npt
from scipy.interpolate import InterpolatedUnivariateSpline as Spline

from . import sparams as sp


[docs] def compute_cable_loss_from_scattering_params( input_s11: sp.ReflectionCoefficient, sparams: sp.SParams ) -> npt.NDArray[float]: """Compute loss from a cable, given the S-params of the cable, and input S11. This function operates in the context of a cable (or balun, etc.) that is attached to an input (generally the antenna, or a calibration load). It computes the loss due to the cable, given the scattering matrix of the cable itself, and the S11 of the input antenna/load. The equations here are described in MIT EDGES Memo #132: https://www.haystack.mit.edu/wp-content/uploads/2020/07/memo_EDGES_132.pdf. We use two of the equations, to be specific: the equation for Gamma_a (the antenna reflection coefficient at the input of the cable), and the equation for the loss, L. SGM: as far as I can tell, this function *doesn't* assume that S12 == S21, though actual calls to this function generally throughout our calibration do make this assumption. """ s12 = sparams.s12 s21 = sparams.s21 s22 = sparams.s22 T = input_s11.de_embed(sparams).reflection_coefficient return ( np.abs(s12 * s21) * (1 - np.abs(T) ** 2) / ( (1 - np.abs(input_s11.reflection_coefficient) ** 2) * np.abs(1 - s22 * T) ** 2 ) )
[docs] def get_cable_loss_model( cable: sp.CoaxialCable | str | Sequence[sp.CoaxialCable | str], ) -> callable: """Return a callable loss model for a particular cable or series of cables. The returned function is suitable for passing to a :class:`Load` as the loss_model. You can pass a single cable (i.e. a :class:`edges.cal.ee.CoaxialCable`) or a list of such cables, each of which is assumed to be joined in a cascade. Each should be equipped with a cable length. Parameters ---------- cable Either a string, or a CoaxialCable instance, or a list of such. If a string, it should be a name present in `ee.KNOWN_CABLES`. """ if isinstance(cable, sp.CoaxialCable | str): cable = [cable] cable = [c if isinstance(c, sp.CoaxialCable) else sp.KNOWN_CABLES[c] for c in cable] def loss_model(s11a: sp.ReflectionCoefficient) -> npt.NDArray: s0 = cable[0].scattering_parameters(s11a.freqs) if len(cable) > 1: for cbl in cable[1:]: ss = cbl.scattering_parameters(s11a.freqs) s0 = s0.cascade_with(ss) return compute_cable_loss_from_scattering_params(s11a, s0) return loss_model
[docs] def get_loss_model_from_file(fname): """Simply read a loss model directly from a file. The file must have two columns separated by whitespace. The first is frequency in MHz and the second should be the loss. """ with fname.open("r") as fl: data = np.genfromtxt(fl) spl = Spline(data[:, 0], data[:, 1]) return lambda s11a: spl(s11a.freqs)
[docs] @attrs.define(slots=False, frozen=True) class LossFunctionGivenSparams: """ A callable that satisfies the signature for a loss function, from given sparams. Measurements required to define the HotLoad temperature, from Monsalve et al. (2017), Eq. 8+9. """ sparams: sp.SParams = attrs.field( validator=attrs.validators.instance_of(sp.SParams) ) def __call__(self, gamma: sp.ReflectionCoefficient) -> np.ndarray: """ Calculate the power gain. Parameters ---------- freq : np.ndarray The frequencies. hot_load_s11 : array The S11 of the hot load. Returns ------- gain : np.ndarray The power gain as a function of frequency. """ if self.sparams.freqs.size != len(gamma.freqs): raise ValueError( "Given gamma doesn't have the same length as the S-params." ) return compute_cable_loss_from_scattering_params(gamma, self.sparams)