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.
@@ -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"
@@ -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