Source code for skrf.media.coaxial

"""
coaxial (:mod:`skrf.media.coaxial`)
============================================================

A coaxial transmission line defined from its electrical or geometrical/physical properties

.. autosummary::
   :toctree: generated/

   Coaxial

"""

from __future__ import annotations

import warnings
from typing import TYPE_CHECKING

import numpy as np
import scipy

from .. import constants as _const
from ..constants import NumberLike
from ..mathFunctions import db_2_np, db_per_100feet_2_db_per_100meter, feet_2_meter
from ..tlineFunctions import surface_impedance, surface_resistivity
from .distributedCircuit import DistributedCircuit
from .media import DefinedGammaZ0, Media

if TYPE_CHECKING:
    from ..frequency import Frequency


[docs] class Coaxial(DistributedCircuit, Media): r""" A coaxial transmission line defined in terms of its inner/outer diameters and permittivity. Parameters ---------- frequency : :class:`~skrf.frequency.Frequency` object Frequency band. z0_port : number, array-like, or None `z0_port` is the port impedance for networks generated by the media. If `z0_port` is not None, the networks generated by the media are renormalized (or in other words embedded) from the characteristic impedance z0 of the media to `z0_port`. Else if `z0_port` is None, the networks port impedances will be the raw characteristic impedance z0 of the media. (Default is None) z0_override : number, array-like, or None `z0_override` override the characteristic impedance for the media. If `z0_override` is not None, the networks generated by the media have their characteristic impedance `z0` overridden by `z0_override`. (Default is None) z0 : number, array-like, or None deprecated parameter, alias to `z0_override` if `z0_override` is None. Emit a deprecation warning. Dint : number, or array-like inner conductor diameter, in m Dout : number, or array-like outer conductor diameter, in m epsilon_r : number, or array-like, optional relative permittivity of the dielectric medium. Default is 1. tan_delta : number, or array-like, optional loss tangent of the dielectric medium. Default is 0. sigma : number, or array-like, optional. conductors electrical conductivity, in S/m. Default is 58e6, the conductivity of annealed copper at 20 °C (100% IACS). tout : number, array-like, or None, optional wall thickness of the outer conductor, in m. If None (default), the shield is infinitely thick. model : str, optional Model of the conductors. 'schelkunoff' (default) is the Bessel function solution of [#Schelkunoff]_, 'tesche' the equivalent circuit approximation of [#Tesche]_. dielectric : dict or None, optional Material of the dielectric filling, as a dict taking the keys ``'ep_r'`` (complex relative permittivity :math:`\epsilon_r (1 - j\tan\delta)`) and ``'mu_r'`` (complex relative permeability), both defaulting to 1. If None (default), the filling is described by `epsilon_r` and `tan_delta`. inner_conductor : dict, list of dict, or None, optional Material of the inner conductor. A single dict describes a bulk conductor, a list of dict a stack of coatings ordered from the dielectric inwards, of which the deepest layer is the bulk that carries the dc current. Each dict takes the keys ``'sigma'``, ``'mu_r'`` and ``'ep_r'`` of the layer itself, and ``'rms_roughness'``, ``'boundary_loc'`` and ``'distribution'`` of the boundary on top of it, which are handed to :func:`~skrf.tlineFunctions.surface_impedance`. If None (default), the conductor is smooth and made of bulk `sigma`. outer_conductor : dict, list of dict, or None, optional Material of the outer conductor, described as `inner_conductor`. Note ---- Dint, Dout, tout, epsilon_r, tan_delta, sigma can all be vectors as long as they are the same length References ---------- .. [#Schelkunoff] S. A. Schelkunoff, "The electromagnetic theory of coaxial transmission lines and cylindrical shields," Bell Syst. Tech. J., vol. 13, no. 4, pp. 532-579, Oct. 1934. https://doi.org/10.1002/j.1538-7305.1934.tb00679.x .. [#Tesche] F. M. Tesche, "A Simple Model for the Line Parameters of a Lossy Coaxial Cable Filled With a Nondispersive Dielectric," IEEE Trans. Electromagn. Compat., vol. 49, no. 1, pp. 12-17, Feb. 2007. https://doi.org/10.1109/TEMC.2006.888185 Examples -------- An RG58C/U flexible cable with copper conductors and a lossy polyethylene filling. Its characteristic impedance and the insertion loss of a 100 mm length of it at 5 GHz: >>> import skrf as rf >>> from skrf.media import Coaxial >>> freq = rf.Frequency(1, 5, 101, unit='GHz') >>> rg58 = Coaxial(freq, Dint=0.91e-3, Dout=2.95e-3, epsilon_r=2.3, ... tan_delta=2e-4, sigma=58e6, z0_port=50) >>> print(f"{rg58.z0[0]:.2f} ohm, {rg58.line(100, 'mm').s_db[-1, 1, 0]:.3f} dB") 46.56-0.05j ohm, -0.094 dB The same cable roughened by 0.6 um rms at the inner conductor and by 1 um rms at the braided shield, which is described by giving each conductor a material of its own instead of the bulk `sigma`: >>> rg58 = Coaxial(freq, Dint=0.91e-3, Dout=2.95e-3, epsilon_r=2.3, ... tan_delta=2e-4, z0_port=50, ... inner_conductor={'sigma': 58e6, 'rms_roughness': 0.6e-6}, ... outer_conductor={'sigma': 58e6, 'rms_roughness': 1e-6}) >>> print(f"{rg58.line(100, 'mm').s_db[-1, 1, 0]:.3f} dB") -0.140 dB An APC-7 air line whose two conductors carry an ENIG finish, 0.05 um of gold over 4 um of nickel over copper, roughened by 50 nm rms at the outside and by 0.2 um rms at the copper underneath: >>> enig = [{'sigma': 41.1e6, 'rms_roughness': 50e-9, 'boundary_loc': 0}, ... {'sigma': 14.5e6, 'mu_r': 20, 'rms_roughness': 50e-9, 'boundary_loc': 0.05e-6}, ... {'sigma': 58e6, 'rms_roughness': 0.2e-6, 'boundary_loc': 4.05e-6}] >>> freq = rf.Frequency(1, 18, 171, unit='GHz') >>> apc7 = Coaxial(freq, Dint=3.04e-3, Dout=7.00e-3, ... inner_conductor=enig, outer_conductor=enig) >>> print(f"{apc7.z0[0]:.2f} ohm, {apc7.line(100, 'mm').s_db[-1, 1, 0]:.3f} dB") 50.22-0.25j ohm, -0.301 dB """ ## CONSTRUCTOR
[docs] def __init__(self, frequency: Frequency | None = None, z0_port: NumberLike | None = None, z0_override: NumberLike | None = None, z0: NumberLike | None = None, Dint: NumberLike = .81e-3, Dout: NumberLike = 5e-3, epsilon_r: NumberLike = 1, tan_delta: NumberLike = 0, sigma: NumberLike = 58e6, tout: NumberLike | None = None, model: str = 'schelkunoff', dielectric: dict | None = None, inner_conductor: dict | list[dict] | None = None, outer_conductor: dict | list[dict] | None = None, *args, **kwargs): Media.__init__(self, frequency = frequency, z0_port = z0_port, z0_override = z0_override, z0 = z0) self.Dint, self.Dout = Dint, Dout self.tout = np.inf if tout is None else tout self.sigma = sigma self.model = model self.inner_conductor, self.outer_conductor = inner_conductor, outer_conductor # warning when describing one conductor and not the other if (inner_conductor is None) != (outer_conductor is None): given, missing = (('outer_conductor', 'inner_conductor') if inner_conductor is None else ('inner_conductor', 'outer_conductor')) warnings.warn( f'Only {given} was given a material, so {missing} stays a smooth bulk ' f'conductor of sigma = {sigma} S/m and unit relative permeability. ' f'Pass {missing} as well to describe both conductors.', UserWarning, stacklevel = 2) # epsilon_r and tan_delta are the special case of a purely dielectric filling self.dielectric = {'ep_r': epsilon_r*(1 - 1j*tan_delta)} if dielectric is None else dielectric ep_r = self.dielectric.get('ep_r', 1) self.epsilon_prime = _const.epsilon_0*np.real(ep_r) # a lossy ep_r = ep' - j*ep'' has a negative imaginary part, while epsilon_second # is ep'' itself, which G expects to be positive self.epsilon_second = -_const.epsilon_0*np.imag(ep_r) # read back from the filling actually in use, which is the dielectric dict # when one is given and the epsilon_r, tan_delta pair otherwise self.epsilon_r = np.real(ep_r) self.tan_delta = -np.imag(ep_r)/np.real(ep_r)
[docs] @classmethod def from_attenuation_VF(cls, frequency: Frequency | None = None, z0_port: NumberLike | None = None, z0: float = 50, att=0, unit='dB/m', VF=1) -> Media: """ Init from electrical properties of the line: attenuation and velocity factor. Attenuation can be expressed in dB/m, dB/100m, dB/ft, dB/100ft, Neper/m or Neper/ft. Default unit is dB/m. A different unit is set by the `unit` parameter. Parameters ---------- frequency : :class:`~skrf.frequency.Frequency` object z0_port : number, array-like, or None `z0_port` is the port impedance for networks generated by the media. If `z0_port` is not None, the networks generated by the media are renormalized (or in other words embedded) from the characteristic impedance z0 of the media to `z0_port`. Else if `z0_port` is None, the networks port impedances will be the raw characteristic impedance z0 of the media. (Default is None) z0 : number desired characteristic impedance att : number, or array-like. optional Attenuation of the coaxial line. The default is 0. If passed as an array, should be of same size than the frequency. unit : string, optional Unit of the attenuation. Can be: 'dB/m', dB/100m'', 'dB/ft', 'dB/100ft', 'Neper/m' or 'Neper/ft' (or 'Np/m', 'Np/ft'). The default is 'dB/m'. VF : number, or array-like. optional Velocity Factor VF [VF]_. The default is 1. If passed as an array, should be of same size than the frequency. Returns ------- media : :class:`~skrf.media.media.Media` References ---------- .. [VF] : https://www.microwaves101.com/encyclopedias/light-phase-and-group-velocities """ # test size of parameters if np.size(np.array(att, dtype="object")) not in (1, np.size(frequency.f)): raise ValueError('Attenuation should be scalar or of same size that the frequency.') # create gamma if unit in ('dB/m', 'db/m'): alpha = db_2_np(att) elif unit in ('dB/100m', 'db/100m'): alpha = db_2_np(att/100) elif unit in ('dB/ft', 'dB/feet'): alpha = db_2_np(att/feet_2_meter()) elif unit in ('dB/100ft', 'dB/100feet'): alpha = db_2_np(db_per_100feet_2_db_per_100meter(att)/100) elif unit in ('Np/m', 'np/m', 'n/m'): alpha = att elif unit in ('Neper/feet', 'Neper/ft', 'Np/feet', 'Np/ft', 'np/feet', 'np/ft', 'N/feet', 'N/ft'): alpha = att/feet_2_meter() else: raise ValueError('Incorrect attenuation unit. Please see documentation. ', unit) beta = 2 * np.pi * frequency.f / _const.c / VF gamma = alpha + 1j*beta # return media object from z0 and gamma return DefinedGammaZ0(frequency=frequency, gamma=gamma, z0_port=z0_port, z0=z0)
[docs] @classmethod def from_Z0_Dout(cls, frequency: Frequency | None = None, z0_port: NumberLike | None = None, z0: float = 50, epsilon_r: NumberLike = 1, Dout: NumberLike = 5e-3, **kw) -> Media: """ Init from characteristic impedance and outer diameter. Parameters ---------- frequency : :class:`~skrf.frequency.Frequency` object z0_port : number, array-like, or None `z0_port` is the port impedance for networks generated by the media. If `z0_port` is not None, the networks generated by the media are renormalized (or in other words embedded) from the characteristic impedance z0 of the media to `z0_port`. Else if `z0_port` is None, the networks port impedances will be the raw characteristic impedance z0 of the media. (Default is None) z0 : number desired characteristic impedance Dout : number, or array-like outer conductor diameter, in m epsilon_r=1 : number, or array-like relative permittivity of the dielectric medium **kw : passed to __init__ Returns ------- media : :class:`~skrf.media.media.Media` """ ep= _const.epsilon_0*epsilon_r if np.imag(z0) !=0: raise NotImplementedError() b = Dout/2. b_over_a = np.exp(2*np.pi*z0*np.sqrt(ep/_const.mu_0)) a = b/b_over_a Dint = 2*a return cls(frequency=frequency, z0_port = z0_port, Dint=Dint, Dout=Dout, epsilon_r=epsilon_r, **kw)
@property def Rs(self) -> NumberLike: """ Surface resistivity in Ohm/area. Returns ------- Rs : number or array surface resistivity Note ---- Not used in code. Kept for backward-compatibility. """ f = self.frequency.f rho = 1./self.sigma mu_r =1 return surface_resistivity(f=f,rho=rho, mu_r=mu_r) @property def a(self) -> NumberLike: """ Inner radius of the coaxial line Returns ------- a : float Inner radius """ return self.Dint/2. @property def b(self) -> NumberLike: """ Outer radius of the coaxial line Returns ------- b : float Outer radius """ return self.Dout/2. def _conductor_impedance(self, r: NumberLike, t: NumberLike | None, conductor: dict | list[dict] | None) -> NumberLike: r""" Impedance per unit length of one conductor, in Ohm/m. A conductor is described by its surface impedance :math:`Z_s`, in Ohm per square, which is used to build the impedance per unit length that also carries the dc resistance and the internal inductance of the conductor. Parameters ---------- r : number, or array-like radius of the conductor, in m. The inner conductor is a rod of radius `r`, the outer conductor a tube whose inside sits at `r`. t : number, array-like, or None wall thickness of the tube, in m. None for the solid inner conductor. conductor : dict, list of dict, or None material stack of the conductor, as described in :class:`Coaxial`. Returns ------- Z : number, or array-like impedance per unit length, in Ohm/m """ if conductor is None: stack = [{}] elif isinstance(conductor, dict): stack = [conductor] else: stack = list(conductor) # the deepest layer is the bulk of the conductor, which carries the dc current # and the internal inductance; the layers on top of it only shape Zs ones = np.ones_like(self.frequency.f) sigma = ones*stack[-1].get('sigma', self.sigma) mu = _const.mu_0*ones*stack[-1].get('mu_r', 1) # a perfect conductor and dc are the two limits the models below cannot evaluate. # Both are put back at the end, so the placeholders substituted here only have to # keep the intermediate results finite. perfect = np.isinf(sigma) dc = self.frequency.f == 0 sigma = np.where(perfect, 1., sigma) # placeholder value for perfect conductor f = np.where(dc, 1., self.frequency.f) # placeholder value for dc, to avoid division by zero w = 2*np.pi*f # dc resistance, where the current is spread uniformly over the conductor Rdc = 1/(np.pi*r**2*sigma) if t is None else 1/(2*np.pi*r*t)/sigma rms_roughness = [layer.get('rms_roughness', 0) for layer in stack] if len(stack) == 1 and not np.any(rms_roughness): Zs = np.sqrt(1j*w*mu/sigma) # a smooth bulk conductor, where Zs is a closed form else: # the wave impinges on the conductor from the dielectric filling, which is # therefore the outside medium of the stack materials = [self.dielectric] + [{k: v for k, v in layer.items() if k in ('sigma', 'mu_r', 'ep_r')} for layer in stack] Zs = surface_impedance(f, materials, rms_roughness = rms_roughness, boundary_loc = [layer.get('boundary_loc', 0) for layer in stack], distribution = [layer.get('distribution', 'norm') for layer in stack]) Zhf = Zs/(2*np.pi*r) # impedance per unit length of a conductor many skin depths deep if self.model == 'tesche': if t is None: Lint = mu/(8*np.pi) # eq. (11) of Tesche, solid rod else: # eq. (13) of Tesche, tube of outer radius c, written in terms of # q = (r/c)**2 so that a wall much thicker than r does not overflow q = (r/(r + t))**2 Lint = mu/(2*np.pi)*(np.log1p(t/r)/(1 - q)**2 + (q - 3)/(4*(1 - q))) # eq. (14) of Tesche Z = Rdc + Zhf/(1 + Zhf/(1j*w)/Lint) elif self.model == 'schelkunoff': # ive(n, z) is I_n(z) scaled by exp(-|Re(z)|) and kve(n, z) is K_n(z) scaled # by exp(z). Both scalings cancel in the ratios below, which keeps a conductor # many skin depths deep from over- and underflowing. g = np.sqrt(1j*w*mu*sigma) # propagation constant inside the metal # scipy returns nan past |z| = 2**31/2. Nothing real comes near it. deepest = 1e8/np.abs(g) x = g*np.minimum(r, deepest) if t is None: # eq. (65) of Schelkunoff, solid rod Z = Zhf*scipy.special.ive(0, x)/scipy.special.ive(1, x) else: # eq. (74) of Schelkunoff, tube, divided by I_1 of its outer surface so # that the wall enters only through Q, what the outer surface sends back. # Q carries the round trip exp(-2*g*t) across the wall, which underflows # to zero once the wall is many skin depths thick and leaves eq. (74) with # its outer radius at infinity. y, gt = g*np.minimum(r + t, deepest), g*np.minimum(t, deepest) Q = ( scipy.special.kve(1, y)/scipy.special.ive(1, y)*np.exp(-(gt + abs(gt.real))) ) Z = Zhf*((scipy.special.ive(0, x)*Q + scipy.special.kve(0, x)) /(scipy.special.kve(1, x) - scipy.special.ive(1, x)*Q)) else: raise ValueError('Unknown conductor model') return np.where(perfect, 0., np.where(dc, Rdc, Z)) @property def _Zc(self) -> NumberLike: """ Impedance per unit length of both conductors together, in Ohm/m. Returns ------- Zc : number, or array-like impedance per unit length, in Ohm/m """ return (self._conductor_impedance(self.a, None, self.inner_conductor) + self._conductor_impedance(self.b, self.tout, self.outer_conductor)) @property def _L_ext(self) -> NumberLike: """ External inductance, in H/m, i.e. the inductance of the field between the two conductors. It comes out complex for a magnetically lossy filling, the imaginary part being the magnetic loss, the same way a lossy dielectric makes the permittivity complex. """ return _const.mu_0*self.dielectric.get('mu_r', 1)/(2.*np.pi)*np.log(self.b/self.a) # derivation of distributed circuit parameters @property def R(self) -> NumberLike: """ Distributed resistance R, in Ohm/m. This is the loss of the two conductors, from their dc resistance up to the skin effect, plus the magnetic loss of the filling if there is any. See the references of :class:`Coaxial`. Returns ------- R : number, or array-like distributed resistance, in Ohm/m """ return self._Zc.real - self.frequency.w*np.imag(self._L_ext) @property def L(self) -> NumberLike: """ Distributed inductance L, in H/m This is the external inductance of the field between the two conductors, plus the internal inductance of the conductors themselves. The latter is dropped at dc, where it is not defined for an infinitely thick shield. Returns ------- L : number, or array-like distributed inductance, in H/m """ w = self.frequency.w Lint = np.divide(self._Zc.imag, w, out=np.zeros_like(w), where=w != 0) return np.real(self._L_ext) + Lint @property def C(self) -> NumberLike: """ Distributed capacitance C, in F/m Returns ------- C : number, or array-like distributed capacitance, in F/m """ return 2.*np.pi*self.epsilon_prime/np.log(self.b/self.a) @property def G(self) -> NumberLike: """ Distributed conductance G, in S/m Returns ------- G : number, or array-like distributed conductance, in S/m """ return 2*np.pi*self.frequency.w*self.epsilon_second/np.log(self.b/self.a) def __str__(self): f=self.frequency f_s = f'Coaxial Media. {f.f_scaled[0]}-{f.f_scaled[-1]}{f.unit}, '\ f'{f.npoints} points.' z0 = self.z0 z0_s = f'z0 = ({z0[0].real:.1f}, {z0[0].imag:.1f}j)-'\ f'({z0[-1].real:.1f}, {z0[-1].imag:.1f}j) Ohm' if self.z0_port is not None: z0_port = self.z0_port z0_port_s = \ f'z0_port = ({z0_port[0].real:.1f}, {z0_port[0].imag:.1f}j)-'\ f'({z0_port[-1].real:.1f}, {z0_port[-1].imag:.1f}j) Ohm\n' else: z0_port_s = 'z0_port is not defined.' try: output = \ f'Coaxial Media. {f_s}\n'\ f'Dint = {self.Dint * 1e3:.2f} mm,'\ f' Dout = {self.Dout * 1e3:.2f} mm\n' \ f'{z0_s}\n{z0_port_s}' except(TypeError): output = \ f'Coaxial Media. {f_s}\n'\ f'Dint = {self.Dint[0] * 1e3:.2f} mm,'\ f' Dout = {self.Dout[0] * 1e3:.2f} mm\n' \ f'{z0_s}\n{z0_port_s}' return output