from abc import abstractmethod
import jax.numpy as jnp
from scipy.constants import c, mu_0, epsilon_0
from dataclasses import fields
from pmrf.functions.math import evaluate_bernstein_basis, evaluate_power_basis
from pmrf.functions.conversions import renormalize_s
from pmrf.frequency import Frequency
from pmrf.parameters import Parameter
from pmrf.models.model import Model
[docs]
class RLGCLine(Model):
"""
**Overview**
An abstract base class for a transmission line defined by its per-unit-length
RLGC (Resistance, Inductance, G, Conductance) parameters.
This class provides the fundamental equations to calculate the ABCD-matrix
of a transmission line given its propagation constant (gamma) and
characteristic impedance (Z_c), which are derived from the RLGC values.
Derived classes must implement the `rlgc` method, which defines how the
four distributed parameters (R, L, G, C) behave as a function of frequency.
"""
length: Parameter = 1.0
[docs]
@abstractmethod
def rlgc(self, freq: Frequency) -> tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray, jnp.ndarray]:
"""Calculates the frequency-dependent RLGC parameters of the line.
This method must be implemented by any derived concrete class.
Args:
freq (Frequency): The frequency axis at which to evaluate the parameters.
Returns:
tuple: A tuple containing the R, L, G, and C parameter vectors, in that order.
"""
raise NotImplementedError("'rlgc' must be implemented in the derived class")
[docs]
def a(self, freq: Frequency) -> jnp.ndarray:
"""Calculates the ABCD-matrix from the line's RLGC parameters.
Args:
freq (Frequency): The frequency axis for the calculation.
Returns:
np.ndarray: The resultant ABCD-matrix.
"""
import numpy as np
w = freq.w
R, L, G, C = self.rlgc(freq)
gamma = jnp.sqrt((R + 1j*w*L) * (G + 1j*w*C))
Zc = jnp.sqrt((R + 1j*w*L) / (G + 1j*w*C))
gL = gamma*self.length
a = jnp.array([
[jnp.cosh(gL), Zc * jnp.sinh(gL)],
[1 / Zc * jnp.sinh(gL), jnp.cosh(gL)]
]).transpose(2, 0, 1)
return a
# def s(self, frequency: Frequency) -> jnp.ndarray:
# """Calculates the S-matrix from the line's RLGC parameters.
# Args:
# frequency (Frequency): The frequency axis for the calculation.
# Returns:
# np.ndarray: The resultant ABCD-matrix.
# """
# import numpy as np
# w = frequency.w
# R, L, G, C = self.rlgc(frequency)
# gamma = jnp.sqrt((R + 1j*w*L) * (G + 1j*w*C))
# Zc = jnp.sqrt((R + 1j*w*L) / (G + 1j*w*C))
# gL = gamma*self.length
# s11 = jnp.zeros(frequency.npoints, dtype=complex)
# s21 = jnp.exp(-1*gL)
# s = jnp.array([
# [s11, s21],
# [s21, s11],
# ]).transpose(2, 0, 1)
# return renormalize_s(s, Zc, self.z0)
[docs]
class ConstantRLGCLine(RLGCLine):
"""
**Overview**
An RLGC line with constant, frequency-independent per-unit-length parameters.
This is the simplest transmission line model, where R, L, G, and C
are single scalar values.
**Example**
```python
import pmrf as prf
# Create a lossless 50-ohm line model
# L and C are chosen to produce Z0=50 and epr=2.2
lossless_line = prf.models.ConstantRLGCLine(
L=368.8e-9, # nH/m
C=147.5e-12, # pF/m
length=0.1 # 10 cm
)
# Calculate its S-parameters over a frequency range
freq = prf.Frequency(start=1, stop=5, npoints=101, unit='ghz')
s = lossless_line.s(freq)
print(f"S21 magnitude at center frequency: {abs(s[freq.center_idx, 1, 0]):.3f}")
```
"""
R: Parameter = 0.0
L: Parameter = 280e-9
G: Parameter = 0.0
C: Parameter = 90e-12
[docs]
def rlgc(self, _: Frequency) -> tuple[Parameter, Parameter, Parameter, Parameter]:
"""Returns the constant RLGC parameters.
Args:
_ (Frequency): The frequency axis (ignored).
Returns:
tuple: The constant R, L, G, and C parameters.
"""
return self.R, self.L, self.G, self.C
[docs]
class DatasheetCoaxial(RLGCLine):
"""
**Overview**
A coaxial line defined by parameters typically found on a datasheet.
This model provides a convenient way to define a coaxial cable from its
nominal impedance, dielectric constant, and loss factors, rather than
from the fundamental RLGC values directly. It includes terms for both
skin effect loss (`k1`) and dielectric loss (`k2`).
**Example**
```python
import pmrf as prf
import numpy as np
# Define a 1m coaxial cable from typical datasheet values
cable = prf.models.DatasheetCoaxial(
zn=50.0,
epr=2.1,
k1=0.2, # Skin effect loss factor
k2=0.01, # Dielectric loss factor
length=1.0
)
freq = prf.Frequency(start=0.1, stop=10, npoints=201, unit='ghz')
s = cable.s(freq)
# Plot the insertion loss
# import matplotlib.pyplot as plt
# plt.plot(freq.f_scaled, 20*np.log10(abs(s[:,1,0])))
# plt.xlabel(f"Frequency ({freq.unit})")
# plt.ylabel("Insertion Loss (dB)")
# plt.grid(True)
# plt.show()
```
"""
zn: Parameter = 50.0
epr: Parameter = 1.0
epr_slope: Parameter | None = None
k1: Parameter = 0.0
k2: Parameter = 0.0
loss_coeffs_normalized: bool = False
freq_bounds: tuple | None = None
[docs]
def rlgc(self, freq: Frequency) -> tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray, jnp.ndarray]:
"""Calculates RLGC parameters from datasheet values.
Args:
freq (Frequency): The frequency axis for the calculation.
Returns:
tuple: The calculated R, L, G, and C parameter vectors.
"""
w = freq.w
zn, k1, k2 = self.zn, self.k1, self.k2
if self.freq_bounds is not None:
w_start, w_stop = self.freq_bounds
else:
w_start, w_stop = w[0], w[-1]
wn = (w - w_start) / (w_stop - w_start)
if self.epr_slope is None:
epr = jnp.ones(w.shape[0]) * self.epr
else:
epr = jnp.ones(w.shape[0]) * self.epr + self.epr_slope * wn
if not self.loss_coeffs_normalized:
k1_norm = k1 * (1.0 / (100 * jnp.sqrt(2*jnp.pi * 10**6)))
k2_norm = k2 * (1.0 / (100 * 2*jnp.pi * 10**6))
else:
k1_norm = k1
k2_norm = k2
sqrt_w = jnp.sqrt(w)
dBtoNeper = jnp.log(10) / 20
alpha_c = k1_norm * dBtoNeper * sqrt_w
alpha_d = k2_norm * dBtoNeper * w
sqrt_epr = jnp.sqrt(epr)
R = 2*zn * alpha_c
L = (zn * sqrt_epr) / c
G = 2/zn * alpha_d
C = (sqrt_epr) / (zn * c)
return R, L, G, C
[docs]
class PhysicalCoaxial(RLGCLine):
"""
**Overview**
A coaxial line defined directly by its physical and material properties.
This model calculates the RLGC parameters from the geometry (inner/outer
diameters) and material properties (permittivity, permeability, loss tangent,
resistivity). It provides a high-fidelity model that accounts for the skin
effect in the conductors and frequency-dependent material properties.
Several material properties can be defined as frequency-dependent polynomials
using the `_model` arguments (e.g., `epr_model`). The available models are:
- **'constant'**: (Default) The parameter is a single scalar value.
- **'ppoly'**: The parameter is a polynomial in the power basis. The `Parameter`
value should be a list of coefficients.
- **'bpoly'**: The parameter is a polynomial in the Bernstein basis. The `Parameter`
value should be a list of coefficients.
**Example**
```python
import pmrf as prf
# A coaxial cable where the dielectric constant (epr) varies with frequency
# We model epr as a 1st-order Bernstein polynomial (a line)
# The coefficients are the start and end values of the line.
phys_cable = prf.models.PhysicalCoaxial(
din=0.9e-3,
dout=2.95e-3,
epr=[2.1, 2.05], # epr goes from 2.1 to 2.05 over the freq range
epr_model='bpoly',
tand=0.0004,
rho=1.72e-8, # Copper resistivity
length=0.5
)
freq = prf.Frequency(start=1, stop=20, npoints=101, unit='ghz')
s_phys = phys_cable.s(freq)
```
"""
din: Parameter = 1.12e-3
dout: Parameter = 3.2e-3
epr: Parameter = 1.0
mur: Parameter = 1.0
tand: Parameter = 0.0
rho: Parameter = 1.68e-8
epr_model: str = 'constant'
mur_model: str = 'constant'
tand_model: str = 'constant'
rho_model: str = 'constant'
separate_rho: bool = False
neglect_skin_inductance: bool = False
# def __post_init__(self):
# poly_params = ['epr', 'mur', 'tand', 'rho']
# # If a polynomial model is specified we default to linear, unless {param}_order has been passed
# for param in poly_params:
# model = getattr(self, f'{param}_model')
# value = jnp.array(getattr(self, param))
# if model == 'constant':
# if not jnp.isscalar(value):
# raise Exception("'Constant' coaxial parameters must have a scalar value")
# else:
# order = jnp
# # Set the coefficients if the user has not already
# if jnp.isscalar(value):
# if model == 'bpoly':
# coeffs = [value] * (order+1)
# elif model == 'ppoly':
# coeffs = [value] + [0.0]*order
# else:
# raise Exception(f"Unknown frequency model for parameter {param}")
# setattr(self, param, coeffs)
[docs]
def interpolated(self, param: str, freq: Frequency) -> jnp.ndarray:
"""Evaluates a potentially frequency-dependent parameter.
Args:
param (str): The name of the parameter to evaluate (e.g., 'epr').
freq (Frequency): The frequency axis for the evaluation.
Returns:
np.ndarray: The parameter's value across the frequency axis.
"""
w = freq.w
if param.startswith('rho'):
model = str(getattr(self, 'rho_model'))
else:
model = str(getattr(self, f'{param}_model'))
if model == 'constant':
value = getattr(self, param) * jnp.ones(w.shape[0])
else:
coeffs = getattr(self, param)
if model.startswith('ppoly'):
value = evaluate_power_basis(w, coeffs, w[0], w[-1])
else:
value = evaluate_bernstein_basis(w, coeffs, w[0], w[-1])
return value
[docs]
def epr_f(self, freq: Frequency) -> jnp.ndarray:
"""The relative permittivity (epsilon_r) as a function of frequency."""
return self.interpolated('epr', freq)
[docs]
def tand_f(self, freq: Frequency) -> jnp.ndarray:
"""The loss tangent (tan_delta) as a function of frequency."""
return self.interpolated('tand', freq)
[docs]
def mur_f(self, freq: Frequency) -> jnp.ndarray:
"""The relative permeability (mu_r) as a function of frequency."""
return self.interpolated('mur', freq)
[docs]
def rho_f(self, freq: Frequency) -> jnp.ndarray:
"""The conductor resistivity (rho) as a function of frequency."""
return self.interpolated('rho', freq)
[docs]
def rhoin_f(self, freq: Frequency) -> jnp.ndarray:
"""The inner conductor resistivity as a function of frequency."""
return self.interpolated('rhoin', freq) if self.separate_rho else self.rho_f(freq)
[docs]
def rhoout_f(self, freq: Frequency) -> jnp.ndarray:
"""The outer conductor resistivity as a function of frequency."""
return self.interpolated('rhoout', freq) if self.separate_rho else self.rho_f(freq)
[docs]
def eps_f(self, freq: Frequency) -> jnp.ndarray:
"""The complex permittivity (epsilon) as a function of frequency."""
return epsilon_0 * self.epr_f(freq) * (1 - 1j * self.tand_f(freq))
[docs]
def mu_f(self, freq: Frequency) -> jnp.ndarray:
"""The complex permeability (mu) as a function of frequency."""
return mu_0 * self.mur_f(freq)
[docs]
def L_prime(self, freq: Frequency) -> jnp.ndarray:
"""The per-unit-length external inductance."""
a, b = self.din / 2, self.dout / 2
lnbOvera = jnp.log(b/a)
return self.mu_f(freq) / (2 * jnp.pi) * lnbOvera
[docs]
def C_prime(self, freq: Frequency) -> jnp.ndarray:
"""The per-unit-length capacitance."""
a, b = self.din / 2, self.dout / 2
lnbOvera = jnp.log(b/a)
return 2 * jnp.pi * jnp.real(self.eps_f(freq)) / lnbOvera
[docs]
def G_diel(self, freq: Frequency) -> jnp.ndarray:
"""The per-unit-length dielectric conductance."""
a, b = self.din / 2, self.dout / 2
lnbOvera = jnp.log(b/a)
return 2 * jnp.pi * freq.w * -jnp.imag(self.eps_f(freq)) / lnbOvera
[docs]
def R_skin(self, freq: Frequency) -> jnp.ndarray:
"""The per-unit-length resistance due to skin effect."""
return jnp.real(self.Z_skin(freq))
[docs]
def L_skin(self, freq: Frequency) -> jnp.ndarray:
"""The per-unit-length internal inductance due to skin effect."""
return jnp.imag(self.Z_skin(freq)) / freq.w
[docs]
def Z_skin(self, freq: Frequency):
"""The per-unit-length internal impedance due to skin effect."""
w, a, b, mu = freq.w, self.din / 2, self.dout / 2, self.mu_f(freq)
sigma_a, sigma_b = 1 / self.rhoin_f(freq), 1 / self.rhoout_f(freq)
L_skin_a = (1 / (2 * jnp.pi * a)) * jnp.sqrt(mu / (2 * w * sigma_a))
L_skin_b = (1 / (2 * jnp.pi * b)) * jnp.sqrt(mu / (2 * w * sigma_b))
L_skin = L_skin_a + L_skin_b
R_skin_a = (1 / (2 * jnp.pi * a)) * jnp.sqrt(w * mu / (2 * sigma_a))
R_skin_b = (1 / (2 * jnp.pi * b)) * jnp.sqrt(w * mu / (2 * sigma_b))
R_skin = R_skin_a + R_skin_b
return R_skin + 1j * w * L_skin
[docs]
def rlgc(self, freq: Frequency) -> tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray, jnp.ndarray]:
"""Calculates RLGC parameters from physical and material properties.
Args:
freq (Frequency): The frequency axis for the calculation.
Returns:
tuple: The calculated R, L, G, and C parameter vectors.
"""
if not self.neglect_skin_inductance:
L = self.L_prime(freq) + self.L_skin(freq)
else:
L = self.L_prime(freq)
C = self.C_prime(freq)
G = self.G_diel(freq)
R = self.R_skin(freq)
return R, L, G, C