Source code for pmrf.models.lines

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 TLine(Model): length: Parameter = 1.0 floating: bool = False # ports 0 (+) and 2 (-) form a terminal pair, as well as ports 1 (+) and 3 (-) renormalize: bool = True
[docs] class RLGCLine(TLine): """ **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. """ floating: bool = False # ports 0 (+) and 2 (-) form a terminal pair, as well as ports 1 (+) and 3 (-)
[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")
# 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 # if self.floating: # raise Exception('Cannot calculate ABCD matrix for a floating line') # 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
[docs] def z0_characteristic(self, frequency: Frequency) -> jnp.ndarray: """Calculates the characteristic impedance from the line's RLGC parameters. Args: frequency (Frequency): The frequency axis for the calculation. Returns: np.ndarray: The resultant ABCD-matrix. """ w = frequency.w R, L, G, C = self.rlgc(frequency) return jnp.sqrt((R + 1j*w*L) / (G + 1j*w*C))
[docs] 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. """ 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 if self.floating: denom = -1 + 9*jnp.exp(2*gL) s11 = (1 + 3*jnp.exp(2*gL)) / denom s12 = 4*jnp.exp(gL) / denom s13 = (-2 + 6*jnp.exp(2*gL)) / denom s14 = -s12 s = jnp.array([ [s11, s12, s13, s14], [s12, s11, s14, s13], [s13, s14, s11, s12], [s14, s13, s12, s11], ]).transpose(2, 0, 1) else: s11 = jnp.zeros(frequency.npoints, dtype=complex) s21 = jnp.exp(-1*gL) s = jnp.array([ [s11, s21], [s21, s11], ]).transpose(2, 0, 1) if self.renormalize: # from skrf import renormalize_s as renormalize_s_skrf # return jnp.array(renormalize_s_skrf(s, Zc, self.z0, 'traveling', 'traveling')) # According to scikit-rf, all the above is defined in terms of traveling S-parameters # (see e.g. skrf.media.Media.line) return renormalize_s(s, Zc, self.z0, 'traveling', 'traveling') return s
[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 DatasheetLine(RLGCLine): """ **Overview** A transmission line defined by parameters typically found on a datasheet, such as nominal impedance and loss factors. This model provides a convenient way to define a transmission line 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 CoaxialLine(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
[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
[docs] class MicrostripLine(RLGCLine): """ A microstrip line defined by its geometric and material properties (i.e. width, height, dielectric constant, tan delta, rho). """ W: Parameter = 3e-3 H: Parameter = 1.6e-3 epr: Parameter = 4.3 tand: Parameter = 0.0 rho: Parameter = 0.0
[docs] def rlgc(self, freq: Frequency) -> tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray, jnp.ndarray]: """Calculates RLGC parameters from physical and material properties. Note that h > w is not yet supported. Args: freq (Frequency): The frequency axis for the calculation. Returns: tuple: The calculated R, L, G, and C parameter vectors. """ W, H = self.W, self.H epr, tand, rho = self.epr, self.tand, self.rho u = W / H t1 = ((epr + 1) / 2) t2 = ((epr - 1) / 2) t3 = 1 / jnp.sqrt(1 + 12 / u) epe = (t1 + t2*t3) * jnp.ones(freq.npoints) Za = (120 * jnp.pi) / (u + 1.393 + 0.667 * jnp.log(u + 1.444)) Ze = Za / jnp.sqrt(epe) L = (Ze * jnp.sqrt(epe)) / c C = (jnp.sqrt(epe)) / (Ze * c) R = (1 / W) * jnp.sqrt(2 * mu_0 * rho) * jnp.sqrt(freq.w) G = (1 / (Za * c)) * (epr * (epe - 1) / (epr - 1)) * tand * freq.w return R, L, G, C