Metadata-Version: 2.5
Name: polars-waveform
Version: 0.1.0
Summary: Waveforms and curve families on Polars, with circuit-style measurements (bandwidth, cross, rise time, ...)
Project-URL: Repository, https://github.com/henjo/polars-waveform
Project-URL: Issues, https://github.com/henjo/polars-waveform/issues
License-Expression: MIT
License-File: LICENSE
Keywords: eda,measurement,ocean,polars,simulation,waveform
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: polars>=1.40
Provides-Extra: altair
Requires-Dist: altair>=5.4; extra == 'altair'
Provides-Extra: matplotlib
Requires-Dist: matplotlib; extra == 'matplotlib'
Requires-Dist: numpy; extra == 'matplotlib'
Provides-Extra: numpy
Requires-Dist: numpy; extra == 'numpy'
Provides-Extra: pandas
Requires-Dist: numpy; extra == 'pandas'
Requires-Dist: pandas>=2.2; extra == 'pandas'
Provides-Extra: test
Requires-Dist: matplotlib; extra == 'test'
Requires-Dist: numpy; extra == 'test'
Requires-Dist: pandas>=2.2; extra == 'test'
Requires-Dist: pytest; extra == 'test'
Requires-Dist: sympy; extra == 'test'
Description-Content-Type: text/markdown

# polars-waveform

