Source code for pmrf.models.components.lines.uniform

"""
Uniform transmission lines (RLGC, coaxial, microstrip)
"""
from abc import ABC, abstractmethod

from scipy.constants import c, mu_0, epsilon_0
import jax.numpy as jnp

from pmrf.frequency import Frequency
from pmrf.rf_functions.conversions import renormalize_s
from pmrf.parameters import Parameter
from pmrf.models.model import Model

[docs] class TransmissionLine(Model, ABC): r""" Abstract base class for all uniform transmission line models. Provides the fundamental equations to construct S-parameters based on frequency-dependent characteristic impedance ($Z_c$) and total complex electrical length ($\gamma L$). Derived classes must implement the `zc_gammaL` method. **Mathematical Formulation** For a single-ended 2-port transmission line, the traveling wave S-parameters with respect to $Z_c$ are: $$S_{11} = S_{22} = 0$$ $$S_{21} = S_{12} = e^{-\gamma L}$$ This model computes these S-parameters and then re-normalized them into $Z_0$ and the power-wave definition using :meth:`pmrf.rf_functions.renormalize_s`. Attributes ---------- floating : bool, default=False If True, modeled as a 4-port differential network (ports 0/1 and 2/3 form terminal pairs). If False, modeled as a 2-port single-ended network. """ floating: bool = False
[docs] @abstractmethod def zc_gammaL(self, frequency: Frequency) -> tuple[jnp.ndarray, jnp.ndarray]: r""" Calculates characteristic impedance ($Z_c$) and complex electrical length ($\gamma L$). Parameters ---------- frequency : Frequency The frequency axis. Returns ------- tuple[jnp.ndarray, jnp.ndarray] Array of characteristic impedance ($Z_c$) and complex electrical length ($\gamma L$). """ raise NotImplementedError
[docs] def s(self, frequency: Frequency) -> jnp.ndarray: zc, gL = self.zc_gammaL(frequency) if self.floating: denom = -1 + 9*jnp.exp(2*gL) a = (1 + 3*jnp.exp(2*gL)) / denom b = 4*jnp.exp(gL) / denom c = (-2 + 6*jnp.exp(2*gL)) / denom d = -b s = jnp.array([ [a, c, b, d], [c, a, d, b], [b, d, a, c], [d, b, c, a], ]).transpose(2, 0, 1) else: a = jnp.zeros(frequency.npoints, dtype=complex) s21 = jnp.exp(-1*gL) s = jnp.array([ [a, s21], [s21, a], ]).transpose(2, 0, 1) # Renormalize into the model's characteristic impedance and power waves # (the above formulation is in terms of traveling waves). return renormalize_s(s, zc, self.z0, 'traveling', 'power')
[docs] class RLGCLine(TransmissionLine, ABC): r""" Abstract base class for a transmission line defined by its per-unit-length RLGC (Resistance, Inductance, Conductance, Capacitance) parameters. Derived classes must implement `rlgc` to define how these parameters behave over frequency. **Mathematical Formulation** The characteristic impedance ($Z_c$) and complex propagation constant ($\gamma$) are derived as: $$Z_c = \sqrt{\frac{R + j\omega L}{G + j\omega C}}$$ $$\gamma = \sqrt{(R + j\omega L)(G + j\omega C)}$$ The total complex electrical length is $\gamma L$. Attributes ---------- length : Parameter, default=1.0 Physical length of the line in meters. """ length: Parameter = 1.0
[docs] @abstractmethod def rlgc(self, freq: Frequency) -> tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray, jnp.ndarray]: r""" Calculates the frequency-dependent RLGC parameters. Parameters ---------- freq : Frequency The frequency axis. Returns ------- tuple The R, L, G, and C parameter vectors. """ raise NotImplementedError("'rlgc' must be implemented in the derived class")
[docs] def zc_gammaL(self, frequency: Frequency) -> jnp.ndarray: w = frequency.w R, L, G, C = self.rlgc(frequency) zc = jnp.sqrt((R + 1j*w*L) / (G + 1j*w*C)) gamma = jnp.sqrt((R + 1j*w*L) * (G + 1j*w*C)) gammaL = gamma*self.length return zc, gammaL
[docs] class PhaseLine(TransmissionLine): r""" Ideal, lossless, and dispersionless transmission line defined by electrical length at a reference frequency. Characteristic impedance is real and constant; phase scales linearly. **Mathematical Formulation** $$Z_c(\omega) = z_c$$ $$\gamma L(\omega) = j \cdot \left(\theta \cdot \frac{\pi}{180}\right) \cdot \frac{\omega}{\omega_0}$$ Example -------- .. code-block:: python import pmrf as prf from pmrf.models import PhaseLine # Create an ideal 90-degree (quarter-wave) 50-ohm line at 1 GHz quarter_wave = PhaseLine( zc=50.0, theta=90.0, f0=1e9 ) freq = prf.Frequency(start=0.5, stop=1.5, npoints=101, unit='ghz') s = quarter_wave.s(freq) Attributes ---------- zc : Parameter, default=50.0 Characteristic impedance in Ohms. theta : Parameter, default=90.0 Electrical length (phase shift) in degrees at reference frequency `f0`. f0 : Parameter, default=1e9 Reference frequency in Hz for `theta`. """ theta: Parameter = 90.0 zc: Parameter = 50.0 f0: float = 1e9
[docs] def zc_gammaL(self, frequency: Frequency) -> jnp.ndarray: zc = self.zc * jnp.ones(frequency.npoints, dtype=complex) theta_rad = self.theta * jnp.pi / 180.0 w0 = 2 * jnp.pi * self.f0 beta_L = theta_rad * (frequency.w / w0) gammaL = 1j * beta_L return zc, gammaL
[docs] class ConstantRLGCLine(RLGCLine): r""" Transmission line with constant, frequency-independent RLGC parameters. **Mathematical Formulation** $$R(\omega) = R$$ $$L(\omega) = L$$ $$G(\omega) = G$$ $$C(\omega) = C$$ Example -------- .. code-block:: python import pmrf as prf from pmrf.models import ConstantRLGCLine lossless_line = ConstantRLGCLine( L=368.8e-9, # nH/m C=147.5e-12, # pF/m length=0.1 # 10 cm ) freq = prf.Frequency(start=1, stop=5, npoints=101, unit='ghz') s = lossless_line.s(freq) Attributes ---------- R : Parameter, default=0.0 Resistance in Ohms/m. L : Parameter, default=280e-9 Inductance in Henries/m. G : Parameter, default=0.0 Conductance in Siemens/m. C : Parameter, default=90e-12 Capacitance in Farads/m. """ R: Parameter = 0.0 L: Parameter = 280e-9 G: Parameter = 0.0 C: Parameter = 90e-12
[docs] def rlgc(self, freq: Frequency) -> tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray, jnp.ndarray]: ones = jnp.ones(freq.npoints) return self.R * ones, self.L * ones, self.G * ones, self.C * ones
[docs] class PhysicalLine(RLGCLine): r""" Transmission line defined by nominal characteristic impedance, relative permittivity, conductor attenuation, and dielectric loss tangent. **Mathematical Formulation** The frequency-dependent attenuation components are computed as: $$\alpha_c = A \cdot \sqrt{\frac{f}{fA}} \cdot \frac{\ln(10)}{20}$$ $$\alpha_d = \frac{\pi f \sqrt{\varepsilon_r}}{c} \cdot \tan\delta$$ Which yield the per-unit-length parameters: $$R = 2 z_n \alpha_c$$ $$L = \frac{z_n \sqrt{\varepsilon_r}}{c}$$ $$G = \frac{2 \alpha_d}{z_n}$$ $$C = \frac{\sqrt{\varepsilon_r}}{z_n c}$$ Example -------- .. code-block:: python import pmrf as prf from pmrf.models import PhysicalLine line = PhysicalLine( zn=50.0, length=1.0, epr=2.2, A=0.01, fA=1.0, tand=0.001 ) freq = prf.Frequency(start=1, stop=10, npoints=101, unit='ghz') s = line.s(freq) Attributes ---------- zn : Parameter, default=50.0 Nominal characteristic impedance defining the L/C ratio. epr : Parameter, default=1.0 Relative permittivity. A : Parameter, default=0.0 Conductor loss in dB/m/sqrt(Hz). fA : Parameter, default=1.0 Frequency scaling reference for attenuation in Hz. tand : Parameter, default=0.0 Dielectric loss tangent. """ zn: Parameter = 50.0 epr: Parameter = 1.0 A: Parameter = 0.0 fA: Parameter = 1.0 tand: Parameter = 0.0
[docs] def rlgc(self, freq: Frequency) -> tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray, jnp.ndarray]: f = freq.f sqrt_epr = jnp.sqrt(self.epr) A_dB = self.A * jnp.sqrt(f / self.fA) alpha_c = A_dB * (jnp.log(10) / 20.0) alpha_d = jnp.pi * sqrt_epr * f / c * self.tand R_val = 2 * self.zn * alpha_c L_val = (self.zn * sqrt_epr) / c G_val = 2 / self.zn * alpha_d C_val = sqrt_epr / (self.zn * c) ones = jnp.ones(freq.npoints) R = R_val * ones L = L_val * ones G = G_val * ones C = C_val * ones return R, L, G, C
[docs] class DatasheetLine(RLGCLine): r""" Transmission line defined by common datasheet parameters (nominal impedance, dielectric constant, and loss factors). Includes skin effect (`k1`) and dielectric loss (`k2`). **Mathematical Formulation** The normalized loss coefficients ($k_{1,norm}$, $k_{2,norm}$) depend on `loss_coeffs_normalized`. Attenuation variables scale natively with $\sqrt{\omega}$ and $\omega$: $$\alpha_c = k_{1,norm} \cdot \frac{\ln(10)}{20} \cdot \sqrt{\omega}$$ $$\alpha_d = k_{2,norm} \cdot \frac{\ln(10)}{20} \cdot \omega$$ Resulting in the per-unit-length components: $$R = 2 z_n \alpha_c$$ $$L = \frac{z_n \sqrt{\varepsilon_r}}{c}$$ $$G = \frac{2 \alpha_d}{z_n}$$ $$C = \frac{\sqrt{\varepsilon_r}}{z_n c}$$ Example -------- .. code-block:: python import pmrf as prf from pmrf.models import DatasheetLine cable = DatasheetLine( 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) Attributes ---------- zn : Parameter, default=50.0 Nominal characteristic impedance. epr : Parameter, default=1.0 Relative permittivity. k1 : Parameter, default=0.0 Skin effect loss factor. k2 : Parameter, default=0.0 Dielectric loss factor. epr_slope : Parameter | None, default=None Linear slope to apply to permittivity over the frequency bounds. loss_coeffs_normalized : bool, default=False If True, k1 and k2 are evaluated directly without normalizing to 100MHz references. freq_bounds : tuple | None, default=None Angular frequency limits (start, stop) used to scale `epr_slope`. Defaults to the analysis array bounds. """ zn: Parameter = 50.0 epr: Parameter = 1.0 k1: Parameter = 0.0 k2: Parameter = 0.0 loss_coeffs_normalized: bool = False
[docs] def rlgc(self, freq: Frequency) -> tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray, jnp.ndarray]: w = freq.w zn, k1, k2 = self.zn, self.k1, self.k2 epr = jnp.ones(w.shape[0]) * self.epr 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): r""" Coaxial line defined directly by its physical geometry and material properties. **Mathematical Formulation** Ideal non-dispersive components ($L'$ and $C'$) and dielectric loss ($G$) are given by: $$L' = \frac{\mu_0 \mu_r}{2\pi} \ln\left(\frac{b}{a}\right)$$ $$C' = \frac{2\pi \varepsilon_0 \varepsilon_r}{\ln(b/a)}$$ $$G_{diel} = \frac{2\pi \omega \varepsilon_0 \varepsilon_r \tan\delta}{\ln(b/a)}$$ The internal surface impedance defining frequency-dependent skin resistance ($R_{skin}$) and skin inductance ($L_{skin}$) is governed by: $$R_{skin} = \frac{1}{2\pi a} \sqrt{\frac{\omega\mu}{2\sigma_a}} + \frac{1}{2\pi b} \sqrt{\frac{\omega\mu}{2\sigma_b}}$$ $$L_{skin} = \frac{1}{2\pi a} \sqrt{\frac{\mu}{2\omega\sigma_a}} + \frac{1}{2\pi b} \sqrt{\frac{\mu}{2\omega\sigma_b}}$$ Where $a$ is the inner radius, $b$ is the outer radius, and $\sigma$ is the conductor conductivity ($1/\rho$). The total per-unit-length inductance is $L = L' + L_{skin}$. Example -------- .. code-block:: python import pmrf as prf from pmrf.models import CoaxialLine phys_cable = CoaxialLine( din=0.9e-3, dout=2.95e-3, epr=1.5, tand=0.0004, rho=1.72e-8, length=0.5 ) freq = prf.Frequency(start=1, stop=20, npoints=101, unit='ghz') s_phys = phys_cable.s(freq) Attributes ---------- din : Parameter, default=1.12e-3 Inner conductor diameter in meters. dout : Parameter, default=3.2e-3 Outer conductor inner diameter in meters. epr : Parameter, default=1.0 Relative permittivity of the dielectric. mur : Parameter, default=1.0 Relative permeability. tand : Parameter, default=0.0 Loss tangent of the dielectric. rho : Parameter, default=1.68e-8 Resistivity of the conductors in Ohm-meters. """ 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 @property def eps(self) -> jnp.ndarray: return epsilon_0 * self.epr * (1 - 1j * self.tand) @property def mu(self) -> jnp.ndarray: return mu_0 * self.mur
[docs] def L_prime(self, freq: Frequency) -> jnp.ndarray: a, b = self.din / 2, self.dout / 2 lnbOvera = jnp.log(b/a) return jnp.ones(freq.npoints) * self.mu / (2 * jnp.pi) * lnbOvera
[docs] def C_prime(self, freq: Frequency) -> jnp.ndarray: a, b = self.din / 2, self.dout / 2 lnbOvera = jnp.log(b/a) return jnp.ones(freq.npoints) * 2 * jnp.pi * jnp.real(self.eps) / lnbOvera
[docs] def G_diel(self, freq: Frequency) -> jnp.ndarray: a, b = self.din / 2, self.dout / 2 lnbOvera = jnp.log(b/a) return 2 * jnp.pi * freq.w * -jnp.imag(self.eps) / lnbOvera
[docs] def R_skin(self, freq: Frequency) -> jnp.ndarray: return jnp.real(self.Z_skin(freq))
[docs] def L_skin(self, freq: Frequency) -> jnp.ndarray: return jnp.imag(self.Z_skin(freq)) / freq.w
[docs] def Z_skin(self, freq: Frequency): w, a, b, mu = freq.w, self.din / 2, self.dout / 2, self.mu sigma_a, sigma_b = 1 / self.rho, 1 / self.rho 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]: L = self.L_prime(freq) + self.L_skin(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): r""" Microstrip line defined by standard geometric and material properties. Relies on standard Wheeler approximations. Note that configurations where height > width (h > w) are not yet supported. **Mathematical Formulation** With ratio $u = \frac{W}{H}$, the effective relative permittivity ($\varepsilon_e$) and ideal impedance terms ($Z_a, Z_e$) are: $$\varepsilon_e = \frac{\varepsilon_r + 1}{2} + \frac{\varepsilon_r - 1}{2} \frac{1}{\sqrt{1 + 12/u}}$$ $$Z_a = \frac{120\pi}{u + 1.393 + 0.667 \ln(u + 1.444)}$$ $$Z_e = \frac{Z_a}{\sqrt{\varepsilon_e}}$$ Which provide the per-unit-length components: $$L = \frac{Z_e \sqrt{\varepsilon_e}}{c}$$ $$C = \frac{\sqrt{\varepsilon_e}}{Z_e c}$$ $$R = \frac{1}{W} \sqrt{2 \mu_0 \rho \omega}$$ $$G = \frac{1}{Z_a c} \frac{\varepsilon_r (\varepsilon_e - 1)}{\varepsilon_r - 1} \tan\delta \cdot \omega$$ Example -------- .. code-block:: python import pmrf as prf from pmrf.models import MicrostripLine phys_microstrip = MicrostripLine( w=4e-3, h=2.0e-3, epr=4.6, tand=0.025, rho=1.72e-8, length=0.5 ) freq = prf.Frequency(start=1, stop=20, npoints=101, unit='ghz') s_phys = phys_microstrip.s(freq) Attributes ---------- w : Parameter, default=3e-3 Width of the microstrip trace in meters. h : Parameter, default=1.6e-3 Height of the dielectric substrate in meters. epr : Parameter, default=4.3 Relative permittivity of the dielectric substrate. tand : Parameter, default=0.0 Dielectric loss tangent. rho : Parameter, default=0.0 Resistivity of the conductor trace and ground plane in Ohm-meters. """ 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]: 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