"""
taper (:mod:`skrf.taper`)
=========================
Taper Objects
Tapered transformers, or tapers in short, are used to match
one impedance to another (from Z1 to Z2) [#]_.
By tapering a transmission line, a very broadband impedance match (low VSWR)
can be realized over a wide bandwidth, the longer the taper, the wider the frequency band.
References
----------
.. [#] https://www.microwaves101.com/encyclopedias/tapered-transformers
.. autosummary::
:toctree: generated/
Taper1D
Linear
Exponential
SmoothStep
Klopfenstein
"""
from __future__ import annotations
from collections.abc import Callable
from numbers import Number
from numpy import arccosh, array, cosh, exp, heaviside, linspace, log, sqrt
from scipy.integrate import quad
from scipy.special import iv
from .network import cascade_list
[docs]
class Taper1D:
"""
Generic 1D Taper Object
"""
[docs]
def __init__(
self,
med,
start: Number,
stop: Number,
n_sections: int,
f: Callable,
length: Number,
length_unit: str = "m",
param: str = "z0",
f_is_normed: bool = True,
med_kw: dict = None,
f_kw: dict = None,
):
"""
Generic 1D Taper Constructor.
Parameters
----------
med : :class:`~skrf.media.media.Media`
A :class:`~skrf.media.media.Media` object or a `@classmethod` `__init__`,
used to generate the transmission line.
See `med_kw` for arguments.
Examples:
* `skrf.media.RectangularWaveguide` # a class
* `skrf.media.RectangularWaveguide.from_z0` # an init
start : number
starting value for `param`
stop : number
stop value for `param`
n_sections : int
number of sections in taper
f : Callable
function defining the taper transition. must take either
no arguments or take `(x,length, start, stop)`.
see `f_is_normed` arguments
length : number
physical length of the taper (in `length_unit`)
length_unit : str
unit of length variable. see `skrf.to_meters`
param : str
name of the parameter of `med` that varies along the taper
f_is_normed: bool
is `f` scalable and normalized. ie can f just be scaled
to fit different start/stop conditions? if so then f is
called with no arguments, and must have domain and raings
of [0,1], and [0,1]
f_kw : dict
passed to `f()` when called
med_kw : dict
passed to `med.__init__` when an instance is created
Note
----
The default behaviour should is to taper based on impedance.
To do this we inspect the `med` class for a `from_z0`
init method, and if it exists, we assign it to `med` attribute,
in `__init__`.
Admittedly having `med` be a class or a method is abuse,
it makes for a intuitive operation.
Examples
--------
Create a linear taper from 100 to 1000mil
>>> from skrf import Frequency, RectangularWaveguide, Taper1D, mil, inch
>>> taper = Taper1D(med=RectangularWaveguide,
param='a',
start=100*mil,
stop=1000*mil,
length=1*inch,
n_sections=20,
f=lambda x: x,
f_is_normed=True,
med_kw={'frequency':Frequency(75,110,101,'ghz')})
"""
if f_kw is None:
f_kw = {}
if med_kw is None:
med_kw = {}
self.med = med
self.param = param
self.start = start
self.stop = stop
self.f = f
self.f_is_normed = f_is_normed
self.length = length
self.length_unit = length_unit
self.n_sections = n_sections
self.med_kw = med_kw
self.f_kw = f_kw
# the default behaviour should be to taper based on impedance.
# to do this we inspect the media class for a `from_z0`
# init method, and if it exists, we assign it to `med` attribute
# admittedly having `med` be a class or a method is abuse,
# it makes for a intuitive operation
if param == "z0":
if hasattr(self.med, "from_z0"):
self.med = self.med.from_z0
def __str__(self) -> str:
return "Taper: {classname}: {param} from {start}-{stop}"
@property
def section_length(self) -> Number:
"""
Section length.
Returns
-------
l : number
"""
return 1.0 * self.length / self.n_sections
@property
def value_vector(self):
if self.f_is_normed:
x = linspace(0, 1, self.n_sections)
y = self.f(x, **self.f_kw) * (self.stop - self.start) + self.start
else:
x = linspace(0, self.length, self.n_sections)
y = self.f(x, self.length, self.start, self.stop, **self.f_kw)
return y
[docs]
def section_at(self, val: Number):
"""
Create a single section of the taper with parameter value `val`.
Parameters
----------
val : number
parameter value
Returns
-------
media : :class:`~skrf.network.Network`
Network instance for the section of the taper
for the given parameter value
"""
return self.media_at(val).line(self.section_length, unit=self.length_unit)
@property
def medias(self) -> list:
"""
List of medias.
Returns
-------
medias : list of :class:`~skrf.media.media.Media`
"""
return [self.media_at(k) for k in self.value_vector]
@property
def sections(self) -> list:
"""
List of sections.
Returns
-------
sections : list of :class:`~skrf.network.Network`
"""
return [self.section_at(k) for k in self.value_vector]
@property
def network(self):
"""
Resulting Network
Returns
-------
ntwk : :class:`~skrf.network.Network`
"""
return cascade_list(self.sections)
[docs]
class Linear(Taper1D):
"""
A linear Taper.
Defined by :math:`f(x)=x`
"""
[docs]
def __init__(self, **kw):
opts = dict(f=lambda x: x, f_is_normed=True)
kw.update(opts)
super().__init__(**kw)
[docs]
class Exponential(Taper1D):
r"""
An Exponential Taper.
Defined by :math:`f(x) = f_0 \exp\left[ \frac{x}{x_1} \ln\left( \frac{f_1}{f_0} \right) \right]`
where:
* :math:`f_0`: start param value
* :math:`f_1`: stop param value
* :math:`x`: independent variable (position along taper)
* :math:`x_1`: length of taper
"""
[docs]
def __init__(self, **kw):
"""
Exponential Taper Constructor
"""
def f(x, length, start, stop):
return start * exp(x / length * (log(stop / start)))
opts = dict(f=f, f_is_normed=False)
kw.update(opts)
super().__init__(**kw)
[docs]
class SmoothStep(Taper1D):
"""
A smoothstep Taper.
There is no analytical basis for this in the EE world that I know
of. it is just a reasonable smooth curve, that is easy to implement.
:math:`f(x) = (3 x^2 - 2x^3)`
References
----------
https://en.wikipedia.org/wiki/Smoothstep
"""
[docs]
def __init__(self, **kw):
"""
Smoothstep Taper Constructor.
"""
def f(x):
return 3 * x**2 - 2 * x**3
opts = dict(f=f, f_is_normed=True)
kw.update(opts)
super().__init__(**kw)
[docs]
class Klopfenstein(Taper1D):
"""
Klopfenstein Taper.
This impedance taper was first described by R. W. Klopfenstein [#microwaves101]_ in a paper titled
"A Transmission Line Taper of Improved Design", published in 1956 [#Klopfenstein]_ .
A correction to Klopfenstein's math was published by in May 1973 by Darko Kajfez and Jame Prewit [#Kajfez]_ .
Parameters
----------
* rmax: maximum reflection coefficient in the pass band in percentage of :math:`\rho_0`
References
----------
.. [#microwaves101] https://www.microwaves101.com/encyclopedias/klopfenstein-taper
.. [#Klopfenstein] R. W. Klopfenstein Proc. IRE. vol. 44 pp. 31-35 Jan. 1956.
doi: 10.1109/JRPROC.1956.274847
https://ieeexplore.ieee.org/document/4051841
.. [#Kajfez] D. Kajfez and J. O. Prewitt, "Correction to "A Transmission Line Taper of Improved Design" (Letters),"
in IEEE Transactions on Microwave Theory and Techniques, vol. 21, no. 5, pp. 364-364, May 1973,
doi: 10.1109/TMTT.1973.1128003.
https://ieeexplore.ieee.org/document/1128003
"""
def _phi(self, z: list[Number], a: Number):
r"""
:math:`\phi` function
"""
return array([quad(lambda y: iv(1, a * sqrt(1 - y**2)) / a / sqrt(1 - y**2), 0, zi)[0] for zi in z])
[docs]
def __init__(self, **kw):
"""
Klopfenstein Taper Constructor.
"""
def f(x, length, start, stop, **kwargs):
rmax = kwargs.get("rmax", 0.05)
z = x - length / 2
log_ratio = log(stop / start) / 2
a = arccosh(1 / rmax)
log_value = log(start * stop) / 2 + log_ratio / cosh(a) * (
a * a * self._phi(2 * z / length, a) + heaviside(z - length / 2, 1) + heaviside(z + length / 2, 1) - 1
)
return exp(log_value)
opts = dict(f=f, f_is_normed=False)
kw.update(opts)
super().__init__(**kw)