1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800
|
"""
.. currentmodule:: skrf.frequency
========================================
frequency (:mod:`skrf.frequency`)
========================================
Provides a frequency object and related functions.
Most of the functionality is provided as methods and properties of the
:class:`Frequency` Class.
Frequency Class
===============
.. autosummary::
:toctree: generated/
Frequency
Functions
=========
.. autosummary::
:toctree: generated/
overlap_freq
Misc
====
.. autosummary::
:toctree: generated/
InvalidFrequencyWarning
"""
from __future__ import annotations
import re
import warnings
from numbers import Number
import numpy as np
from numpy import (
geomspace,
gradient, # used to center attribute `t` at 0
linspace,
pi,
)
from .constants import FREQ_UNITS, ZERO, FrequencyUnitT, NumberLike, SweepTypeT
from .util import Axes, axes_kwarg, find_nearest_index, slice_domain
class InvalidFrequencyWarning(UserWarning):
"""Thrown if frequency values aren't monotonously increasing
"""
pass
class Frequency:
"""
A frequency band.
The frequency object provides a convenient way to work with and
access a frequency band. It contains a frequency vector as well as
a frequency unit. This allows a frequency vector in a given unit
to be available (:attr:`f_scaled`), as well as an absolute frequency
axis in 'Hz' (:attr:`f`).
A Frequency object can be created from either (start, stop, npoints)
using the default constructor, :func:`__init__`. Or, it can be
created from an arbitrary frequency vector by using the class
method :func:`from_f`.
Internally, the frequency information is stored in the `f` property
combined with the `unit` property. All other properties, `start`
`stop`, etc are generated from these.
"""
unit_dict = {k.lower(): k for k in FREQ_UNITS}
"""
Dictionary to convert unit string with correct capitalization for display.
"""
multiplier_dict={k.lower(): v for k,v in FREQ_UNITS.items()}
"""
Frequency unit multipliers.
"""
def __init__(self, start: float = 0, stop: float = 0, npoints: int = 0,
unit: FrequencyUnitT | None = None, sweep_type: SweepTypeT = 'lin') -> None:
"""
Frequency initializer.
Creates a Frequency object from start/stop/npoints and a unit.
Alternatively, the class method :func:`from_f` can be used to
create a Frequency object from a frequency vector instead.
Parameters
----------
start : number, optional
start frequency in units of `unit`. Default is 0.
stop : number, optional
stop frequency in units of `unit`. Default is 0.
npoints : int, optional
number of points in the band. Default is 0.
unit : string, optional
Frequency unit of the band: 'Hz', 'kHz', 'MHz', 'GHz', 'THz'.
This is used to create the attribute :attr:`f_scaled`.
It is also used by the :class:`~skrf.network.Network` class
for plots vs. frequency. Default is 'Hz'.
sweep_type : string, optional
Type of the sweep: 'lin' or 'log'.
'lin' for linear and 'log' for logarithmic. Default is 'lin'.
Note
----
The attribute `unit` sets the frequency multiplier, which is used
to scale the frequency when `f_scaled` is referenced.
Note
----
The attribute `unit` is not case sensitive.
Hence, for example, 'GHz' or 'ghz' is the same.
See Also
--------
from_f : constructs a Frequency object from a frequency
vector instead of start/stop/npoints.
:attr:`unit` : frequency unit of the band
Examples
--------
>>> wr1p5band = Frequency(start=500, stop=750, npoints=401, unit='ghz')
>>> logband = Frequency(1, 1e9, 301, sweep_type='log')
"""
if unit is None:
warnings.warn('''
Frequency unit not passed: uses 'Hz' per default.
''',
DeprecationWarning, stacklevel=2)
unit = 'Hz'
self._unit = unit.lower()
start = self.multiplier * start
stop = self.multiplier * stop
if npoints == 0:
self._f = np.array([])
elif sweep_type.lower() == 'lin':
self._f = linspace(start, stop, npoints)
elif sweep_type.lower() == 'log' and start > 0:
self._f = geomspace(start, stop, npoints)
else:
raise ValueError('Sweep Type not recognized')
def __str__(self) -> str:
"""
"""
try:
output = \
'%s-%s %s, %i pts' % \
(self.f_scaled[0], self.f_scaled[-1], self.unit, self.npoints)
except (IndexError):
output = "[no freqs]"
return output
def __repr__(self) -> str:
"""
"""
return self.__str__()
def __getitem__(self, key: str | int | slice) -> Frequency:
"""
Slices a Frequency object based on an index, or human readable string.
Parameters
----------
key : str, int, or slice
if int, then it is interpreted as the index of the frequency
if str, then should be like '50.1-75.5ghz', or just '50'.
If the frequency unit is omitted then :attr:`unit` is
used.
Examples
--------
>>> b = rf.Frequency(50, 100, 101, 'ghz')
>>> a = b['80-90ghz']
>>> a.plot_s_db()
"""
output = self.copy()
if isinstance(key, str):
# they passed a string try and do some interpretation
re_hyphen = re.compile(r'\s*-\s*')
re_letters = re.compile('[a-zA-Z]+')
freq_unit = re.findall(re_letters,key)
if len(freq_unit) == 0:
freq_unit = self.unit
else:
freq_unit = freq_unit[0]
key_nounit = re.sub(re_letters,'',key)
edges = re.split(re_hyphen,key_nounit)
edges_freq = Frequency.from_f([float(k) for k in edges],
unit = freq_unit)
if len(edges_freq) ==2:
slicer=slice_domain(output.f, edges_freq.f)
elif len(edges_freq)==1:
key = find_nearest_index(output.f, edges_freq.f[0])
slicer = slice(key,key+1,1)
else:
raise ValueError()
try:
output._f = np.array(output.f[slicer]).reshape(-1)
return output
except(IndexError) as err:
raise IndexError('slicing frequency is incorrect') from err
if output.f.shape[0] > 0:
output._f = np.array(output.f[key]).reshape(-1)
else:
output._f = np.empty(shape=(0))
return output
@classmethod
def from_f(cls, f: NumberLike, unit: FrequencyUnitT | None = None) -> Frequency:
"""
Construct Frequency object from a frequency vector.
The unit is set by kwarg 'unit'
Parameters
----------
f : scalar or array-like
frequency vector
*args, **kwargs : arguments, keyword arguments
passed on to :func:`__init__`.
Returns
-------
myfrequency : :class:`Frequency` object
the Frequency object
Raises
------
InvalidFrequencyWarning:
If frequency points are not monotonously increasing
Examples
--------
>>> f = np.linspace(75,100,101)
>>> rf.Frequency.from_f(f, unit='GHz')
"""
if np.isscalar(f):
f = [f]
temp_freq = cls(0,0,0,unit=unit)
temp_freq._f = np.asarray(f) * temp_freq.multiplier
temp_freq.check_monotonic_increasing()
return temp_freq
def __eq__(self, other: object) -> bool:
#return (list(self.f) == list(other.f))
# had to do this out of practicality
if not isinstance(other, self.__class__):
return False
if len(self.f) != len(other.f):
return False
elif len(self.f) == len(other.f) == 0:
return True
else:
return (max(abs(self.f-other.f)) < ZERO)
def __ne__(self,other: object) -> bool:
return (not self.__eq__(other))
def __len__(self) -> int:
"""
The number of frequency points
"""
return self.npoints
def __add__(self, other: Frequency | NumberLike) -> Frequency:
out = self.copy()
out._f = self.f + (other.f if isinstance(other, Frequency) else other)
return out
def __sub__(self, other: Frequency | NumberLike) -> Frequency:
out = self.copy()
out._f = self.f - (other.f if isinstance(other, Frequency) else other)
return out
def __mul__(self, other: Frequency | NumberLike) -> Frequency:
out = self.copy()
out._f = self.f * (other.f if isinstance(other, Frequency) else other)
return out
def __rmul__(self, other: Frequency | NumberLike) -> Frequency:
out = self.copy()
out._f = self.f * (other.f if isinstance(other, Frequency) else other)
return out
def __div__(self, other: Frequency | NumberLike) -> Frequency:
out = self.copy()
out._f = self.f / (other.f if isinstance(other, Frequency) else other)
return out
def __truediv__(self, other: Frequency | NumberLike) -> Frequency:
out = self.copy()
out._f = self.f / (other.f if isinstance(other, Frequency) else other)
return out
def __floordiv__(self, other: Frequency | NumberLike) -> Frequency:
out = self.copy()
out._f = self.f // (other.f if isinstance(other, Frequency) else other)
return out
def __mod__(self, other: Frequency | NumberLike) -> Frequency:
out = self.copy()
out._f = self.f % (other.f if isinstance(other, Frequency) else other)
return out
def check_monotonic_increasing(self) -> None:
"""Validate the frequency values
Raises
------
InvalidFrequencyWarning:
If frequency points are not monotonously increasing
"""
increase = np.diff(self.f) > 0
if not increase.all():
warnings.warn("Frequency values are not monotonously increasing!\n"
"To get rid of the invalid values call `drop_non_monotonic_increasing`",
InvalidFrequencyWarning, stacklevel=2)
def drop_non_monotonic_increasing(self) -> list[int]:
"""Drop duplicate and invalid frequency values and return the dropped indices
Returns:
list[int]: The dropped indices
"""
invalid = np.diff(self.f, prepend=self.f[0]-1) <= 0
self._f = self._f[~invalid]
return list(np.flatnonzero(invalid))
@property
def start(self) -> float:
"""
Starting frequency in Hz.
"""
return self.f[0]
@property
def start_scaled(self) -> float:
"""
Starting frequency in :attr:`unit`'s.
"""
return self.f_scaled[0]
@property
def stop_scaled(self) -> float:
"""
Stop frequency in :attr:`unit`'s.
"""
return self.f_scaled[-1]
@property
def stop(self) -> float:
"""
Stop frequency in Hz.
"""
return self.f[-1]
@property
def npoints(self) -> int:
"""
Number of points in the frequency.
"""
return len(self.f)
@property
def center(self) -> float:
"""
Center frequency in Hz.
Returns
-------
center : number
the exact center frequency in units of Hz
"""
return self.start + (self.stop-self.start)/2.
@property
def center_idx(self) -> int:
"""
Closes idx of :attr:`f` to the center frequency.
"""
return self.npoints // 2
@property
def center_scaled(self) -> float:
"""
Center frequency in :attr:`unit`'s.
Returns
-------
center : number
the exact center frequency in units of :attr:`unit`'s
"""
return self.start_scaled + (self.stop_scaled-self.start_scaled)/2.
@property
def step(self) -> float:
"""
The inter-frequency step size (in Hz) for evenly-spaced
frequency sweeps
See Also
--------
df : for general case
"""
if self.span == 0:
return 0.
else:
return self.span / (self.npoints - 1.)
@property
def step_scaled(self) -> float:
"""
The inter-frequency step size (in :attr:`unit`) for evenly-spaced
frequency sweeps.
See Also
--------
df : for general case
"""
if self.span_scaled == 0:
return 0.
else:
return self.span_scaled / (self.npoints - 1.)
@property
def span(self) -> float:
"""
The frequency span.
"""
return abs(self.stop-self.start)
@property
def span_scaled(self) -> float:
"""
The frequency span.
"""
return abs(self.stop_scaled-self.start_scaled)
@property
def f(self) -> np.ndarray:
"""
Frequency vector in Hz.
Returns
----------
f : :class:`numpy.ndarray`
The frequency vector in Hz
See Also
----------
f_scaled : frequency vector in units of :attr:`unit`
w : angular frequency vector in rad/s
"""
return self._f
@property
def f_scaled(self) -> np.ndarray:
"""
Frequency vector in units of :attr:`unit`.
Returns
-------
f_scaled : numpy.ndarray
A frequency vector in units of :attr:`unit`
See Also
--------
f : frequency vector in Hz
w : frequency vector in rad/s
"""
return self.f/self.multiplier
@property
def w(self) -> np.ndarray:
r"""
Angular frequency in radians/s.
Angular frequency is defined as :math:`\omega=2\pi f` [#]_
Returns
-------
w : :class:`numpy.ndarray`
Angular frequency in rad/s
References
----------
.. [#] https://en.wikipedia.org/wiki/Angular_frequency
See Also
--------
f_scaled : frequency vector in units of :attr:`unit`
f : frequency vector in Hz
"""
return 2*pi*self.f
@property
def df(self) -> np.ndarray:
"""
The gradient of the frequency vector.
Note
----
The gradient is calculated using::
`gradient(self.f)`
"""
return gradient(self.f)
@property
def df_scaled(self) -> np.ndarray:
"""
The gradient of the frequency vector (in unit of :attr:`unit`).
Note
----
The gradient is calculated using::
`gradient(self.f_scaled)`
"""
return gradient(self.f_scaled)
@property
def dw(self) -> np.ndarray:
"""
The gradient of the frequency vector (in radians).
Note
----
The gradient is calculated using::
`gradient(self.w)`
"""
return gradient(self.w)
@property
def unit(self) -> FrequencyUnitT:
"""
Unit of this frequency band.
Possible strings for this attribute are:
'Hz', 'kHz', 'MHz', 'GHz', 'THz'
Setting this attribute is not case sensitive.
Returns
-------
unit : string
String representing the frequency unit
"""
return self.unit_dict[self._unit]
@unit.setter
def unit(self, unit: FrequencyUnitT) -> None:
self._unit = unit.lower()
@property
def multiplier(self) -> float:
"""
Multiplier for formatting axis.
This accesses the internal dictionary `multiplier_dict` using
the value of :attr:`unit`
Returns
-------
multiplier : number
multiplier for this Frequencies unit
"""
return self.multiplier_dict[self._unit]
def copy(self) -> Frequency:
"""
Returns a new copy of this frequency.
"""
freq = Frequency.from_f(self.f, unit='Hz')
freq.unit = self.unit
return freq
def _t_padded(self, *, pad: int = 0, n: int | None = None, bandpass: bool | None = None) -> np.ndarray:
if bandpass is None:
bandpass = self.f[0] != 0
if n is None:
n = self.npoints + pad
n = n if bandpass else n * 2 - 1
if bandpass:
dt = 1 / (n * self.step)
t_stop = (n - 1) // 2 * dt
t_start = -t_stop - dt if n % 2 == 0 else (-n // 2 + 1) * dt
t = np.linspace(t_start, t_stop, num=n, endpoint=True)
else:
dt = 1 / (n * self.step)
t = np.linspace(-dt * (n // 2), dt * (n // 2), num=n, endpoint=True)
return t
@property
def t(self) -> np.ndarray:
"""
Time vector in s.
t_period = 2*(n-1)/f_step
"""
return self._t_padded(bandpass=True)
@property
def t_ns(self) -> np.ndarray:
"""
Time vector in ns.
t_period = 2*(n-1)/f_step
"""
return self.t*1e9
def round_to(self, val: FrequencyUnitT | Number = 'Hz') -> None:
"""
Round off frequency values to a specified precision.
This is useful for dealing with finite precision limitations of
VNA's and/or other software
Parameters
----------
val : string or number
if val is a string it should be a frequency :attr:`unit`
(ie 'Hz', 'MHz',etc). if its a number, then this returns
f = f-f%val
Examples
--------
>>> f = skrf.Frequency.from_f([.1,1.2,3.5],unit='Hz')
>>> f.round_to('Hz')
"""
if isinstance(val, str):
val = self.multiplier_dict[val.lower()]
self.f = np.round(self.f/val)*val
def overlap(self,f2: Frequency) -> Frequency:
"""
Calculates overlapping frequency between self and f2.
See Also
--------
overlap_freq
"""
return overlap_freq(self, f2)
@property
def sweep_type(self) -> str:
"""
Frequency sweep type.
Returns
-------
sweep_type: str
'lin' if linearly increasing, 'log' or 'unknown'.
"""
if np.allclose(self.f, linspace(self.f[0], self.f[-1], self.npoints), rtol=0.05):
sweep_type = 'lin'
elif self.f[0] and np.allclose(self.f, geomspace(self.f[0], self.f[-1], self.npoints), rtol=0.05):
sweep_type = 'log'
else:
sweep_type = 'unknown'
return sweep_type
@axes_kwarg
def labelXAxis(self, ax: Axes = None):
"""
Label the x-axis of a plot.
Sets the labels of a plot using :func:`matplotlib.x_label` with
string containing the frequency unit.
Parameters
----------
ax : :class:`matplotlib.Axes` or None, optional
Axes on which to label the plot.
Defaults is None, for the current axe
returned by :func:`matplotlib.gca()`
"""
ax.set_xlabel(f'Frequency ({self.unit})')
@axes_kwarg
def plot(self, y: NumberLike, *args, ax: Axes=None, **kwargs):
"""
Plot something vs this frequency.
This plots whatever is given vs. `self.f_scaled` and then
calls `labelXAxis`.
"""
from .plotting import scale_frequency_ticks
try:
if len(np.shape(y)) > 2:
# perhaps the dimensions are empty, try to squeeze it down
y = y.squeeze()
if len(np.shape(y)) > 2:
# the dimensions are full, so lets loop and plot each
for m in range(np.shape(y)[1]):
for n in range(np.shape(y)[2]):
self.plot(y[:, m, n], *args, **kwargs)
return
if len(y) == len(self):
pass
else:
raise IndexError(['thing to plot doesn\'t have same'
' number of points as f'])
except(TypeError):
y = y * np.ones(len(self))
# plt.plot(self.f_scaled, y, *args, **kwargs)
ax.plot(self.f, y, *args, **kwargs)
scale_frequency_ticks(ax, self.unit)
ax.autoscale(axis='x', tight=True)
self.labelXAxis()
def overlap_freq(f1: Frequency,f2: Frequency) -> Frequency:
"""
Calculates overlapping frequency between f1 and f2.
Or, put more accurately, this returns a Frequency that is the part
of f1 that is overlapped by f2. The resultant start frequency is
the smallest f1.f that is greater than f2.f.start, and the stop
frequency is the largest f1.f that is smaller than f2.f.stop.
This way the new frequency overlays onto f1.
Parameters
----------
f1 : :class:`Frequency`
a frequency object
f2 : :class:`Frequency`
a frequency object
Returns
-------
f3 : :class:`Frequency`
part of f1 that is overlapped by f2
"""
if f1.start > f2.stop:
raise ValueError('Out of bounds. f1.start > f2.stop')
elif f2.start > f1.stop:
raise ValueError('Out of bounds. f2.start > f1.stop')
start = max(f1.start, f2.start)
stop = min(f1.stop, f2.stop)
f = f1.f[(f1.f>=start) & (f1.f<=stop)]
freq = Frequency.from_f(f, unit = 'Hz')
freq.unit = f1.unit
return freq
|