polars-waveform 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- polars_waveform/__init__.py +49 -0
- polars_waveform/base.py +176 -0
- polars_waveform/cx.py +145 -0
- polars_waveform/functions.py +325 -0
- polars_waveform/nested.py +82 -0
- polars_waveform/pandas_waveform.py +354 -0
- polars_waveform/plot.py +156 -0
- polars_waveform/py.typed +0 -0
- polars_waveform/source.py +41 -0
- polars_waveform/waveform.py +1253 -0
- polars_waveform-0.1.0.dist-info/METADATA +200 -0
- polars_waveform-0.1.0.dist-info/RECORD +14 -0
- polars_waveform-0.1.0.dist-info/WHEEL +4 -0
- polars_waveform-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Waveforms on Polars: curves and curve families with circuit-style measurements.
|
|
2
|
+
|
|
3
|
+
A :class:`Waveform` is a value column over one or more index columns, backed by a lazy
|
|
4
|
+
``pl.LazyFrame`` and materialized only when a value is needed. The last index column is the
|
|
5
|
+
sweep (time, frequency, offset, ...); any others are groups (corners, Monte Carlo runs, measured
|
|
6
|
+
operating points), so one Waveform can hold a whole family of curves::
|
|
7
|
+
|
|
8
|
+
import polars as pl
|
|
9
|
+
import polars_waveform as pw
|
|
10
|
+
|
|
11
|
+
w = pw.Waveform(pl.scan_parquet("lab.parquet"), "gain", index=["dut", "temp", "freq"])
|
|
12
|
+
w.bandwidth() # one row per dut x temp
|
|
13
|
+
w - w.mean() # per-curve results broadcast back over the curves
|
|
14
|
+
w.leaf(dut="A1", temp=25.0) # one curve
|
|
15
|
+
|
|
16
|
+
Measurements follow OCEAN semantics (``xmax`` is the x at the largest y; ``cross`` counts edges
|
|
17
|
+
from 1); the OCEAN and pycircuit spellings (``dB20``, ``unityGainFreq``, ``IIP3``, ...) are aliases.
|
|
18
|
+
Complex data is ``Struct{re, im}``; :mod:`.cx` registers ``pl.col(...).cx`` for it.
|
|
19
|
+
:func:`from_nested` builds families from nested channel columns, :meth:`Waveform.from_arrays`
|
|
20
|
+
from numpy grids, and :class:`ResultSource` is the shape shared by result objects
|
|
21
|
+
(``polars_psf.Result`` implements it). :class:`WaveformBase` is the contract other waveform
|
|
22
|
+
kinds implement; :class:`PandasWaveform` is one for values Polars cannot compute with (sympy
|
|
23
|
+
expressions, ...), and :func:`from_arrays` picks the kind the values need.
|
|
24
|
+
|
|
25
|
+
Modules: :mod:`.waveform` (the class), :mod:`.functions` (calculator functions), :mod:`.cx`
|
|
26
|
+
(complex helpers), :mod:`.nested`, :mod:`.source`, :mod:`.pandas_waveform`.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from importlib.metadata import PackageNotFoundError
|
|
30
|
+
from importlib.metadata import version as _version
|
|
31
|
+
|
|
32
|
+
from . import cx
|
|
33
|
+
from .base import WaveformBase
|
|
34
|
+
from .functions import * # noqa: F403
|
|
35
|
+
from .functions import __all__ as _functions
|
|
36
|
+
from .nested import from_nested
|
|
37
|
+
from .pandas_waveform import PandasWaveform, from_arrays
|
|
38
|
+
from .source import ResultSource
|
|
39
|
+
from .waveform import Waveform, either, falling, raising
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"PandasWaveform", "ResultSource", "Waveform", "WaveformBase", "cx", "either", "falling", "from_arrays",
|
|
43
|
+
"from_nested", "raising", *_functions,
|
|
44
|
+
] # fmt: skip
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
__version__ = _version("polars-waveform")
|
|
48
|
+
except PackageNotFoundError: # running from a source tree without an install
|
|
49
|
+
__version__ = "0+unknown"
|
polars_waveform/base.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""The contract shared by all waveform kinds.
|
|
2
|
+
|
|
3
|
+
:class:`~polars_waveform.Waveform` stores numeric data in Polars (lazy, families as group
|
|
4
|
+
columns). Other kinds implement the same interface with their own storage, for example a
|
|
5
|
+
symbolic waveform holding sympy expressions in numpy object arrays: Polars can store Python
|
|
6
|
+
objects but has no kernels to compute with them, so such data needs its own container.
|
|
7
|
+
|
|
8
|
+
A subclass implements storage, construction and elementwise math (the abstract methods), and
|
|
9
|
+
:meth:`WaveformBase.numeric` to turn itself into a numeric :class:`~polars_waveform.Waveform`.
|
|
10
|
+
Measurements it does not implement itself (``bandwidth``, ``cross``, ``rise_time``, ...) are
|
|
11
|
+
inherited from this class and run on ``numeric()``, so they work on any kind once its values are
|
|
12
|
+
numbers.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from abc import ABC, abstractmethod
|
|
18
|
+
|
|
19
|
+
__all__ = ["WaveformBase"]
|
|
20
|
+
|
|
21
|
+
# measurements that need numbers: inherited by other kinds, evaluated on numeric()
|
|
22
|
+
_NUMERIC = (
|
|
23
|
+
"value", "ymax", "ymin", "xmax", "xmin", "argmax", "argmin", "average", "mean", "rms", "stddev",
|
|
24
|
+
"cross", "frequency", "period", "rise_time", "fall_time", "slew_rate", "overshoot", "settling_time",
|
|
25
|
+
"bandwidth", "unity_gain_frequency", "phase_margin", "gain_margin", "integ", "iinteg", "deriv",
|
|
26
|
+
"clip", "dft", "to_polars", "plot", "semilogx", "semilogy", "loglog", "stem", "bode",
|
|
27
|
+
) # fmt: skip
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class WaveformBase(ABC):
|
|
31
|
+
"""A value over one or more index columns: groups (one curve per combination) and a sweep.
|
|
32
|
+
|
|
33
|
+
The interface every waveform kind provides: names and units, conversion from and to numpy
|
|
34
|
+
arrays (:meth:`from_arrays`, :meth:`to_arrays`), arithmetic, the elementwise functions
|
|
35
|
+
(``db20``, ``phase``, ...), and the measurements, which by default evaluate on
|
|
36
|
+
:meth:`numeric`.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
# --- storage and names (abstract) -------------------------------------------------------
|
|
40
|
+
@classmethod
|
|
41
|
+
@abstractmethod
|
|
42
|
+
def from_arrays(cls, x, y, xlabels=None, ylabel=None, xunits=None, yunit=None) -> WaveformBase:
|
|
43
|
+
"""A waveform from numpy arrays: one x array per sweep axis (the last one is the sweep,
|
|
44
|
+
the others become groups) and ``y`` with shape ``(len(x0), len(x1), ...)``, or ragged
|
|
45
|
+
object arrays (see :meth:`polars_waveform.Waveform.from_arrays`)."""
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
def to_arrays(self):
|
|
49
|
+
"""``(xs, y)`` as numpy arrays in the layout :meth:`from_arrays` accepts."""
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def numeric(self):
|
|
53
|
+
"""This waveform as a numeric :class:`~polars_waveform.Waveform`."""
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
@abstractmethod
|
|
57
|
+
def index(self) -> list[str]:
|
|
58
|
+
"""Index column names: the groups followed by the sweep."""
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def groups(self) -> list[str]:
|
|
62
|
+
"""Index columns other than the sweep (one curve per combination)."""
|
|
63
|
+
return [c for c in self.index if c != self.xname]
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
@abstractmethod
|
|
67
|
+
def xname(self) -> str: ...
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
@abstractmethod
|
|
71
|
+
def yname(self) -> str: ...
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
@abstractmethod
|
|
75
|
+
def xunit(self): ...
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
@abstractmethod
|
|
79
|
+
def yunit(self): ...
|
|
80
|
+
|
|
81
|
+
# --- arithmetic (abstract core, shared operators) ------------------------------------------
|
|
82
|
+
@abstractmethod
|
|
83
|
+
def _binop(self, other, op: str, *, reverse: bool = False) -> WaveformBase:
|
|
84
|
+
"""``self <op> other`` (``other <op> self`` if ``reverse``) for op in + - * / < > <= >=."""
|
|
85
|
+
|
|
86
|
+
def __add__(self, other):
|
|
87
|
+
return self._binop(other, "+")
|
|
88
|
+
|
|
89
|
+
def __radd__(self, other):
|
|
90
|
+
return self._binop(other, "+", reverse=True)
|
|
91
|
+
|
|
92
|
+
def __sub__(self, other):
|
|
93
|
+
return self._binop(other, "-")
|
|
94
|
+
|
|
95
|
+
def __rsub__(self, other):
|
|
96
|
+
return self._binop(other, "-", reverse=True)
|
|
97
|
+
|
|
98
|
+
def __mul__(self, other):
|
|
99
|
+
return self._binop(other, "*")
|
|
100
|
+
|
|
101
|
+
def __rmul__(self, other):
|
|
102
|
+
return self._binop(other, "*", reverse=True)
|
|
103
|
+
|
|
104
|
+
def __truediv__(self, other):
|
|
105
|
+
return self._binop(other, "/")
|
|
106
|
+
|
|
107
|
+
def __rtruediv__(self, other):
|
|
108
|
+
return self._binop(other, "/", reverse=True)
|
|
109
|
+
|
|
110
|
+
def __lt__(self, other):
|
|
111
|
+
return self._binop(other, "<")
|
|
112
|
+
|
|
113
|
+
def __le__(self, other):
|
|
114
|
+
return self._binop(other, "<=")
|
|
115
|
+
|
|
116
|
+
def __gt__(self, other):
|
|
117
|
+
return self._binop(other, ">")
|
|
118
|
+
|
|
119
|
+
def __ge__(self, other):
|
|
120
|
+
return self._binop(other, ">=")
|
|
121
|
+
|
|
122
|
+
# --- elementwise (abstract) ----------------------------------------------------------------
|
|
123
|
+
@abstractmethod
|
|
124
|
+
def __neg__(self): ...
|
|
125
|
+
|
|
126
|
+
@abstractmethod
|
|
127
|
+
def __abs__(self): ...
|
|
128
|
+
|
|
129
|
+
@abstractmethod
|
|
130
|
+
def __pow__(self, other): ...
|
|
131
|
+
|
|
132
|
+
@abstractmethod
|
|
133
|
+
def real(self): ...
|
|
134
|
+
|
|
135
|
+
@abstractmethod
|
|
136
|
+
def imag(self): ...
|
|
137
|
+
|
|
138
|
+
@abstractmethod
|
|
139
|
+
def conj(self): ...
|
|
140
|
+
|
|
141
|
+
@abstractmethod
|
|
142
|
+
def phase(self, deg: bool = True): ...
|
|
143
|
+
|
|
144
|
+
@abstractmethod
|
|
145
|
+
def db10(self): ...
|
|
146
|
+
|
|
147
|
+
@abstractmethod
|
|
148
|
+
def db20(self): ...
|
|
149
|
+
|
|
150
|
+
def __pos__(self):
|
|
151
|
+
return self
|
|
152
|
+
|
|
153
|
+
def abs(self):
|
|
154
|
+
"""Magnitude (``abs(w)``)."""
|
|
155
|
+
return abs(self)
|
|
156
|
+
|
|
157
|
+
mag = abs
|
|
158
|
+
|
|
159
|
+
def conjugate(self):
|
|
160
|
+
return self.conj()
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _numeric_method(name: str):
|
|
164
|
+
def method(self, *args, **kwargs):
|
|
165
|
+
num = self.numeric()
|
|
166
|
+
if type(num) is type(self): # a numeric kind must implement the measurement itself
|
|
167
|
+
raise NotImplementedError(f"{type(self).__name__}.{name}()")
|
|
168
|
+
return getattr(num, name)(*args, **kwargs)
|
|
169
|
+
|
|
170
|
+
method.__name__ = name
|
|
171
|
+
method.__doc__ = f"``{name}`` evaluated on :meth:`numeric` (see ``polars_waveform.Waveform.{name}``)."
|
|
172
|
+
return method
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
for _name in _NUMERIC:
|
|
176
|
+
setattr(WaveformBase, _name, _numeric_method(_name))
|
polars_waveform/cx.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Complex-number helpers for Struct{re, im} columns.
|
|
2
|
+
|
|
3
|
+
Registers a ``cx`` expression namespace on import::
|
|
4
|
+
|
|
5
|
+
pl.col("out").cx.abs() # magnitude
|
|
6
|
+
pl.col("out").cx.db20() # 20*log10(|z|)
|
|
7
|
+
pl.col("out").cx.phase() # degrees (deg=False for radians)
|
|
8
|
+
|
|
9
|
+
The same functions are available as ``pw.cx.abs("out")`` etc. (accept a column name or expression).
|
|
10
|
+
Arithmetic on two complex columns: ``cx.mul("a", "b")``, ``cx.div``, ``cx.add``, ``cx.sub`` and
|
|
11
|
+
``cx.pow(x, n)`` for a real exponent; ``cx.from_real(x)`` lifts a real column to ``Struct{re, im}``.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import math
|
|
15
|
+
|
|
16
|
+
import polars as pl
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"abs",
|
|
20
|
+
"add",
|
|
21
|
+
"complex",
|
|
22
|
+
"conj",
|
|
23
|
+
"db10",
|
|
24
|
+
"db20",
|
|
25
|
+
"div",
|
|
26
|
+
"from_real",
|
|
27
|
+
"im",
|
|
28
|
+
"mul",
|
|
29
|
+
"phase",
|
|
30
|
+
"pow",
|
|
31
|
+
"re",
|
|
32
|
+
"sub",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _e(x) -> pl.Expr:
|
|
37
|
+
return pl.col(x) if isinstance(x, str) else x
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def re(x) -> pl.Expr:
|
|
41
|
+
return _e(x).struct.field("re")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def im(x) -> pl.Expr:
|
|
45
|
+
return _e(x).struct.field("im")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def abs(x) -> pl.Expr:
|
|
49
|
+
return (re(x) ** 2 + im(x) ** 2).sqrt()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def db20(x) -> pl.Expr:
|
|
53
|
+
return 20 * abs(x).log10()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def db10(x) -> pl.Expr:
|
|
57
|
+
"""10*log10(|z|), for power quantities."""
|
|
58
|
+
return 10 * abs(x).log10()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def phase(x, deg: bool = True) -> pl.Expr:
|
|
62
|
+
p = pl.arctan2(im(x), re(x))
|
|
63
|
+
return p * (180 / math.pi) if deg else p
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def conj(x) -> pl.Expr:
|
|
67
|
+
return pl.struct(re(x).alias("re"), (-im(x)).alias("im"))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def complex(re_expr, im_expr) -> pl.Expr:
|
|
71
|
+
"""Build a Struct{re, im} column."""
|
|
72
|
+
return pl.struct(_e(re_expr).alias("re"), _e(im_expr).alias("im"))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def from_real(x) -> pl.Expr:
|
|
76
|
+
"""A real column as ``Struct{re, im}`` with ``im = 0``."""
|
|
77
|
+
return complex(x, _e(x) * 0.0)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def add(a, b) -> pl.Expr:
|
|
81
|
+
return complex(re(a) + re(b), im(a) + im(b))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def sub(a, b) -> pl.Expr:
|
|
85
|
+
return complex(re(a) - re(b), im(a) - im(b))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def mul(a, b) -> pl.Expr:
|
|
89
|
+
ar, ai, br, bi = re(a), im(a), re(b), im(b)
|
|
90
|
+
return complex(ar * br - ai * bi, ar * bi + ai * br)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def div(a, b) -> pl.Expr:
|
|
94
|
+
ar, ai, br, bi = re(a), im(a), re(b), im(b)
|
|
95
|
+
den = br * br + bi * bi
|
|
96
|
+
return complex((ar * br + ai * bi) / den, (ai * br - ar * bi) / den)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def pow(x, n: float) -> pl.Expr:
|
|
100
|
+
"""``x ** n`` for a real exponent (principal branch)."""
|
|
101
|
+
mag = abs(x) ** n
|
|
102
|
+
th = pl.arctan2(im(x), re(x)) * n
|
|
103
|
+
return complex(mag * th.cos(), mag * th.sin())
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@pl.api.register_expr_namespace("cx")
|
|
107
|
+
class _CxNamespace:
|
|
108
|
+
def __init__(self, expr: pl.Expr):
|
|
109
|
+
self._e = expr
|
|
110
|
+
|
|
111
|
+
def re(self) -> pl.Expr:
|
|
112
|
+
return re(self._e)
|
|
113
|
+
|
|
114
|
+
def im(self) -> pl.Expr:
|
|
115
|
+
return im(self._e)
|
|
116
|
+
|
|
117
|
+
def abs(self) -> pl.Expr:
|
|
118
|
+
return abs(self._e)
|
|
119
|
+
|
|
120
|
+
def db20(self) -> pl.Expr:
|
|
121
|
+
return db20(self._e)
|
|
122
|
+
|
|
123
|
+
def db10(self) -> pl.Expr:
|
|
124
|
+
return db10(self._e)
|
|
125
|
+
|
|
126
|
+
def phase(self, deg: bool = True) -> pl.Expr:
|
|
127
|
+
return phase(self._e, deg)
|
|
128
|
+
|
|
129
|
+
def conj(self) -> pl.Expr:
|
|
130
|
+
return conj(self._e)
|
|
131
|
+
|
|
132
|
+
def mul(self, other) -> pl.Expr:
|
|
133
|
+
return mul(self._e, other)
|
|
134
|
+
|
|
135
|
+
def div(self, other) -> pl.Expr:
|
|
136
|
+
return div(self._e, other)
|
|
137
|
+
|
|
138
|
+
def add(self, other) -> pl.Expr:
|
|
139
|
+
return add(self._e, other)
|
|
140
|
+
|
|
141
|
+
def sub(self, other) -> pl.Expr:
|
|
142
|
+
return sub(self._e, other)
|
|
143
|
+
|
|
144
|
+
def pow(self, n: float) -> pl.Expr:
|
|
145
|
+
return pow(self._e, n)
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"""Calculator functions: ``db20(w)``, ``bandwidth(w, 3, "low")``, ``rise_time(w)``.
|
|
2
|
+
|
|
3
|
+
Each forwards to the :class:`Waveform` method of the same name, so it works per curve of a
|
|
4
|
+
family (corners, Monte Carlo) and returns one row per curve there. The semantics follow OCEAN:
|
|
5
|
+
``xmax`` is the x at the largest y, and ``cross`` counts edges from 1 with
|
|
6
|
+
``"rising"``/``"falling"``/``"either"``. The OCEAN names (``dB20``, ``unityGainFreq``,
|
|
7
|
+
``riseTime``, ``leafValue``, ...) and pycircuit names (``unityGainFrequency``, ``IIP3``, ...) are
|
|
8
|
+
aliases.
|
|
9
|
+
|
|
10
|
+
Arguments follow OCEAN's order where it has positional ones; OCEAN's ``initType``/``finalType``
|
|
11
|
+
flags (initial value given as an x instead of a y) are not supported: pass y values.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import math
|
|
17
|
+
|
|
18
|
+
from .base import WaveformBase
|
|
19
|
+
from .plot import compression_plot, set_plot_backend
|
|
20
|
+
from .waveform import Waveform
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
# calculator functions
|
|
24
|
+
"db10", "db20", "mag", "phase", "phase_deg", "phase_rad", "real", "imag", "conjugate",
|
|
25
|
+
"value", "ymax", "ymin", "xmax", "xmin", "average", "rms", "stddev",
|
|
26
|
+
"cross", "deriv", "integ", "iinteg", "clip", "dft", "leaf_value", "log10", "exp", "sqrt",
|
|
27
|
+
"bandwidth", "unity_gain_frequency", "phase_margin", "gain_margin",
|
|
28
|
+
"rise_time", "fall_time", "slew_rate", "overshoot", "settling_time", "delay", "frequency", "period",
|
|
29
|
+
"im2", "im3", "iip2", "iip3", "calc_extrapolation_line", "compression_point", "compression_plot",
|
|
30
|
+
"set_plot_backend",
|
|
31
|
+
# OCEAN aliases
|
|
32
|
+
"dB10", "dB20", "phaseDeg", "phaseRad", "leafValue", "unityGainFreq", "phaseMargin", "gainMargin",
|
|
33
|
+
"riseTime", "fallTime", "slewRate", "settlingTime",
|
|
34
|
+
# pycircuit aliases
|
|
35
|
+
"unityGainFrequency", "IM2", "IM3", "IIP2", "IIP3",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# --- elementwise: also on plain numbers and numpy arrays --------------------------------------
|
|
40
|
+
def _num(w):
|
|
41
|
+
"""True if ``w`` is a plain number or array (not a waveform)."""
|
|
42
|
+
return not isinstance(w, WaveformBase)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def db20(w):
|
|
46
|
+
"""``20*log10(|w|)``; works on waveforms, numbers and numpy arrays."""
|
|
47
|
+
if _num(w):
|
|
48
|
+
import numpy as np
|
|
49
|
+
|
|
50
|
+
return 20.0 * np.log10(np.abs(w))
|
|
51
|
+
return w.db20()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def db10(w):
|
|
55
|
+
"""``10*log10(|w|)``, for power quantities."""
|
|
56
|
+
if _num(w):
|
|
57
|
+
import numpy as np
|
|
58
|
+
|
|
59
|
+
return 10.0 * np.log10(np.abs(w))
|
|
60
|
+
return w.db10()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def mag(w):
|
|
64
|
+
if _num(w):
|
|
65
|
+
import numpy as np
|
|
66
|
+
|
|
67
|
+
return np.abs(w)
|
|
68
|
+
return w.mag()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def phase(w, deg: bool = True):
|
|
72
|
+
"""Phase in degrees (radians with ``deg=False``)."""
|
|
73
|
+
if _num(w):
|
|
74
|
+
import numpy as np
|
|
75
|
+
|
|
76
|
+
return np.angle(w, deg=deg)
|
|
77
|
+
return w.phase(deg)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def phase_deg(w):
|
|
81
|
+
return phase(w, deg=True)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def phase_rad(w):
|
|
85
|
+
return phase(w, deg=False)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def real(w):
|
|
89
|
+
if _num(w):
|
|
90
|
+
import numpy as np
|
|
91
|
+
|
|
92
|
+
return np.real(w)
|
|
93
|
+
return w.real()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def imag(w):
|
|
97
|
+
if _num(w):
|
|
98
|
+
import numpy as np
|
|
99
|
+
|
|
100
|
+
return np.imag(w)
|
|
101
|
+
return w.imag()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def conjugate(w):
|
|
105
|
+
if _num(w):
|
|
106
|
+
import numpy as np
|
|
107
|
+
|
|
108
|
+
return np.conjugate(w)
|
|
109
|
+
return w.conj()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def log10(w):
|
|
113
|
+
if _num(w):
|
|
114
|
+
import numpy as np
|
|
115
|
+
|
|
116
|
+
return np.log10(w)
|
|
117
|
+
return w.log10()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def exp(w):
|
|
121
|
+
if _num(w):
|
|
122
|
+
import numpy as np
|
|
123
|
+
|
|
124
|
+
return np.exp(w)
|
|
125
|
+
return w.exp()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def sqrt(w):
|
|
129
|
+
if _num(w):
|
|
130
|
+
import numpy as np
|
|
131
|
+
|
|
132
|
+
return np.sqrt(w)
|
|
133
|
+
return w.sqrt()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def deriv(w: Waveform) -> Waveform:
|
|
137
|
+
return w.deriv()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def iinteg(w: Waveform) -> Waveform:
|
|
141
|
+
return w.iinteg()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def clip(w: Waveform, xfrom: float, xto: float | None = None) -> Waveform:
|
|
145
|
+
return w.clip(xfrom, xto)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def dft(w: Waveform) -> Waveform:
|
|
149
|
+
return w.dft()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def leaf_value(w: Waveform, **values) -> Waveform:
|
|
153
|
+
"""One leaf of a family: ``leaf_value(w, temp=27, rval=1000)``."""
|
|
154
|
+
return w.leaf(**values)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# --- measurements (scalar per curve) ----------------------------------------------------------
|
|
158
|
+
def value(w: Waveform, x):
|
|
159
|
+
return w.value(x)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def ymax(w: Waveform, axis=-1):
|
|
163
|
+
return w.ymax(axis)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def ymin(w: Waveform, axis=-1):
|
|
167
|
+
return w.ymin(axis)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def xmax(w: Waveform):
|
|
171
|
+
"""x where y is largest."""
|
|
172
|
+
return w.xmax()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def xmin(w: Waveform):
|
|
176
|
+
"""x where y is smallest."""
|
|
177
|
+
return w.xmin()
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def average(w: Waveform, axis=-1):
|
|
181
|
+
return w.average(axis)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def rms(w: Waveform, axis=-1):
|
|
185
|
+
return w.rms(axis)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def stddev(w: Waveform, axis=-1):
|
|
189
|
+
return w.stddev(axis)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def integ(w: Waveform, xfrom: float | None = None, xto: float | None = None):
|
|
193
|
+
return w.integ(xfrom, xto)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def cross(w: Waveform, threshold: float = 0.0, edge: int = 1, type: str = "either"):
|
|
197
|
+
"""x of the ``edge``-th crossing of ``threshold`` (from 1; negative counts from the end)."""
|
|
198
|
+
return w.cross(threshold, edge, type)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def bandwidth(w: Waveform, db: float = 3.0, type: str = "low"):
|
|
202
|
+
return w.bandwidth(db, type)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def unity_gain_frequency(w: Waveform):
|
|
206
|
+
return w.unity_gain_frequency()
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def phase_margin(w: Waveform):
|
|
210
|
+
return w.phase_margin()
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def gain_margin(w: Waveform):
|
|
214
|
+
return w.gain_margin()
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def rise_time(w: Waveform, initial=None, final=None, theta1: float = 10.0, theta2: float = 90.0):
|
|
218
|
+
"""Rise time (OCEAN ``riseTime(wave initVal nil finalVal nil theta1 theta2)``)."""
|
|
219
|
+
return w.rise_time(theta1, theta2, initial, final)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def fall_time(w: Waveform, initial=None, final=None, theta1: float = 10.0, theta2: float = 90.0):
|
|
223
|
+
return w.fall_time(theta1, theta2, initial, final)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def slew_rate(w: Waveform, initial=None, final=None, theta1: float = 10.0, theta2: float = 90.0):
|
|
227
|
+
return w.slew_rate(theta1, theta2, initial, final)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def overshoot(w: Waveform, initial=None, final=None):
|
|
231
|
+
return w.overshoot(initial, final)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def settling_time(w: Waveform, initial=None, final=None, tolerance: float = 1.0):
|
|
235
|
+
return w.settling_time(tolerance, initial, final)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def delay(
|
|
239
|
+
wf1: Waveform,
|
|
240
|
+
wf2: Waveform,
|
|
241
|
+
value1: float = 0.0,
|
|
242
|
+
value2: float | None = None,
|
|
243
|
+
edge1: str = "either",
|
|
244
|
+
nth1: int = 1,
|
|
245
|
+
edge2: str | None = None,
|
|
246
|
+
nth2: int = 1,
|
|
247
|
+
):
|
|
248
|
+
"""x of ``wf2``'s ``nth2`` crossing of ``value2`` minus that of ``wf1`` (OCEAN ``delay``
|
|
249
|
+
keyword names)."""
|
|
250
|
+
return wf1.delay(wf2, value1, value2, nth1, nth2, edge1, edge2)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def frequency(w: Waveform, threshold: float | None = None):
|
|
254
|
+
return w.frequency(threshold)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def period(w: Waveform, threshold: float | None = None):
|
|
258
|
+
return w.period(threshold)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# --- RF (pycircuit) ---------------------------------------------------------------------------
|
|
262
|
+
def im3(w: Waveform, fund1: float, fund2: float, fund0=None) -> float:
|
|
263
|
+
"""Third-order intermodulation tone at ``fund1 + 2*fund2`` of an output spectrum.
|
|
264
|
+
|
|
265
|
+
``fund0`` (the LO of a mixer) is accepted for pycircuit compatibility; output tones are
|
|
266
|
+
measured at IF, so it does not shift them (only :func:`iip3` uses it, for the input)."""
|
|
267
|
+
return abs(w).value(fund1 + 2 * fund2)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def im2(w: Waveform, fund1: float, fund2: float, fund0=None) -> float:
|
|
271
|
+
"""Second-order intermodulation tone at ``fund1 + fund2`` of an output spectrum (``fund0``:
|
|
272
|
+
see :func:`im3`)."""
|
|
273
|
+
return abs(w).value(fund1 + fund2)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def iip3(output: Waveform, input: Waveform, fund1: float, fund2: float, fund0=None) -> float:
|
|
277
|
+
"""Input-referred third-order intercept point."""
|
|
278
|
+
s = abs(output)
|
|
279
|
+
if fund0 is None:
|
|
280
|
+
gain = (s / abs(input)).value(fund1)
|
|
281
|
+
else:
|
|
282
|
+
gain = s.value(abs(fund1)) / abs(input).value(abs(abs(fund1) + fund0))
|
|
283
|
+
return math.sqrt(s.value(abs(fund1)) * s.value(abs(fund2)) ** 2 / s.value(fund1 + 2 * fund2)) / gain
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def iip2(output: Waveform, input: Waveform, fund1: float, fund2: float, fund0=None) -> float:
|
|
287
|
+
"""Input-referred second-order intercept point."""
|
|
288
|
+
s = abs(output)
|
|
289
|
+
if fund0 is None:
|
|
290
|
+
gain = (s / abs(input)).value(fund1)
|
|
291
|
+
else:
|
|
292
|
+
gain = s.value(abs(fund1)) / abs(input).value(abs(abs(fund1) + fund0))
|
|
293
|
+
return s.value(abs(fund1)) * s.value(abs(fund2)) / s.value(fund1 + fund2) / gain
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def calc_extrapolation_line(
|
|
297
|
+
w_db: Waveform, slope: float = 1.0, extrapolation_point: float | None = None, **_
|
|
298
|
+
) -> Waveform:
|
|
299
|
+
"""Linear (in dB) extrapolation of ``w_db`` through its low end (smallest x)."""
|
|
300
|
+
if extrapolation_point is None:
|
|
301
|
+
extrapolation_point = w_db.x.min()
|
|
302
|
+
m = w_db.value(extrapolation_point) - slope * extrapolation_point
|
|
303
|
+
return m + slope * w_db.xval()
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def compression_point(
|
|
307
|
+
w_db: Waveform, slope: float = 1.0, compression: float = 1.0, extrapolation_point=None, **_
|
|
308
|
+
) -> float:
|
|
309
|
+
"""Input-referred compression point: where the response falls ``compression`` dB below the
|
|
310
|
+
extrapolated small-signal line."""
|
|
311
|
+
line = calc_extrapolation_line(w_db, slope, extrapolation_point)
|
|
312
|
+
return (line - w_db).cross(compression)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# --- aliases ----------------------------------------------------------------------------------
|
|
316
|
+
# OCEAN
|
|
317
|
+
dB20, dB10 = db20, db10
|
|
318
|
+
phaseDeg, phaseRad = phase_deg, phase_rad
|
|
319
|
+
leafValue = leaf_value
|
|
320
|
+
unityGainFreq = unity_gain_frequency
|
|
321
|
+
phaseMargin, gainMargin = phase_margin, gain_margin
|
|
322
|
+
riseTime, fallTime, slewRate, settlingTime = rise_time, fall_time, slew_rate, settling_time
|
|
323
|
+
# pycircuit
|
|
324
|
+
unityGainFrequency = unity_gain_frequency
|
|
325
|
+
IM2, IM3, IIP2, IIP3 = im2, im3, iip2, iip3
|