"""Functions defining expected losses from the instruments."""
from __future__ import annotations
import numpy as np
from edges_cal import reflection_coefficient as rc
from pathlib import Path
from scipy import integrate
from ..config import config
[docs]
def balun_and_connector_loss(
band: str,
freq,
gamma_ant,
monte_carlo_flags=(False, False, False, False, False, False, False, False),
):
"""
Compute balun and connector losses.
Parameters
----------
band : str {'low3', 'mid'}
Parameters of the loss are different for each antenna.
freq : array-like
Frequency in MHz
gamma_ant: float
Reflection coefficient of antenna at the reference plane, the LNA input.
monte_carlo_flags : tuple of bool
Which parameters to add a random offset to, in order:
* tube_inner_radius
* tube_outer_radius
* tube_length
* connector_inner_radius
* connector_outer_radius
* connector_length
* metal_conductivity
* teflon_permittivity
Returns
-------
Gb : float or array-like
The balun loss
Gc : float or array-like
The connector loss
"""
# Angular frequency
w = 2 * np.pi * freq * 1e6
# Inch-to-meters conversion
inch2m = 1 / 39.370
# Conductivity of copper
# Pozar 3rd edition. Alan uses a different number. What
sigma_copper0 = 5.96 * 10**7
# Metal conductivity
sigma_copper = 1 * sigma_copper0
sigma_brass = 0.29 * sigma_copper0 # 0.24
sigma_xx_inner = 0.24 * sigma_copper0
sigma_xx_outer = 0.024 * sigma_copper0
# Permeability
u0 = (
4 * np.pi * 10 ** (-7)
) # permeability of free space (same for copper, brass, etc., all nonmagnetic
ur_air = 1 # relative permeability of air
u_air = u0 * ur_air
# Permittivity
c = 299792458 # speed of light
e0 = 1 / (u0 * c**2) # permittivity of free space
parameters = {
"low": {
"balun_length": 43.6 * inch2m,
"connector_length": 1.1811023622 * inch2m,
"er_air": 1.07,
"ric_b": ((5 / 16) * inch2m) / 2,
"roc_b": ((3 / 4) * inch2m) / 2,
"roc_c": (0.161 * inch2m) / 2,
"ric_c": (0.05 * inch2m) / 2,
},
"mid": {
"balun_length": 35 * inch2m,
"connector_length": 0.03,
"er_air": 1.2,
"ric_b": ((16 / 32) * inch2m) / 2,
"roc_b": ((1.25) * inch2m) / 2,
"roc_c": (0.161 * inch2m) / 2,
"ric_c": (0.05 * inch2m) / 2,
},
}
ep_air = e0 * parameters[band]["er_air"]
tan_delta_air = 0
epp_air = ep_air * tan_delta_air
er_teflon = 2.05 # why Alan????
ep_teflon = e0 * er_teflon
# http://www.kayelaby.npl.co.uk/general_physics/2_6/2_6_5.html
tan_delta_teflon = 0.0002
epp_teflon = ep_teflon * tan_delta_teflon
ur_teflon = 1 # relative permeability of teflon
u_teflon = u0 * ur_teflon
ric_b = parameters[band]["ric_b"]
if monte_carlo_flags[0]:
# 1-sigma of 3%
ric_b *= 1 + 0.03 * np.random.normal()
roc_b = parameters[band]["roc_b"]
if monte_carlo_flags[1]:
# 1-sigma of 3%
roc_b *= 1 + 0.03 * np.random.normal()
l_b = parameters[band]["balun_length"] # length in meters
if monte_carlo_flags[2]:
l_b += 0.001 * np.random.normal() # 1-sigma of 1 mm
# Connector dimensions
ric_c = parameters[band]["ric_c"] # radius of outer wall of inner conductor
if monte_carlo_flags[3]:
# 1-sigma of 3%, about < 0.04 mm
ric_c *= 1 + 0.03 * np.random.normal()
roc_c = parameters[band]["roc_c"]
if monte_carlo_flags[4]:
# 1-sigma of 3%
roc_c *= 1 + 0.03 * np.random.normal()
l_c = parameters[band]["connector_length"]
if monte_carlo_flags[5]:
l_c += 0.0001 * np.random.normal()
if monte_carlo_flags[6]:
sigma_copper *= 1 + 0.01 * np.random.normal()
sigma_brass *= 1 + 0.01 * np.random.normal()
sigma_xx_inner *= 1 + 0.01 * np.random.normal()
sigma_xx_outer *= 1 + 0.01 * np.random.normal()
if monte_carlo_flags[7] == 1:
# 1-sigma of 1%
epp_teflon *= 1 + 0.01 * np.random.normal()
# Skin Depth
skin_depth_copper = np.sqrt(2 / (w * u0 * sigma_copper))
skin_depth_brass = np.sqrt(2 / (w * u0 * sigma_brass))
skin_depth_xx_inner = np.sqrt(2 / (w * u0 * sigma_xx_inner))
skin_depth_xx_outer = np.sqrt(2 / (w * u0 * sigma_xx_outer))
# Surface resistance
Rs_copper = 1 / (sigma_copper * skin_depth_copper)
Rs_brass = 1 / (sigma_brass * skin_depth_brass)
Rs_xx_inner = 1 / (sigma_xx_inner * skin_depth_xx_inner)
Rs_xx_outer = 1 / (sigma_xx_outer * skin_depth_xx_outer)
def get_induc_cap_res_cond_prop(
ric, roc, skin_depth_inner, skin_depth_outer, rs_inner, rs_outer, u, ep, epp
):
L_inner = u0 * skin_depth_inner / (4 * np.pi * ric)
L_dielec = (u / (2 * np.pi)) * np.log(roc / ric)
L_outer = u0 * skin_depth_outer / (4 * np.pi * roc)
L = L_inner + L_dielec + L_outer
C = 2 * np.pi * ep / np.log(roc / ric)
R = (rs_inner / (2 * np.pi * ric)) + (rs_outer / (2 * np.pi * roc))
G = 2 * np.pi * w * epp / np.log(roc / ric)
return (
np.sqrt((R + 1j * w * L) * (G + 1j * w * C)),
np.sqrt((R + 1j * w * L) / (G + 1j * w * C)),
)
# Inductance per unit length
gamma_b, Zchar_b = get_induc_cap_res_cond_prop(
ric_b,
roc_b,
skin_depth_copper,
skin_depth_brass,
Rs_copper,
Rs_brass,
u_air,
ep_air,
epp_air,
)
gamma_c, Zchar_c = get_induc_cap_res_cond_prop(
ric_c,
roc_c,
skin_depth_xx_inner,
skin_depth_xx_outer,
Rs_xx_inner,
Rs_xx_outer,
u_teflon,
ep_teflon,
epp_teflon,
)
# Impedance of Agilent terminations
Zref = 50
Ropen, Rshort, Rmatch = rc.agilent_85033E(freq * 1e6, Zref, 1)
def get_gamma(r):
Z = rc.gamma2impedance(r, Zref)
Zin_b = rc.input_impedance_transmission_line(Zchar_b, gamma_b, l_b, Z)
Zin_c = rc.input_impedance_transmission_line(Zchar_c, gamma_c, l_c, Z)
Rin_b = rc.impedance2gamma(Zin_b, Zref)
Rin_c = rc.impedance2gamma(Zin_c, Zref)
return Rin_b, Rin_c
Rin_b_open, Rin_c_open = get_gamma(Ropen)
Rin_b_short, Rin_c_short = get_gamma(Rshort)
Rin_b_match, Rin_c_match = get_gamma(Rmatch)
# S-parameters (it has to be done in this order, first the Connector+Bend, then the
# Balun)
ra_c, S11c, S12S21c, S22c = rc.de_embed(
Ropen, Rshort, Rmatch, Rin_c_open, Rin_c_short, Rin_c_match, gamma_ant
)
# Reflection of antenna only, at the input of bend+connector
ra_b, S11b, S12S21b, S22b = rc.de_embed(
Ropen, Rshort, Rmatch, Rin_b_open, Rin_b_short, Rin_b_match, ra_c
)
def get_g(S11_rev, S12S21, ra_x, ra_y):
return (
np.abs(S12S21)
* (1 - np.abs(ra_x) ** 2)
/ ((np.abs(1 - S11_rev * ra_x)) ** 2 * (1 - (np.abs(ra_y)) ** 2))
)
Gb = get_g(S22b, S12S21b, ra_b, ra_c)
Gc = get_g(S22c, S12S21c, ra_c, gamma_ant)
return Gb, Gc
def _get_loss(fname: str | Path, freq: np.ndarray, n_terms: int) -> np.ndarray:
gr = np.genfromtxt(fname)
fr = gr[:, 0]
dr = gr[:, 1]
par = np.polyfit(fr, dr, n_terms)
model = np.polyval(par, freq)
return 1 - model
[docs]
def ground_loss_from_beam(beam, deg_step: float) -> np.ndarray:
"""
Calculate ground loss from a given beam instance.
Parameters
----------
beam : Beam instance
The beam to use for the calculation.
deg_step : float
Frequency in MHz. For mid-band (low-band), between 50 and 150 (120) MHz.
Returns
-------
gain: array of the gain values
"""
p_in = np.zeros_like(beam.beam)
gain_t = np.zeros((np.shape(beam.beam)[0], np.shape(beam.beam)[2]))
gain = np.zeros(np.shape(beam.beam)[0])
for k in range(np.shape(beam.frequency)[0]):
p_in[k] = (
np.sin((90 - np.transpose([beam.elevation] * 360)) * deg_step * np.pi / 180)
* beam.beam[k]
)
gain_t[k] = integrate.trapz(p_in[k], dx=deg_step * np.pi / 180, axis=0)
gain[k] = integrate.trapz(gain_t[k], dx=deg_step * np.pi / 180, axis=0)
gain[k] = gain[k] / (4 * np.pi)
return gain
[docs]
def ground_loss(
filename: str | Path,
freq: np.ndarray,
beam=None,
deg_step: float = 1.0,
band: str | None = None,
configuration: str = "",
) -> np.ndarray:
"""
Calculate ground loss of a particular antenna at given frequencies.
Parameters
----------
filename : path
File in which value of the ground loss for this instrument are tabulated.
freq : array-like
Frequency in MHz. For mid-band (low-band), between 50 and 150 (120) MHz.
beam
A :class:`Beam` instance with which the ground loss may be computed.
deg_step
The steps (in degrees) of the azimuth angle in the beam (if given).
band : str, optional
The instrument to find the ground loss for. Only required if `filename`
doesn't exist and isn't an absolute path (in which case the standard directory
structure will be searched using ``band``).
configuration : str, optional
The configuration of the instrument. A string, such as "45deg", which defines
the orientation or other configuration parameters of the instrument, which may
affect the ground loss.
"""
if beam is not None:
return ground_loss_from_beam(beam, deg_step)
elif str(filename).startswith(":"):
if band is None:
raise ValueError(
f"For non-absolute path {filename}, you must provide 'band'."
)
if str(filename) == ":":
# Use the built-in loss files
fl = "ground"
if configuration:
fl += "_" + configuration
filename = Path(__file__).parent / "data" / "loss" / band / (fl + ".txt")
if not filename.exists():
return np.ones_like(freq)
else:
# Find the file in the standard directory structure
filename = (
Path(config["paths"]["antenna"]) / band / "loss" / str(filename)[1:]
)
return _get_loss(str(filename), freq, 8)
else:
filename = Path(filename)
return _get_loss(str(filename), freq, 8)
[docs]
def antenna_loss(
filename: [str, Path, bool],
freq: [np.ndarray],
band: [None, str] = None,
configuration: [str] = "",
):
"""
Calculate antenna loss of a particular antenna at given frequencies.
Parameters
----------
filename : path
File in which value of the antenna loss for this instrument are tabulated.
freq : array-like
Frequency in MHz. For mid-band (low-band), between 50 and 150 (120) MHz.
band : str, optional
The instrument to find the antenna loss for. Only required if `filename`
starts with the magic ':' (in which case the standard directory
structure will be searched using ``band``).
configuration : str, optional
The configuration of the instrument. A string, such as "45deg", which defines
the orientation or other configuration parameters of the instrument, which may
affect the antenna loss.
"""
if str(filename).startswith(":"):
if str(filename) == ":":
# Use the built-in loss files
fl = "antenna"
if configuration:
fl += "_" + configuration
filename = Path(__file__).parent / "data" / "loss" / band / (fl + ".txt")
if not filename.exists():
return np.zeros_like(freq)
else:
# Find the file in the standard directory structure
filename = (
Path(config["paths"]["antenna"]) / band / "loss" / str(filename)[1:]
)
else:
filename = Path(filename)
return _get_loss(str(filename), freq, 11)