Source code for skrf.media.media
"""
media (:mod:`skrf.media.media`)
========================================
Media class.
.. autosummary::
:toctree: generated/
Media
DefinedGammaZ0
"""
from __future__ import annotations
import re
import warnings
from abc import ABC, abstractmethod
from copy import deepcopy as copy
from numbers import Number
from pathlib import Path
import numpy as npy
from numpy import any, array, gradient, imag, ones, real
from scipy import stats
from scipy.constants import c
from .. import mathFunctions as mf
from ..constants import S_DEF_DEFAULT, ZERO, NumberLike, to_meters
from ..frequency import Frequency
from ..network import Network, connect, impedance_mismatch, innerconnect
[docs]
class Media(ABC):
"""
Abstract Base Class for a single mode on a transmission line media.
This class init's with `frequency` and `z0_port` (the port impedance);
attributes shared by all media. Methods defined here make use of the
properties :
* `gamma` - (complex) media propagation constant
* `z0` - (complex) media characteristic impedance
Which define the properties of a specific media. Any sub-class of Media
must implement these properties. `gamma` and `z0` should return
complex arrays of the same length as `frequency`. `gamma` must
follow the convention:
* positive real(gamma) = attenuation
* positive imag(gamma) = forward propagation
Parameters
----------
frequency : :class:`~skrf.frequency.Frequency` object or None
frequency band of this transmission line medium.
Defaults to None, which produces a 1-10ghz band with 101 points.
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` overrided by `z0_override`.
(Default is None)
z0 : number, array-like, or None
deprecated parameter, alias to `z0_override` if `z0_override` is None.
Emmit a deprecation warning.
Notes
-----
Full-wave simulations results can be networks with complex and
frequency-dependant `z0` port impedances.
However, the regular touchstone (s-parameters) files generated by most VNA
measurements are normalized to a single real value, e. g. 1. or 50 ohm.
Manually setting the `z0_port` to a single real value enforce the second
behaviour. The default (`z0_port = None`) is the first behaviour.
"""
[docs]
def __init__(self, frequency: Frequency | None = None,
z0_port: NumberLike | None = None,
z0_override: NumberLike | None = None,
z0: NumberLike | None = None):
if frequency is None:
frequency = Frequency(1,10,101,'GHz')
if z0 is not None:
# warns of deprecation
warnings.warn(
'Use of `z0` in Media initialization is deprecated.\n'
'If `z0_override` is None, '
'then `z0_override` will take z0 value.\n'
'`z0` will be removed of Media initialization in version 1.0',
DeprecationWarning, stacklevel = 2)
if z0_override is None:
z0_override = z0
self.frequency = frequency.copy()
self.z0_port = z0_port
self.z0_override = z0_override
[docs]
def mode(self, **kw) -> Media:
r"""
Create another mode in this medium.
Convenient way to return a copy this Media object
with eventually different properties.
Parameters
----------
\*\*kwargs : keyword arguments passed to the copy
Returns
-------
copy : :class:`Media`
A copy of this Media object with \*\*kwargs attribute
"""
out = copy(self)
for k in kw:
setattr(self, k, kw[k])
return out
[docs]
def copy(self) -> Media:
"""
Copy of this Media object.
Returns
-------
copy : :class:`Media`
A copy of this Media object
"""
return copy(self)
def __eq__(self,other):
"""
Test for numerical equality (up to :data:`~skrf.constants.ZERO`).
"""
if self.frequency != other.frequency:
return False
if max(abs(self.z0 - other.z0)) > ZERO:
return False
if max(abs(self.gamma - other.gamma)) > ZERO:
return False
if self.z0_port is None and other.z0_port is not None:
if max(abs(self.z0 - other.z0_port)) > ZERO:
return False
return True
def __len__(self) -> int:
"""
Length of frequency axis.
"""
return len(self.frequency)
@property
def npoints(self) -> int:
"""
Number of points of the frequency axis.
Returns
-------
npoints : int
Number of points of the frequency axis.
"""
return self.frequency.npoints
@npoints.setter
def npoints(self, val):
self.frequency.npoints = val
@property
def z0_port(self) -> npy.ndarray:
"""
Port Impedance.
Returns
-------
z0_port : :class:`numpy.ndarray`
"""
if self._z0_port is None:
return self._z0_port
return self._z0_port*ones(len(self))
@z0_port.setter
def z0_port(self, val):
self._z0_port = val
@property
def z0_override(self) -> npy.ndarray:
"""
Port Impedance.
Returns
-------
z0_port : :class:`numpy.ndarray`
"""
if self._z0_override is None:
return self._z0_override
return self._z0_override*ones(len(self))
@z0_override.setter
def z0_override(self, val):
self._z0_override = val
@property
@abstractmethod
def gamma(self):
r"""
Propagation constant.
In skrf, defined as :math:`\gamma = \alpha + j \beta`.
Returns
-------
gamma : :class:`numpy.ndarray`
complex propagation constant for this media
Notes
-----
`gamma` must adhere to the following convention:
* positive real(gamma) = attenuation
* positive imag(gamma) = forward propagation
"""
return None
@property
def alpha(self) -> npy.ndarray:
"""
Real (attenuation) component of gamma.
Returns
-------
alpha : :class:`numpy.ndarray`
"""
return real(self.gamma)
@property
def beta(self) -> npy.ndarray:
"""
Imaginary (propagating) component of gamma.
Returns
-------
beta : :class:`numpy.ndarray`
"""
return imag(self.gamma)
@property
@abstractmethod
def z0_characteristic(self):
"""
Characteristic Impedance, :math:`z_0`.
This abstract method has to be defined in the Media Class.
Returns
-------
z0_characteristic : npy.ndarray
Characteristic Impedance in units of ohms
"""
return None
@property
def z0(self):
"""
Return Characteristic Impedance `z0_characteristic`.
If `z0_override` is not None, it is returned instead.
Returns
-------
z0 : npy.ndarray
z0_characteristic or z0_override in units of ohms
"""
if self.z0_override is None:
return self.z0_characteristic
else:
return self.z0_override
# left for backward compatibility
@property
def Z0(self):
"""
Characteristic Impedance
Returns
-------
z0 : npy.ndarray
Characteristic Impedance in units of ohms
"""
# warns of deprecation
warnings.warn(
'Use of `Z0` (uppercase) is deprecated and return `z0`.\n'
'Use characteristic impedance `z0` (lowercase) instead.\n'
'`Z0` will be removed in version 1.0.',
DeprecationWarning, stacklevel = 2)
return self.z0
@property
def v_p(self) -> npy.ndarray:
r"""
Complex phase velocity (in m/s).
.. math::
j \cdot \omega / \gamma
Notes
-----
The `j` is used so that real phase velocity corresponds to
propagation
where:
* :math:`\omega` is angular frequency (rad/s),
* :math:`\gamma` is complex propagation constant (rad/m)
Returns
-------
v_p : :class:`numpy.ndarray`
See Also
--------
propagation_constant
gamma
"""
return 1j*(self.frequency.w/self.gamma)
@property
def v_g(self):
r"""
Complex group velocity (in m/s).
.. math::
j \cdot d \omega / d \gamma
where:
* :math:`\omega` is angular frequency (rad/s),
* :math:`\gamma` is complex propagation constant (rad/m)
Notes
-----
the `j` is used to make propagation real, this is needed because
skrf defined the gamma as :math:`\gamma= \alpha +j\beta`.
Returns
-------
v_g : :class:`numpy.ndarray`
References
----------
https://en.wikipedia.org/wiki/Group_velocity
See Also
--------
propagation_constant
v_p
gamma
"""
dw = self.frequency.dw
dk = gradient(self.gamma)
return dw/dk
[docs]
def get_array_of(self, x):
try:
if len(x)!= len(self):
# we have to make a decision
pass
except(TypeError):
y = x* ones(len(self))
return y
## Other Functions
[docs]
def theta_2_d(self, theta: NumberLike, deg:bool = True, bc: bool = True) -> NumberLike:
r"""
Convert electrical length to physical distance.
The electrical length is given by :math:`d=\theta/\beta`.
The given electrical length can be given either at the center frequency
or on the entire band depending of the parameter `bc`.
Parameters
----------
theta : number
electrical length, at band center (see deg for unit)
deg : Boolean, optional
is theta in degrees?
Default is True (theta is assumed in degrees)
bc : bool, optional.
evaluate only at band center, or across the entire band?
Default is True (evaluation assumed at band center)
Returns
--------
d : number, array-like
physical distance in meters
"""
if deg:
theta = mf.degree_2_radian(theta)
gamma = self.gamma
if bc:
return 1.0*theta/npy.imag(gamma[int(gamma.size/2)])
else:
return 1.0*theta/npy.imag(gamma)
[docs]
def electrical_length(self, d: NumberLike, deg: bool = False) -> NumberLike:
r"""
Calculate the complex electrical length for a given distance.
Electrical length is given by :math:`\theta=\gamma d`.
Parameters
----------
d: number or array-like
delay distance, in meters
deg: Boolean, optional
return electrical length in deg?
Default is False (returns electrical length in radians)
Returns
-------
theta: number or array-like
complex electrical length in radians or degrees, depending on
value of deg.
"""
gamma = self.gamma
if not deg:
return gamma*d
else:
return mf.radian_2_degree(gamma*d)
## Network creation
# lumped elements
[docs]
def match(self, nports: int = 1, z0: NumberLike | None = None,
z0_norm: bool = False, **kwargs) -> Network:
r"""
Perfect matched load (:math:`\Gamma_0 = 0`).
Parameters
----------
nports : int
number of ports
z0 : number, or array-like or None
port impedance. Default is
None, in which case the Media's :attr:`z0` is used.
This sets the resultant Network's
:attr:`~skrf.network.Network.z0`.
z0_norm : bool
is z0 normalized to this media's `z0`?
\*\*kwargs : key word arguments
passed to :class:`~skrf.network.Network` initializer
Returns
-------
match : :class:`~skrf.network.Network` object
a n-port match
Examples
--------
>>> my_match = my_media.match(2,z0 = 50, name='Super Awesome Match')
"""
result = Network(**kwargs)
result.frequency = self.frequency
result.s = npy.zeros((self.frequency.npoints, nports, nports), dtype=complex)
if z0 is None:
if self.z0_port is None:
z0 = self.z0
else:
z0 = self.z0_port
else:
if isinstance(z0, str):
z0 = parse_z0(z0)*self.z0
if z0_norm:
z0 = z0*self.z0
result.z0 = z0
return result
[docs]
def load(self, Gamma0: NumberLike, nports: int = 1,
z0: NumberLike | None = None, **kwargs) -> Network:
r"""
Load of given reflection coefficient.
Parameters
----------
Gamma0 : number, array-like
Reflection coefficient of load (linear, not in db). If its
an array it must be of shape: `kxnxn`, where k is number of frequency
points in media, and n is `nports`
nports : int
number of ports
z0 : number, or array-like or None
port impedance. Default is
None, in which case the Media's :attr:`z0` is used.
This sets the resultant Network's
:attr:`~skrf.network.Network.z0`.
\*\*kwargs : key word arguments
passed to :func:`match`, which is called initially to create a
'blank' network.
Returns
-------
load : :class:`~skrf.network.Network` object
n-port load, where S = Gamma0*eye(...)
See Also
--------
match
open
short
"""
result = self.match(nports, z0 = z0, **kwargs)
result.s = npy.array(Gamma0).reshape(-1, 1, 1) * \
npy.eye(nports, dtype=complex).reshape((-1, nports, nports)).\
repeat(self.frequency.npoints, 0)
return result
[docs]
def short(self, nports: int = 1,
z0: NumberLike | None = None, **kwargs) -> Network:
r"""
Short (:math:`\Gamma_0 = -1`)
For s_def = 'power' (:math:`-Z_{ref}^*/Z_{ref}`)
Parameters
----------
nports : int
number of ports
\*\*kwargs : key word arguments passed to :func:`load`.
z0 : number, or array-like or None
port impedance. Default is
None, in which case the Media's :attr:`z0` is used.
This sets the resultant Network's
:attr:`~skrf.network.Network.z0`.
Returns
-------
match : :class:`~skrf.network.Network` object
a n-port short circuit
Notes
-----
This calls ::
load(-1.0, nports, **kwargs)
See Also
--------
match
open
load
"""
if z0 is None:
if self.z0_port is None:
z0 = self.z0
else:
z0 = self.z0_port
s_short = -1
# Powerwave short is not necessarily -1
if kwargs.get('s_def', S_DEF_DEFAULT) == 'power':
s_short = -npy.conjugate(z0) / z0
return self.load(s_short, nports, z0 = z0, **kwargs)
[docs]
def open(self, nports: int = 1, **kwargs) -> Network:
r"""
Open (:math:`\Gamma_0 = 1`).
Parameters
----------
nports : int
number of ports
\*\*kwargs : key word arguments passed to :func:`load`
Returns
-------
match : :class:`~skrf.network.Network` object
a n-port open circuit
Notes
-----
This calls ::
load(1.0, nports, **kwargs)
See Also
--------
match
load
short
"""
return self.load(1.0, nports, **kwargs)
[docs]
def resistor(self, R: NumberLike, *args, **kwargs) -> Network:
r"""
Resistor.
Parameters
----------
R : number, array
Resistance , in Ohms. If this is an array, must be of
same length as frequency vector.
\*args, \*\*kwargs : arguments, key word arguments
passed to :func:`match`, which is called initially to create a
'blank' network.
Returns
-------
resistor : a 2-port :class:`~skrf.network.Network`
See Also
--------
match
short
open
load
capacitor
inductor
"""
result = self.match(nports=2, **kwargs)
y = npy.zeros(shape=result.s.shape, dtype=complex)
R = npy.array(R)
y[:, 0, 0] = 1.0 / R
y[:, 1, 1] = 1.0 / R
y[:, 0, 1] = -1.0 / R
y[:, 1, 0] = -1.0 / R
result.y = y
return result
[docs]
def capacitor(self, C: NumberLike, **kwargs) -> Network:
r"""
Capacitor.
Parameters
----------
C : number, array
Capacitance, in Farads. If this is an array, must be of
same length as frequency vector.
\*\*kwargs : key word arguments
passed to :func:`match`, which is called initially to create a
'blank' network.
Returns
-------
capacitor : a 2-port :class:`~skrf.network.Network`
See Also
--------
match
short
open
load
resistor
inductor
"""
result = self.match(nports=2, **kwargs)
w = self.frequency.w
y = npy.zeros(shape=result.s.shape, dtype=complex)
C = npy.array(C)
y[:, 0, 0] = 1j * w * C
y[:, 1, 1] = 1j * w * C
y[:, 0, 1] = -1j * w * C
y[:, 1, 0] = -1j * w * C
result.y = y
return result
[docs]
def inductor(self, L: NumberLike, **kwargs) -> Network:
r"""
Inductor.
Parameters
----------
L : number, array
Inductance, in Henries. If this is an array, must be of
same length as frequency vector.
\*\*kwargs : key word arguments
passed to :func:`match`, which is called initially to create a
'blank' network.
Returns
-------
inductor : a 2-port :class:`~skrf.network.Network`
See Also
--------
match
short
open
load
capacitor
resistor
"""
result = self.match(nports=2, **kwargs)
w = self.frequency.w
y = npy.zeros(shape=result.s.shape, dtype=complex)
L = npy.array(L)
y[:, 0, 0] = 1.0 / (1j * w * L)
y[:, 1, 1] = 1.0 / (1j * w * L)
y[:, 0, 1] = -1.0 / (1j * w * L)
y[:, 1, 0] = -1.0 / (1j * w * L)
result.y = y
return result
[docs]
def impedance_mismatch(self, z1: NumberLike, z2: NumberLike, **kwargs) -> Network:
r"""
Two-port network for an impedance mismatch.
Parameters
----------
z1 : number, or array-like
complex impedance of port 1
z2 : number, or array-like
complex impedance of port 2
\*\*kwargs : key word arguments
passed to :func:`match`, which is called initially to create a
'blank' network.
Returns
-------
missmatch : :class:`~skrf.network.Network` object
a 2-port network representing the impedance mismatch
Notes
-----
If z1 and z2 are arrays, they must be of same length
as the :attr:`Media.frequency.npoints`
See Also
--------
match
short
open
load
capacitor
inductor
resistor
"""
result = self.match(nports=2, **kwargs)
s_def = kwargs.get('s_def', S_DEF_DEFAULT)
z1 = npy.array(z1)
z2 = npy.array(z2)
mismatch = npy.broadcast_to(impedance_mismatch(z1, z2, s_def), result.s.shape)
result.s = mismatch
return result
# splitter/couplers
[docs]
def tee(self, **kwargs) -> Network:
r"""
Ideal, lossless tee. (3-port splitter).
Parameters
----------
\*\*kwargs : key word arguments
passed to :func:`match`, which is called initially to create a
'blank' network.
Returns
-------
tee : :class:`~skrf.network.Network` object
a 3-port splitter
See Also
----------
splitter : this just calls splitter(3)
match : called to create a 'blank' network
"""
return self.splitter(3, **kwargs)
[docs]
def splitter(self, nports: int, **kwargs) -> Network:
r"""
Ideal, lossless n-way splitter.
The port impedances can be mismatched and the power is split
accordingly.
For n > 2, the splitter is not matched because the power wave entering
one port meet the equivalent impedance of the other ports in parallel.
.. math::
S_{ii} = \frac{Y_i - \sum_{j\neq i} Y_j}{Y_i + \sum_{j\neq i} Y_j}
= \frac{2Y_i}{\sum_{j=1...n} Y_j} - 1
The remaining power is split between the other ports depending their
impedances.
.. math::
\sum_{j=1...n} S_{ij}^2 = 1 - S_{ii}^2
.. math::
S_{ij} = \frac{2\sqrt{Y_i \cdot Y_j}}{\sum_{k=1...n} Y_{k}}
Parameters
----------
nports : int
number of ports
\*\*kwargs : key word arguments
passed to :func:`match`, which is called initially to create a
'blank' network.
Returns
-------
tee : :class:`~skrf.network.Network` object
a n-port splitter
See Also
--------
match : called to create a 'blank' network
"""
result = self.match(nports, **kwargs)
y0s = npy.array(1./result.z0)
y_k = y0s.sum(axis=1)
s = npy.zeros((self.frequency.npoints, nports, nports),
dtype='complex')
s = 2 *npy.sqrt(npy.einsum('ki,kj->kij', y0s, y0s)) / y_k[:, None, None]
npy.einsum('kii->ki', s)[:] -= 1 # Sii
result.s = s
return result
# transmission line
[docs]
def to_meters(self, d: NumberLike, unit: str = 'deg') -> NumberLike:
"""
Translate various units of distance into meters.
This is a method of media to allow for electrical lengths as
inputs. For dispersive media, mean group velocity is used to
translate time-based units to distance.
Parameters
----------
d : number or array-like
the value
unit : str
the unit to that x is in:
['deg','rad','m','cm','um','in','mil','s','us','ns','ps']
Returns
-------
d_m : number, array-like
d in meters
See Also
--------
skrf.constants.to_meters
"""
unit = unit.lower()
#import pdb;pdb.set_trace()
d_dict ={'deg':self.theta_2_d(d,deg=True),
'rad':self.theta_2_d(d,deg=False),
}
if unit in d_dict:
return d_dict[unit]
else:
# mean group velocity is used to translate time-based
# units to distance
if 's' in unit:
# they are specifying a time unit so calculate
# the group velocity. (note this fails for media of
# too little points, as it uses gradient)
v_g = -self.v_g.imag.mean()
else:
v_g = c
return to_meters(d=d,unit=unit, v_g=v_g)
[docs]
def thru(self,
z0: NumberLike | None = None,
**kwargs) -> Network:
r"""
Matched transmission line of length 0.
Parameters
----------
z0 : number, or array-like or None
port impedance. Default is
None, in which case the Media's :attr:`z0` is used.
This sets the resultant Network's
:attr:`~skrf.network.Network.z0`.
\*\*kwargs : key word arguments
passed to :func:`match`, which is called initially to create a
'blank' network.
Returns
-------
thru : :class:`~skrf.network.Network` object
matched transmission line of 0 length
See Also
--------
line : this just calls line(0)
open, short, match
"""
if z0 is None:
if self.z0_port is None:
z0 = self.z0
else:
z0 = self.z0_port
return self.line(0, z0 = z0, **kwargs)
[docs]
def line(self, d: NumberLike, unit: str = 'deg',
z0: NumberLike | str | None = None, embed: bool = False, **kwargs) -> Network:
r"""
Transmission line of a given length and impedance.
The units of `length` are interpreted according to the value
of `unit`. If `z0` is not None, then a line specified impedance
is produced. if `embed` is also True, then the line is embedded
in this media's z0 environment, creating a mismatched line.
Parameters
----------
d : number
the length of transmission line (see unit argument)
unit : ['deg','rad','m','cm','um','in','mil','s','us','ns','ps']
the units of d. See :func:`to_meters`, for details
z0 : number, string, or array-like or None
the characteristic impedance of the line, if different
from self.z0. To set z0 in terms of normalized impedance,
pass a string, like `z0='1+.2j'`
embed : bool
deprecated parameter that only emmit a warning if set to true.
\*\*kwargs : key word arguments
passed to :func:`match`, which is called initially to create a
'blank' network.
Returns
-------
line : :class:`~skrf.network.Network` object
matched transmission line of given length
Examples
--------
>>> my_media.line(1, 'mm', z0=100)
>>> my_media.line(90, 'deg', z0='2') # set z0 as normalized impedance
"""
if embed:
# warns of deprecation
warnings.warn(
'Use of `embed` is deprecated and has no more effect.\n'
'Lines are renormalized to the media port impedance `z0_port`'
'if it is not None, otherwise they use characterisic'
'impedance `z0`.\n'
'`embed` will be removed in version 1.0',
DeprecationWarning, stacklevel = 2)
if isinstance(z0,str):
z0 = parse_z0(z0)* self.z0
if z0 is None:
z0 = self.z0
s_def = kwargs.pop('s_def', S_DEF_DEFAULT)
# The use of either traveling or pseudo waves s-parameters definition
# is required here.
# The definition of the reflection coefficient for power waves has
# conjugation.
result = self.match(nports=2, z0 = z0, s_def='traveling', **kwargs)
theta = self.electrical_length(self.to_meters(d=d, unit=unit))
s11 = npy.zeros(self.frequency.npoints, dtype=complex)
s21 = npy.exp(-1*theta)
result.s = \
npy.array([[s11, s21],[s21,s11]]).transpose().reshape(-1,2,2)
# renormalize (or embed) into z0_port if required
if self.z0_port is not None:
result.renormalize(self.z0_port)
result.renormalize(result.z0, s_def=s_def)
return result
[docs]
def delay_load(self, Gamma0: NumberLike, d: Number, unit: str = 'deg', **kwargs) -> Network:
r"""
Delayed load.
A load with reflection coefficient `Gamma0` at the end of a
matched line of length `d`.
Parameters
----------
Gamma0 : number, array-like
reflection coefficient of load (not in dB)
d : number
the length of transmission line (see unit argument)
unit : ['deg','rad','m','cm','um','in','mil','s','us','ns','ps']
the units of d. See :func:`to_meters`, for details
\*\*kwargs : key word arguments
passed to :func:`line` and :func:`load`
Returns
-------
delay_load : :class:`~skrf.network.Network` object
a delayed load
Examples
----------
>>> my_media.delay_load(-.5, 90, 'deg', Z0=50)
Notes
-----
This calls ::
line(d, unit, **kwargs) ** load(Gamma0, **kwargs)
See Also
--------
line : creates the network for line
load : creates the network for the load
delay_short
delay_open
"""
return self.line(d=d, unit=unit, **kwargs) ** self.load(Gamma0=Gamma0,
**kwargs)
[docs]
def delay_short(self, d: Number, unit: str = 'deg', **kwargs) -> Network:
r"""
Delayed Short.
A transmission line of given length terminated with a short.
Parameters
----------
d : number
the length of transmission line (see unit argument)
unit : ['deg','rad','m','cm','um','in','mil','s','us','ns','ps']
the units of d. See :func:`to_meters`, for details
\*\*kwargs : key word arguments passed to :func:`delay_load`.
Returns
-------
delay_short : :class:`~skrf.network.Network` object
a delayed short
Notes
-----
This calls::
delay_load(Gamma0=-1.0, d=d, unit=unit, **kwargs)
See Also
--------
delay_load
delay_open
"""
return self.delay_load(Gamma0=-1.0, d=d, unit=unit, **kwargs)
[docs]
def delay_open(self, d: Number, unit: str = 'deg', **kwargs) -> Network:
r"""
Delayed open transmission line.
Parameters
----------
d : number
the length of transmission line (see unit argument)
unit : ['deg','rad','m','cm','um','in','mil','s','us','ns','ps']
the units of d. See :func:`to_meters`, for details
\*\*kwargs : key word arguments passed to :func:`delay_load`
Returns
-------
delay_open : :class:`~skrf.network.Network` object
a delayed open
Notes
-----
This calls::
delay_load(Gamma0=1.0, d=d, unit=unit, **kwargs)
See Also
--------
delay_load
delay_short
"""
return self.delay_load(Gamma0=1.0, d=d, unit=unit, **kwargs)
[docs]
def shunt(self, ntwk: Network, **kwargs) -> Network:
r"""
Shunts a :class:`~skrf.network.Network`.
This creates a :func:`tee` and connects
`ntwk` to port 1, and returns the result.
Parameters
----------
ntwk : :class:`~skrf.network.Network` object
\*\*kwargs : keyword arguments passed to :func:`tee`
Returns
-------
shunted_ntwk : :class:`~skrf.network.Network` object
a shunted a ntwk. The resultant shunted_ntwk will have
(2 + ntwk.number_of_ports -1) ports.
See Also
--------
shunt_delay_load
shunt_delay_open
shunt_delay_short
shunt_resistor
shunt_capacitor
shunt_inductor
"""
return connect(self.tee(**kwargs), 1, ntwk, 0)
[docs]
def shunt_delay_load(self, *args, **kwargs) -> Network:
r"""
Shunted delayed load.
Parameters
----------
\*args,\*\*kwargs : arguments, keyword arguments
passed to func:`delay_load`
Returns
--------
shunt_delay_load : :class:`~skrf.network.Network` object
a shunted delayed load (2-port)
Notes
-----
This calls::
shunt(delay_load(*args, **kwargs))
See Also
--------
shunt
shunt_delay_open
shunt_delay_short
shunt_resistor
shunt_capacitor
shunt_inductor
"""
return self.shunt(self.delay_load(*args, **kwargs), **kwargs)
[docs]
def shunt_delay_open(self,*args,**kwargs) -> Network:
r"""
Shunted delayed open.
Parameters
----------
\*args,\*\*kwargs : arguments, keyword arguments
passed to func:`delay_open`
Returns
-------
shunt_delay_open : :class:`~skrf.network.Network` object
shunted delayed open (2-port)
Notes
-----
This calls::
shunt(delay_open(*args, **kwargs))
See Also
--------
shunt
shunt_delay_load
shunt_delay_short
shunt_resistor
shunt_capacitor
shunt_inductor
"""
return self.shunt(self.delay_open(*args, **kwargs), **kwargs)
[docs]
def shunt_delay_short(self, *args, **kwargs) -> Network:
r"""
Shunted delayed short.
Parameters
----------
\*args,\*\*kwargs : arguments, keyword arguments
passed to func:`delay_short`
Returns
-------
shunt_delay_load : :class:`~skrf.network.Network` object
shunted delayed open (2-port)
Notes
-----
This calls::
shunt(delay_short(*args, **kwargs))
See Also
--------
shunt
shunt_delay_load
shunt_delay_open
shunt_capacitor
shunt_inductor
"""
return self.shunt(self.delay_short(*args, **kwargs), **kwargs)
[docs]
def shunt_resistor(self, R: NumberLike, **kwargs) -> Network:
r"""
Shunted resistor.
Parameters
----------
R : number, array-like
Resistor in Ohm.
\*\*kwargs : arguments, keyword arguments
passed to func:`resistor`
Returns
-------
shunt_resistor : :class:`~skrf.network.Network` object
shunted resistor (2-port)
Notes
-----
This calls::
shunt(resistor(R, **kwargs) ** short())
See Also
--------
shunt
shunt_delay_load
shunt_delay_open
shunt_delay_short
shunt_inductor
shunt_capacitor
"""
return self.shunt(self.resistor(R=R, **kwargs) **
self.short(**kwargs), **kwargs)
[docs]
def shunt_capacitor(self, C: NumberLike, **kwargs) -> Network:
r"""
Shunted capacitor.
Parameters
----------
C : number, array-like
Capacitance in Farads.
\*\*kwargs : arguments, keyword arguments
passed to func:`capacitor`
Returns
-------
shunt_capacitor : :class:`~skrf.network.Network` object
shunted capacitor (2-port)
Notes
-----
This calls::
shunt(capacitor(C, **kwargs) ** short())
See Also
--------
shunt
shunt_delay_load
shunt_delay_open
shunt_delay_short
shunt_resistor
shunt_inductor
"""
return self.shunt(self.capacitor(C=C, **kwargs) **
self.short(**kwargs), **kwargs)
[docs]
def shunt_inductor(self, L: NumberLike, **kwargs) -> Network:
r"""
Shunted inductor.
Parameters
----------
L : number, array-like
Inductance in Henries.
\*\*kwargs : arguments, keyword arguments
passed to func:`inductor`
Returns
-------
shunt_inductor : :class:`~skrf.network.Network` object
shunted inductor(2-port)
Notes
-----
This calls::
shunt(inductor(L, **kwargs) ** short())
See Also
--------
shunt
shunt_delay_load
shunt_delay_open
shunt_delay_short
shunt_resistor
shunt_capacitor
"""
return self.shunt(self.inductor(L=L, **kwargs) **
self.short(**kwargs), **kwargs)
[docs]
def capacitor_q(self, C: NumberLike, f_0: NumberLike, q_factor: NumberLike, **kwargs) -> Network:
r"""
Capacitor with Q factor.
Parameters
----------
C : number, array-like
Capacitance in Farads.
f_0 : number
Frequency at which Q is defined, in Hz.
q_factor : number
Q-factor of capacitor
\*\*kwargs : arguments, keyword arguments
passed to func:`capacitor`
Returns
-------
capacitor_q : :class:`~skrf.network.Network` object
capacitor_q (2-port)
"""
idea_cap = self.shunt(self.capacitor(C=C, **kwargs), **kwargs)
rac = q_factor / (C * 2 * npy.pi * f_0)
idea_res = self.shunt(self.resistor(R=rac), **kwargs)
return innerconnect(connect(idea_cap, 1, idea_res, 2), 1, 3)
[docs]
def inductor_q(self,
L: NumberLike,
f_0: NumberLike,
q_factor: NumberLike,
rdc: NumberLike = 0.0,
**kwargs) -> Network:
r"""
Inductor with Q factor.
Parameters
----------
L : number, array-like
Inductance in Henries.
f_0 : number
Frequency at which Q is defined, in Hz.
q_factor : number
Q-factor of inductor
rdc: number, optional
DC resistance, in Ohms. Default is 0 Ohm.
\*\*kwargs : arguments, keyword arguments
passed to func:`inductor`
Returns
-------
inductor_q : :class:`~skrf.network.Network` object
inductor_q (2-port)
"""
w_q = 2 * npy.pi * f_0
if rdc == 0.0:
rdc = 0.05 * w_q * L / q_factor
rq1 = w_q * L / q_factor
rq2 = npy.sqrt(rq1**2 - rdc**2)
qt = w_q * L / rq2
rac = self.frequency.w * L / qt
r1 = npy.sqrt(rdc**2 + rac**2)
return self.inductor(L=L, **kwargs) ** self.resistor(R=r1)
[docs]
def attenuator(self, s21: NumberLike, db: bool = True, d: Number = 0,
unit: str = 'deg', name: str = '', **kwargs) -> Network:
r"""
Ideal matched attenuator of a given length.
Parameters
----------
s21 : number, array-like
the attenuation
db : bool, optional
is s21 in dB? otherwise assumes linear. Default is True (dB).
d : number, optional
length of attenuator. Default is 0.
unit : ['deg','rad','m','cm','um','in','mil','s','us','ns','ps']
the units of d. See :func:`to_meters`, for details.
Default is 'deg'
name : str
Name for the returned attenuator Network
\*\*kwargs : arguments, keyword arguments
passed to func:`line`
Returns
-------
ntwk : :class:`~skrf.network.Network` object
2-port attenuator
"""
s21 = npy.array(s21)
if db:
s21 = mf.db_2_magnitude(s21)
result = self.match(nports=2)
result.s[:, 0, 1] = s21
result.s[:, 1, 0] = s21
result = result ** self.line(d=d, unit=unit, **kwargs)
result.name = name
return result
[docs]
def lossless_mismatch(self, s11: NumberLike, db: bool = True, **kwargs) -> Network:
r"""
Lossless, symmetric mismatch defined by its return loss.
Parameters
----------
s11 : complex number, number, or array-like
the reflection coefficient. if db==True, then phase is ignored
db : bool, optional
is s11 in db? otherwise assumes linear. Default is True (dB)
\*\*kwargs : arguments, keyword arguments
passed to func:`match`
Returns
-------
ntwk : :class:`~skrf.network.Network` object
2-port lossless mismatch
"""
result = self.match(nports=2, **kwargs)
s11 = npy.array(s11)
if db:
s11 = mf.db_2_magnitude(s11)
result.s[:, 0, 0] = s11
result.s[:, 1, 1] = s11
s21_mag = npy.sqrt(1 - npy.abs(s11) ** 2)
s21_phase = npy.angle(s11) + npy.pi / 2 * (npy.angle(s11) <= 0) - npy.pi / 2 * (npy.angle(s11) > 0)
result.s[:, 0, 1] = s21_mag * npy.exp(1j * s21_phase)
result.s[:, 1, 0] = result.s[:, 0, 1]
return result
[docs]
def isolator(self, source_port: int = 0, **kwargs) -> Network:
r"""
Two-port isolator.
Parameters
-------------
source_port: int in [0,1], optional
port at which power can flow from. Default is 0.
\*\*kwargs : arguments, keyword arguments
passed to func:`thru`
Returns
-------
ntwk : :class:`~skrf.network.Network` object
2-port isolator
"""
result = self.thru(**kwargs)
if source_port == 0:
result.s[:, 0, 1] = 0
elif source_port == 1:
result.s[:, 1, 0] = 0
return result
## Noisy Networks
[docs]
def white_gaussian_polar(self, phase_dev: Number, mag_dev: Number,
n_ports: int = 1, **kwargs) -> Network:
r"""
Complex zero-mean gaussian white-noise network.
Creates a network whose s-matrix is complex zero-mean gaussian
white-noise, of given standard deviations for phase and
magnitude components.
This 'noise' network can be added to networks to simulate
additive noise.
Parameters
----------
phase_mag : number
standard deviation of magnitude
phase_dev : number
standard deviation of phase
n_ports : int
number of ports.
\*\*kwargs : passed to :class:`~skrf.network.Network`
initializer
Returns
--------
result : :class:`~skrf.network.Network` object
a noise network
"""
shape = (self.frequency.npoints, n_ports,n_ports)
phase_rv= stats.norm(loc=0, scale=phase_dev).rvs(size = shape)
mag_rv = stats.norm(loc=0, scale=mag_dev).rvs(size = shape)
result = Network(**kwargs)
result.frequency = self.frequency
result.s = mag_rv*npy.exp(1j*phase_rv)
return result
[docs]
def random(self, n_ports: int = 1, reciprocal: bool = False, matched: bool = False,
symmetric: bool = False, **kwargs) -> Network:
r"""
Complex random network.
Creates a n-port network whose s-matrix is filled with random
complex numbers. Optionally, result can be matched or reciprocal.
Parameters
----------
n_ports : int
number of ports.
reciprocal : bool
makes s-matrix symmetric ($S_{mn} = S_{nm}$)
symmetric : bool
makes s-matrix diagonal have single value ($S_{mm}=S_{nn}$)
matched : bool
makes diagonals of s-matrix zero
\*\*kwargs : passed to :class:`~skrf.network.Network`
initializer
Returns
-------
result : :class:`~skrf.network.Network` object
the network
"""
result = self.match(nports = n_ports, **kwargs)
result.s = mf.rand_c(self.frequency.npoints, n_ports,n_ports)
if reciprocal and n_ports>1:
for m in range(n_ports):
for n in range(n_ports):
if m>n:
result.s[:,m,n] = result.s[:,n,m]
if symmetric:
for m in range(n_ports):
for n in range(n_ports):
if m==n:
result.s[:,m,n] = result.s[:,0,0]
if matched:
for m in range(n_ports):
for n in range(n_ports):
if m==n:
result.s[:,m,n] = 0
return result
## OTHER METHODS
[docs]
def extract_distance(self, ntwk: Network) -> NumberLike:
"""
Determines physical distance from a transmission or reflection Network.
Given a matched transmission or reflection measurement the
physical distance is estimated at each frequency point based on
the scattering parameter phase of the ntwk and propagation constant.
Notes
-----
If the Network is a reflect measurement, the returned distance will
be twice the physical distance.
Parameters
----------
ntwk : `Network`
A one-port network of either the reflection or the transmission.
Returns
-------
d : number or array_like
physical distance
Examples
--------
>>> air = rf.air50
>>> l = air.line(1, 'cm')
>>> d_found = air.extract_distance(l.s21)
>>> d_found
"""
if ntwk.nports ==1:
dphi = gradient(ntwk.s_rad_unwrap.flatten())
dgamma = gradient(self.gamma.imag)
return -dphi/dgamma
else:
raise ValueError('ntwk must be one-port. Select s21 or s12 for a two-port.')
[docs]
def write_csv(self, filename: str = 'f,gamma,Z0,z0.csv'):
"""
write this media's frequency, gamma, Z0, and z0 to a csv file.
Parameters
----------
filename : string, optional
file name to write out data to.
Default is 'f,gamma,Z0,z0.csv', so you probably want to specify it.
See Also
--------
from_csv : class method to initialize Media object from a
csv file written from this function
"""
header = 'f[%s], Re(z0), Im(z0), Re(gamma), Im(gamma), Re(z0_port), Im(z0_port)\n'%self.frequency.unit
g,z,pz = self.gamma, \
self.z0, self.z0_port if self.z0_port is not None else self.z0
data = npy.vstack(\
[self.frequency.f_scaled, z.real, z.imag, \
g.real, g.imag, pz.real, pz.imag]).T
npy.savetxt(filename,data,delimiter=',',header=header)
[docs]
class DefinedGammaZ0(Media):
"""
A media directly defined by its propagation constant and characteristic impedance.
Parameters
----------
frequency : :class:`~skrf.frequency.Frequency` object or None
frequency band of this transmission line medium.
Default is None, which produces a 1-10ghz band with 101 points.
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)
gamma : number or array-like, optional
complex propagation constant. `gamma` must adhere to
the following convention:
* positive real(gamma) = attenuation
* positive imag(gamma) = forward propagation
Default is 1j (lossless).
z0 : number or array-like, optional.
complex characteristic impedance of the media.
Default is 50 ohm.
Z0 : number, array-like, or None
deprecated parameter, only emmit a deprecation warning.
"""
[docs]
def __init__(self, frequency: Frequency | None = None,
z0_port: NumberLike | None = None,
z0: NumberLike = 50,
Z0: NumberLike | None = None,
gamma: NumberLike = 1j):
super().__init__(frequency=frequency, z0_port=z0_port)
self.gamma= gamma
self.z0_characteristic = z0
if Z0 is not None:
# warns of deprecation
warnings.warn(
'Use of `Z0` in DefinedGammaZ0 initialization is deprecated.\n'
'`Z0` has no effect. Use `z0` instead\n'
'`Z0` will be removed in version 1.0',
DeprecationWarning, stacklevel = 2)
[docs]
@classmethod
def from_csv(cls, filename: str, *args, **kwargs) -> Media:
"""
Create a Media from numerical values stored in a csv file.
The csv file format must be written by the function :func:`write_csv`,
or similar method which produces the following format::
f[$unit], Re(Z0), Im(Z0), Re(gamma), Im(gamma), Re(port Z0), Im(port Z0)
1, 1, 1, 1, 1, 1, 1
2, 1, 1, 1, 1, 1, 1
.....
See Also
--------
write_csv
"""
try:
fid = open(filename)
except(TypeError):
# they may have passed a file
fid = filename
header = fid.readline()
# this is not the correct way to do this ... but whatever
f_unit = header.split(',')[0].split('[')[1].split(']')[0]
f,z_re,z_im,g_re,g_im,pz_re,pz_im = \
npy.loadtxt(fid, delimiter=',').T
if isinstance(filename, (str, Path)):
fid.close()
return cls(
frequency = Frequency.from_f(f, unit=f_unit),
z0 = z_re+1j*z_im,
gamma = g_re+1j*g_im,
z0_port = pz_re+1j*pz_im,
**kwargs
)
@property
def npoints(self):
return self.frequency.npoints
@npoints.setter
def npoints(self,val):
# this is done to trigger checks on vector lengths for
# gamma/Z0/z0
new_freq= self.frequency.copy()
new_freq.npoints = val
self.frequency = new_freq
@property
def frequency(self):
return self._frequency
@frequency.setter
def frequency(self, val):
if hasattr(self, '_frequency') and self._frequency is not None:
# they are updating the frequency, we may have to do something
attrs_to_test = [self._gamma, self._z0, self._z0_port]
if any([has_len(k) for k in attrs_to_test]):
raise NotImplementedError("updating a Media frequency, with non-constant "
"gamma/Z0/z0 is not worked out yet")
self._frequency = val
@property
def z0_characteristic(self):
"""
Characteristic Impedance, :math:`z_0`.
Returns
-------
z0_characteristic : npy.ndarray
Characteristic Impedance in units of ohms
"""
return self._z0*ones(len(self))
@z0_characteristic.setter
def z0_characteristic(self, val):
self._z0 = val
@property
def gamma(self):
"""
Propagation constant.
Returns
---------
gamma : :class:`numpy.ndarray`
complex propagation constant for this media
Notes
------
`gamma` must adhere to the following convention:
* positive real(gamma) = attenuation
* positive imag(gamma) = forward propagation
"""
return self._gamma*ones(len(self))
@gamma.setter
def gamma(self, val):
self._gamma = val
def has_len(x: NumberLike) -> bool:
"""
Test of x has any length (ie is a vector).
This is slightly non-trivial because [3] has len() but is
doesn't really have any length.
"""
try:
return (len(array(x))>1)
except TypeError:
return False
def parse_z0(s: str) -> NumberLike:
"""
Parse a z0 string.
Parameters
----------
s : str
z0 string, like '50+10j'
Returns
-------
z0 : npy.ndarray
Raises
------
ValueError
If could not arse the z0 string.
"""
# they passed a string for z0, try to parse it
re_numbers = re.compile(r'\d+')
numbers = re.findall(re_numbers, s)
if len(numbers)==2:
out = float(numbers[0]) +1j*float(numbers[1])
elif len(numbers)==1:
out = float(numbers[0])
else:
raise ValueError('couldnt parse z0 string')
return out