Waveforms and curve families on [Polars](https://pola.rs), with circuit-style measurements:
bandwidth, crossings, rise time, phase margin, ... Works on any Polars data: simulation results
(through [polars-psf](https://github.com/henjo/polars-psf) for Cadence® Spectre® PSF files), lab measurements in Parquet,
or a DataFrame you built yourself. Pure Python; depends only on `polars`.

polars-waveform is an independent community project, not affiliated with or endorsed by the Polars
project or by Cadence Design Systems, Inc.

```sh
pip install polars-waveform          # or: uv add polars-waveform
```

## Quick start

```python
import polars as pl
import polars_waveform as pw

df = pl.DataFrame({"t": [0.0, 1.0, 2.0, 3.0], "v": [0.0, 0.4, 0.9, 1.0]})
w = pw.Waveform(df, "v")             # the last index column (t) is the sweep

w.ymax(), w.cross(0.5), w.rise_time(), w.value(1.5)
w.deriv(), w.clip(0.5, 2.5), w * 2 - 1
```

## Families: one curve per operating point

Index columns besides the sweep are groups. A Waveform then holds a family of curves (devices,
temperatures, Monte Carlo runs), and every measurement returns a table with one row per curve:

```python
lf = pl.scan_parquet("lab.parquet")                          # lazy: read on first use
gain = pw.Waveform(lf.select("dut", "temp", "freq", "gain"), "gain", index=["dut", "temp", "freq"])

gain.bandwidth()                     # dut | temp | bandwidth
gain.leaf(dut="A1", temp=25.0)       # one curve (a single curve gives plain numbers)
gain - gain.mean()                   # per-curve results broadcast back over the curves
```

Those tables are ordinary Polars DataFrames: sort, filter, join with other measurements, and
`write_csv` / `write_excel`.

Families can also be reshaped, all lazily:

```python
gain.reduce("min", over="temp")      # worst case over temperature: a family over dut x freq
gain.ymax(axis="temp")               # the same with an axis argument (axis=-1, the sweep, is per curve)
gain.reorder(["dut", "freq", "temp"])  # curves over temperature, one per dut and frequency
for dut, g in gain.along("dut"):     # iterate over one group column
    print(dut, g.bandwidth())
gain[-1]                             # last sample of every curve (a number for a single curve)
gain[10:20]                          # a slice of every curve
```

## From numpy arrays

`Waveform.from_arrays` takes one array per sweep axis and an n-dimensional `y` (the last axis is
the sweep, the others become groups), or ragged object arrays; `to_arrays()` converts back:

```python
w = pw.Waveform.from_arrays([temps, freqs], gain_2d, xlabels=["temp", "freq"], ylabel="gain")
xs, y = w.to_arrays()
```

numpy functions keep waveforms as waveforms: `np.abs(w)`, `np.log10(w)`, `array + w`.

## Nested channels

Measurement tables often keep one row per operating point and store each *channel*, a curve
with its own sweep, in a nested column:

```
lbw | vdd | idd | ... | pn: {offset: [..], psd: [..]} | spurs: {freq: [..], level: [..]}
```

`from_nested` turns a channel into a family, one curve per row, without flattening the table.
Only the group columns and that channel are read:

```python
pn = pw.from_nested(pl.scan_parquet("pll.parquet"), "pn", x="offset", y="psd",
                    groups=["lbw", "vdd"], units={"offset": "Hz", "psd": "dBc/Hz"})

pn.value(1e6)                                    # phase noise at 1 MHz offset, per lbw x vdd
(10 ** (pn / 10)).integ(1e4, 1e7)                # integrated phase noise, per lbw x vdd
```

Both nested shapes work: a struct of lists (`{x: [..], y: [..]}`) and a list of structs.

## Plotting

```sh
pip install "polars-waveform[matplotlib]"    # or [altair] for interactive charts
```

```python
w.plot()                         # matplotlib: one line per curve, labelled with its group values
w.semilogx(), w.loglog(), w.stem()
h.bode()                         # magnitude and phase of a complex response
w.plot(backend="altair")         # interactive chart (notebooks); pw.set_plot_backend("altair")
pw.compression_plot(gain_db)     # response, extrapolated line and the compression point
```

## Measurements

The semantics follow OCEAN, and the OCEAN and pycircuit names are aliases (`pw.dB20`,
`pw.unityGainFreq`, `pw.riseTime`, `pw.IIP3`, ...):

| | |
|---|---|
| values | `value(x)`, `ymax()`, `ymin()`, `xmax()` (x at the largest y), `xmin()`, `average()`/`mean()`, `rms()`, `stddev()` |
| crossings | `cross(threshold, edge=1, type="either")` (edges from 1, negative from the end), `delay(other, ...)`, `frequency()`, `period()` |
| transient | `rise_time()`, `fall_time()`, `slew_rate()`, `overshoot()`, `settling_time()` |
| frequency | `bandwidth(db, "low"/"high"/"band")`, `unity_gain_frequency()`, `phase_margin()`, `gain_margin()` |
| shape | `deriv()`, `integ(xfrom, xto)`, `iinteg()`, `clip(xfrom, xto)`, `dft()`, `leaf(**groups)` |
| families | `reduce(how, over)`, `ymax(axis=...)` and friends, `reorder()`, `swapaxes()`, `along()`, `dimension_first()`, `leaf()`, `w[i]`, `w[a:b]` |
| math | `+ - * /`, `**`, `10 ** w`, `abs()`/`mag()`, `db10()`, `db20()`, `phase()`, `real()`, `imag()`, `conj()`, `log10()`, `exp()`, `sqrt()` |
| RF | `pw.im2`, `pw.im3`, `pw.iip2`, `pw.iip3`, `pw.compression_point` |

The elementwise functions (`pw.db20`, `pw.phase`, `pw.mag`, ...) also take plain numbers and
numpy arrays. Complex data is a `Struct{re, im}` column; arithmetic and `db20()`/`phase()` handle it, and
`pl.col("h").cx.db20()` (registered by `polars_waveform.cx`) does the same in plain Polars.

## Symbolic waveforms

Polars can store Python objects but cannot compute with them. `pw.PandasWaveform` holds such
values, for example sympy expressions from a symbolic circuit analysis, in the same layout
(groups, sweep, value) on a pandas Series, which applies Python operators element by element.
`pw.from_arrays` picks the kind the values need:

```python
import sympy
R, C = sympy.symbols("R C", positive=True)

h = pw.from_arrays(freqs, [1 / (1 + 2j * sympy.pi * f * R * C) for f in freqs],
                   xlabels=["freq"], ylabel="H", xunits=["Hz"])
h.db20()                                    # still symbolic
h.map(sympy.simplify)                       # any function, value by value
h.subs({R: 1e3, C: 1e-9}).bandwidth()       # numbers: measured on numeric()
```

Measurements and plots run on `numeric()`, the equivalent `Waveform`; it names the free symbols
while some are left. Needs `pip install "polars-waveform[pandas]"`. Both kinds implement
`pw.WaveformBase`, the contract for waveform kinds: names, units, `from_arrays` / `to_arrays`,
arithmetic and the elementwise functions.

## Result sources

`pw.ResultSource` is the shape shared by result objects that hand out waveforms: `leaves`,
`names`, `v(signal, **params)` and `scan()`. `polars_psf.Result` implements it for simulation
results; a lab-data handler can implement it too, so analysis code runs on both.

## Development

```sh
pip install -e ".[test]"
pytest tests
ruff check .
```

## License

MIT.
