"""
rectangularWaveguide (:mod:`skrf.media.rectangularWaveguide`)
================================================================
Represents a single mode of a homogeneously filled rectangular
waveguide of cross-section `a` x `b`. The mode is determined by
`mode-type` (`'te'` or `'tm'`) and mode indices ( `m` and `n` ).
==================================== ============= ===============
Quantity Symbol Variable
==================================== ============= ===============
Characteristic Wave Number :math:`k_0` :attr:`k0`
Cut-off Wave Number :math:`k_c` :attr:`kc`
Longitudinal Wave Number :math:`k_z` :attr:`gamma`
Transverse Wave Number (a) :math:`k_x` :attr:`kx`
Transverse Wave Number (b) :math:`k_y` :attr:`ky`
Characteristic Impedance :math:`z_0` :attr:`z0`
==================================== ============= ===============
.. autosummary::
:toctree: generated/
RectangularWaveguide
"""
from __future__ import annotations
import warnings
from numbers import Number
from typing import TYPE_CHECKING
import numpy as np
from .. import constants as _const
from ..data import materials
from ..tlineFunctions import surface_impedance
from .media import Media
if TYPE_CHECKING:
from ..constants import NumberLike
from ..frequency import Frequency
[docs]
class RectangularWaveguide(Media):
r"""
A single mode of a homogeneously filled rectangular waveguide.
Parameters
----------
frequency : :class:`~skrf.frequency.Frequency` object
frequency band of this transmission line medium
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.
a : number, optional
width of waveguide, in meters.
Default is 1.
b : number or None, optional
height of waveguide, in meters.
If `None` defaults to a/2.
Default is None
mode_type : ['te','tm']
mode type, transverse electric (te) or transverse magnetic
(tm) to-z. where z is direction of propagation
m : int
mode index in 'a'-direction
n : int
mode index in 'b'-direction
ep_r : number, array-like,
filling material's relative permittivity
mu_r : number, array-like
filling material's relative permeability
rho : number, array-like, string, or None
resistivity (ohm-m) of the conductor walls. If array-like
must be same length as frequency. if str, it must be a key in
:data:`skrf.data.materials`.
Default is 1/58e6, the resistivity of annealed copper at 20 °C
(100% IACS). Use 0, or None, for perfectly conducting walls.
roughness : number, or array-like
rms roughness of the conductor walls, in meter. It is handed to
:func:`~skrf.tlineFunctions.surface_impedance`, and it applies to any wall
whose material does not carry an ``'rms_roughness'`` of its own.
model : str, optional
Model of the loss. 'lomakin' (default) is the two-wire model of
[#Lomakin]_, where the loss of the walls shapes the phase constant as well
as the attenuation (agrees better with EM simulations). 'marcuvitz' is the
power loss method of [#Marcuvitz]_, which is also given by the IEC 60153-2
and IEEE 1785.1 standards and discussed in [#Skinner]_; it leaves the phase
constant at its lossless value.
A TM mode, or a mode with both indices nonzero, is not covered by 'lomakin'
and follows 'marcuvitz' instead, which is warned about.
dielectric : dict or None, optional
Material filling the waveguide, as a dict taking the keys ``'ep_r'``
(complex relative permittivity) and ``'mu_r'`` (complex relative
permeability), both defaulting to 1. If None (default), the filling is
described by parameters `ep_r` and `mu_r`.
wall_a : dict, list of dict, or None, optional
Material of the pair of walls of width `a`. It describes both walls of the pair.
A single dict describes a bulk conductor, a list of dict a stack of coatings ordered
from the filling inwards, of which the deepest layer is the bulk. 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 pair is described by `rho` and `roughness`.
wall_b : dict, list of dict, or None, optional
Material of the pair of walls of width `b`, described in similar way as
`wall_a`. The two pairs are usually of the same material, but would have
different effective conductivities, e.g., different roughness due to
machining or 3D printed waveguides [#Zhu]_, [#Lomakin3D]_.
\*args, \*\*kwargs : arguments, keyword arguments
passed to :class:`~skrf.media.media.Media`'s constructor
(:func:`~skrf.media.media.Media.__init__`
Note
----
The two-wire model is derived for the TE10 mode. It carries over to any
TE_m0 and TE_0n mode. It does not carry over to a mode with both indices nonzero.
References
----------
.. [#Lomakin] K. Lomakin, G. Gold and K. Helmreich, "Analytical Waveguide Model
Precisely Predicting Loss and Delay Including Surface Roughness," IEEE Trans.
Microw. Theory Techn., vol. 66, no. 6, pp. 2649-2662, June 2018.
https://doi.org/10.1109/TMTT.2018.2827383
.. [#Marcuvitz] N. Marcuvitz, "Transmission-line modes," in Waveguide Handbook,
2nd ed. New York: McGraw-Hill, 1951, Chapter 2.
.. [#Skinner] J. Skinner, D. Koller, H.-U. Nickel, N. M. Ridler and S. Lucyszyn,
"Derivation of Rectangular Metallic Waveguide Attenuation Constant for
IEC 60153-2 and IEEE 1785.1 International Standards," IEEE Trans. THz Sci.
Technol., vol. 15, no. 4, pp. 734-737, July 2025.
https://doi.org/10.1109/TTHZ.2025.3573847
.. [#Zhu] L. Zhu et al., "3-D Printed THz Waveguide Components," IEEE Access,
vol. 11, pp. 79073-79086, 2023.
https://doi.org/10.1109/ACCESS.2023.3297271
.. [#Lomakin3D] K. Lomakin et al., "SLA-Printed 3-D Waveguide Paths for E-Band
Using Electroless Silver Plating," IEEE Trans. Compon. Packag. Manuf.
Technol., vol. 9, no. 12, pp. 2476-2481, Dec. 2019.
https://doi.org/10.1109/TCPMT.2019.2927671
Examples
--------
Most common usage is standard aspect ratio (2:1) dominant
mode, TE10 mode of wr10 waveguide can be constructed by
>>> import numpy as np
>>> import skrf as rf
>>> from skrf.constants import mil
>>> from skrf.media import RectangularWaveguide
>>> freq = rf.Frequency(75, 110, 101, unit='ghz')
>>> RectangularWaveguide(freq, a=100*mil)
Rectangular Waveguide Media. 75.0-110.0 GHz. 101 points
a= 2.54e-03m, b= 1.27e-03m
A WR-12 guide of brass, with the `a` walls rougher than the `b` ones,
and the attenuation of a 100 mm length of it at 90 GHz:
>>> freq = rf.Frequency(60, 90, 61, unit='GHz')
>>> wr12 = RectangularWaveguide(freq, a=3.0988e-3, b=1.5494e-3,
... wall_a={'sigma': 0.28*58e6, 'rms_roughness': 1e-6},
... wall_b={'sigma': 0.28*58e6, 'rms_roughness': 0.4e-6})
>>> print(f"{wr12.line(100, 'mm').s_db[-1, 1, 0]:.3f} dB")
-1.002 dB
A WR-6.5 waveguide (D-band) whose walls 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(110, 170, 61, unit='GHz')
>>> wr65 = RectangularWaveguide(freq, a=1.6510e-3, b=0.8255e-3,
... wall_a=enig, wall_b=enig)
>>> smooth_copper = RectangularWaveguide(freq, a=1.6510e-3, b=0.8255e-3, rho=1/58e6)
>>> np2db = 20*np.log10(np.e)
>>> print(f"{np2db*wr65.gamma[-1].real:.1f} dB/m, against "
... f"{np2db*smooth_copper.gamma[-1].real:.1f} dB/m for smooth copper")
20.3 dB/m, against 4.6 dB/m for smooth copper
"""
[docs]
def __init__(self, frequency: Frequency | None = None,
z0_port: NumberLike | None = None,
z0_override: NumberLike | None = None,
z0: NumberLike | None = None,
a: float = 1, b: float | None = None,
mode_type: str = 'te', m: int = 1, n: int = 0,
ep_r: None | NumberLike = 1, mu_r: None | NumberLike = 1,
rho: None | NumberLike | str = 1/58e6,
roughness: None | NumberLike = None,
model: str = 'lomakin',
dielectric: dict | None = None,
wall_a: dict | list[dict] | None = None,
wall_b: dict | list[dict] | None = None,
*args, **kwargs):
Media.__init__(self, frequency = frequency,
z0_port = z0_port, z0_override = z0_override, z0 = z0)
if b is None:
b = a/2.
if mode_type.lower() not in ['te','tm']:
raise ValueError('mode_type must be either \'te\' or \'tm\'')
# a TE mode needs one nonzero index and a TM mode needs both, the field being
# identically zero otherwise. Neither is a mode at any size or frequency.
if (m == 0 and n == 0) or (mode_type.lower() == 'tm' and m*n == 0):
raise ValueError(f'{mode_type.upper()}{m}{n} is not a mode: a TE mode needs '
'one nonzero index and a TM mode needs both')
if model.lower() not in ['lomakin','marcuvitz']:
raise ValueError('model must be either \'lomakin\' or \'marcuvitz\'')
self.a = a
self.b = b
self.mode_type = mode_type.lower()
self.m = m
self.n = n
self.model = model.lower()
self.wall_a, self.wall_b = wall_a, wall_b
self.dielectric = {'ep_r': ep_r, 'mu_r': mu_r} if dielectric is None else dielectric
self.ep_r = self.dielectric.get('ep_r', 1)
self.mu_r = self.dielectric.get('mu_r', 1)
self.rho = rho
self.roughness = roughness
# warning when describing one wall pair and not the other
if (wall_a is None) != (wall_b is None):
given, missing = (('wall_b', 'wall_a') if wall_a is None
else ('wall_a', 'wall_b'))
warnings.warn(
f'Only {given} was given a material, so {missing} stays a smooth bulk '
f'conductor of rho = {self.rho} ohm*m and unit relative permeability. '
f'Pass {missing} as well to describe both wall pairs.',
UserWarning, stacklevel = 2)
# warning when the mode falls outside of what the chosen model covers.
if self._model_used != self.model:
warnings.warn(
f"The '{self.model}' model does not cover the "
f"{self.mode_type.upper()}{self.m}{self.n} mode, so the "
f"'{self._model_used}' model is used instead. Pass "
f"model='{self._model_used}' to select it explicitly.",
UserWarning, stacklevel = 2)
def __str__(self):
f=self.frequency
output = (
f'Rectangular Waveguide Media. {f.f_scaled[0]}-{f.f_scaled[-1]} {f.unit}. {f.npoints} points'
f'\n a= {self.a:.2e}m, b= {self.b:.2e}m')
return output
def __repr__(self):
return self.__str__()
[docs]
@classmethod
def from_z0(cls, frequency: Frequency, z0: NumberLike, f: Number,
ep_r=1, mu_r=1, **kw) -> Media:
"""
Initialize from specified impedance at a given frequency, assuming
the fundamental TE10 mode.
Parameters
----------
frequency : Frequency Object
z0 : number /array
characteristic impedance to create at `f`
f : number
frequency (in Hz) at which the resultant waveguide has the
characteristic impedance z0
ep_r : number, array-like,
filling material's relative permittivity
mu_r : number, array-like
filling material's relative permeability
"""
mu = _const.mu_0*mu_r
ep = _const.epsilon_0*ep_r
w = 2*np.pi*f
a = np.pi/(w*mu) * 1./np.sqrt(1/(z0*1j)**2+ep/mu)
kw.update(dict(frequency=frequency,a=a, m=1, n=0, ep_r=ep_r, mu_r=mu_r))
return cls(**kw)
@property
def ep(self) -> NumberLike:
r"""
The permittivity of the filling material.
.. math::
\varepsilon = \varepsilon_r \varepsilon_0
Returns
-------
ep : number
filling material's permittivity in F/m.
"""
return self.ep_r * _const.epsilon_0
@property
def mu(self) -> NumberLike:
r"""
The permeability of the filling material.
.. math::
\mu = \mu_r \mu_0
Returns
-------
mu : number
filling material's permeability in H/m.
"""
return self.mu_r * _const.mu_0
@property
def k0(self) -> NumberLike:
r"""
Characteristic wave number.
.. math::
k_0 = \frac{\omega}{v} = \omega \sqrt{\varepsilon \mu}
Returns
-------
k0 : number
characteristic wave number
"""
return 2*np.pi*self.frequency.f*np.sqrt(self.ep * self.mu)
@property
def ky(self) -> NumberLike:
r"""
Eigenvalue in the `b` direction.
Defined as
.. math::
k_y = n \frac{\pi}{b}
Returns
-------
ky : number
eigenvalue in `b` direction
"""
return self.n*np.pi/self.b
@property
def kx(self) -> NumberLike:
r"""
Eigenvalue in the 'a' direction.
Defined as
.. math::
k_x = m \frac{\pi}{a}
Returns
-------
kx : number
eigenvalue in `a` direction
"""
return self.m*np.pi/self.a
@property
def kc(self) -> NumberLike:
r"""
Cut-off wave number.
Defined as
.. math::
k_c = \sqrt {k_x^2 + k_y^2} = \sqrt {
{m \frac{\pi}{a}}^2 + {n \frac{\pi}{b}}^2}
Returns
-------
kc : number
cut-off wavenumber
"""
return np.sqrt( self.kx**2 + self.ky**2)
@property
def f_cutoff(self) -> NumberLike:
r"""
cutoff frequency for this mode.
.. math::
f_c = \frac{v}{2 \pi} \sqrt {
{m \frac{\pi}{a}}^2 + {n \frac{\pi}{b}}^2}
where :math:`v= 1/\sqrt{\varepsilon \mu}`.
"""
v = 1/np.sqrt(self.ep*self.mu)
return v* self.kc/(2*np.pi)
@property
def f_norm(self) -> NumberLike:
"""
Frequency vector normalized to cutoff.
"""
return self.frequency.f/self.f_cutoff
@property
def rho(self) -> None | NumberLike:
"""
Resistivity of all four walls in ohm*m.
This is the bulk resistivity of the metal, and it describes any wall pair
that `wall_a` or `wall_b` does not describe itself. Roughness is
not folded into it. Zero, or None, is a perfect conductor.
Parameters
----------
val : float, array-like, str or None
the resistivity in ohm*m. If array-like must be same length
as self.frequency. if str, it must be a key in
:data:`skrf.data.materials`.
Examples
---------
>>> import numpy as np
>>> import skrf as rf
>>> from skrf.media import RectangularWaveguide
>>> wg = RectangularWaveguide(rf.Frequency(75, 110, 101, unit='GHz'), a=2.54e-3)
>>> wg.rho = 2.8e-8
>>> wg.rho = 2.8e-8*np.ones(len(wg.frequency))
>>> wg.rho = 'al'
>>> wg.rho = 'aluminum'
>>> wg.rho = 0
"""
return self._rho
@rho.setter
def rho(self, val: None | NumberLike | str):
if isinstance(val, str):
self._rho = materials[val.lower()]['resistivity(ohm*m)']
else:
self._rho = val
@property
def lambda_guide(self) -> NumberLike:
r"""
Guide wavelength.
.. math::
\lambda_g = \frac{2\pi}{\beta}
The distance in which the phase of the field increases by 2 pi.
See Also
--------
k0
"""
return 2*np.pi/self.beta
@property
def lambda_cutoff(self) -> NumberLike:
r"""
Cutoff wavelength.
.. math::
\lambda_c = v/f_c
where :math:`v= 1/\sqrt{\varepsilon \mu}` and :math:`f_c` the cut-off frequency.
See Also
--------
f_cutoff
"""
v = 1/np.sqrt(self.ep*self.mu)
return v/self.f_cutoff
@property
def _model_used(self) -> str:
"""
The loss model actually used, which is `model` itself unless the mode
falls outside of what that model covers and it has to fall back.
The two-wire model is derived for TE_m0 and TE_0n modes only, so exactly
one of the two indices has to be zero.
"""
covers_mode = {'lomakin': self.mode_type == 'te' and (self.m == 0) != (self.n == 0),
'marcuvitz': True}
return self.model if covers_mode[self.model] else 'marcuvitz'
def _wall_impedance(self, wall: dict | list[dict] | None) -> NumberLike:
r"""
Surface impedance of one pair of walls, in Ohm per square.
Parameters
----------
wall : dict, list of dict, or None
material stack of the wall, as described in
:class:`RectangularWaveguide`. None falls back to `rho` and `roughness`.
Returns
-------
Zs : number, or array-like
surface impedance, in Ohm per square
"""
# the surface impedance cannot be evaluated at dc, where no mode exists
# anyway. A small placeholder there keeps it continuous with the rest of the
# band instead of dividing by zero.
f = np.where(self.frequency.f == 0, 1e-6, self.frequency.f)
w = 2*np.pi*f
if wall is None:
if self.rho is None or np.all(np.equal(self.rho, 0)):
return np.zeros_like(f) # perfectly conducting walls
stack = [{'sigma': 1/np.asarray(self.rho)}]
else:
stack = [wall] if isinstance(wall, dict) else list(wall)
# a layer that does not name a roughness of its own takes the one of the
# roughness parameter
default = 0 if self.roughness is None else self.roughness
rms_roughness = [layer.get('rms_roughness', default) for layer in stack]
if len(stack) == 1 and not np.any(rms_roughness):
# a smooth bulk conductor, where the surface impedance has a closed form
sigma = np.ones_like(f)*stack[-1].get('sigma', np.inf)
mu = _const.mu_0*np.ones_like(f)*stack[-1].get('mu_r', 1)
return np.sqrt(1j*w*mu/sigma)
# the wave impinges on the wall from the filling, which is therefore the
# outside medium of the stack
layers = [self.dielectric] + [{k: v for k, v in layer.items()
if k in ('sigma', 'mu_r', 'ep_r')} for layer in stack]
return surface_impedance(f, layers, rms_roughness = rms_roughness,
boundary_loc = [layer.get('boundary_loc', 0) for layer in stack],
distribution = [layer.get('distribution', 'norm') for layer in stack])
@property
def _rotated(self) -> tuple:
"""
The guide turned so that the field varies along `A` and not along `B`, as
both the two-wire model and the power loss method are written for. Returns
the two dimensions and the surface impedance of the matching wall pairs,
`Zs_A` being that of the pair of width `A`.
Only a TE_0n mode is actually turned. Any other mode is already in this
orientation, or has no expression of its own and only borrows the TE_m0 one.
"""
Zs_a = self._wall_impedance(self.wall_a)
Zs_b = self._wall_impedance(self.wall_b)
if self.m == 0 and self.n != 0:
# a TE_0n mode is the TE_m0 one of the guide turned on its side, so the
# two dimensions and the two pairs of walls exchange roles
return self.b, self.a, Zs_b, Zs_a
return self.a, self.b, Zs_a, Zs_b
@property
def _ZY(self) -> tuple:
r"""
Series impedance in Ohm/m and shunt admittance in S/m of the mode, as the
model in use states them. `gamma` and `z0_characteristic` follow from the
pair the same way for every model, so this is the one place a model is
written. See the references of :class:`RectangularWaveguide`.
"""
# the shunt branch shorts out at dc, where no mode exists anyway
w = np.where(self.frequency.w == 0, 2*np.pi*1e-6, self.frequency.w)
if self._model_used == 'lomakin':
A, B, Zs_A, Zs_B = self._rotated
# the longitudinal current scales with the dimension the field does not
# vary along, not with the one it does
Z = 1j*w*self.mu + 2/B*Zs_A
Z2 = (1j*w*self.mu + 2/B*Zs_A + 4/A*Zs_B)/self.kc**2
return Z, 1j*w*self.ep + 1/Z2
# the power loss method keeps the walls out of the branches and perturbs the
# lossless gamma by their loss instead, so one branch is the filling alone
# and the other is whatever reproduces that gamma
gamma = np.sqrt(np.asarray(self.kc**2 - self.k0**2, dtype=complex)) + self.alpha_c
if self.mode_type == 'te':
return 1j*w*self.mu, gamma**2/(1j*w*self.mu)
return gamma**2/(1j*w*self.ep), 1j*w*self.ep
@property
def gamma(self) -> NumberLike:
r"""
The propagation constant (aka Longitudinal wave number).
Defined as
.. math::
k_z = \sqrt {k_c^2 - k_0^2}
With a lossless filling this is purely imaginary above cutoff, where the
mode propagates, and purely real below it, where the mode is evanescent.
A lossy filling or lossy walls make it complex throughout.
Returns
-------
gamma : number
The propagation constant
"""
Z, Y = self._ZY
return np.sqrt(Z*Y)
@property
def alpha_c(self) -> NumberLike:
r"""
Loss of the walls by the power loss method, in Np/m. This is ch. 2, eq. (14a)
of Marcuvitz, which the IEC 60153-2 and IEEE 1785.1 standards give for the
attenuation constant. See the references of :class:`RectangularWaveguide`.
.. math::
\alpha = \frac{R_{s,a}
+ \frac{2b}{a}\left(\frac{f_c}{f}\right)^2 R_{s,b}}
{\eta b \sqrt{1 - \left(\frac{f_c}{f}\right)^2}}
where :math:`\eta` is the intrinsic impedance of the filling and :math:`R_s`
the surface resistance of a pair of walls. The two pairs are carried
separately, unlike ch. 2, eq. (14a) as published, which the same material on
both recovers.
It is derived for TE10 and holds for any TE_m0 and TE_0n mode, the mode
entering only through :math:`f_c`. Any other mode falls back to it and is
only estimated, its `b` walls also carrying longitudinal current. It
diverges at cutoff and is zero below it, where the evanescent decay of the
mode dwarfs the loss of the walls.
See Also
--------
rho
"""
f_n = self.f_norm
# below cutoff the power loss method cannot be evaluated, and the conductor
# loss is negligible anyway against the evanescent decay of the mode
propagating = f_n > 1.
f_n = np.where(propagating, f_n, 2.) # placeholder, discarded below
A, B, Zs_A, Zs_B = self._rotated
eta = np.sqrt(self.mu/self.ep)
alpha = (Zs_A.real + 2.*B/A*(1/f_n)**2*Zs_B.real)/(eta*B*np.sqrt(1-(1/f_n)**2))
return np.where(propagating, alpha, 0.)
@property
def z0_characteristic(self) -> NumberLike:
"""
The characteristic impedance, :math:`z_0`.
The characteristic impedance depends of the mode ('te' or 'tm').
Returns
-------
z0_characteristic : np.ndarray
Characteristic Impedance in units of ohms
"""
Z, Y = self._ZY
return np.sqrt(Z/Y)