Metadata-Version: 2.4
Name: psfox
Version: 0.2.0
Classifier: Programming Language :: Rust
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Scientific/Engineering
Classifier: Intended Audience :: Developers
Requires-Dist: numpy>=1.23
License-File: LICENSE-MIT
License-File: LICENSE-APACHE
Summary: A fast Rust-backed reader for Cadence binary PSF files, with a clean numpy API
License-Expression: MIT OR Apache-2.0
Requires-Python: >=3.9
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Homepage, https://github.com/Zwelckovich/psfox
Project-URL: Repository, https://github.com/Zwelckovich/psfox

# psfox

A fast Rust-backed reader for **Cadence binary PSF** files, with a clean numpy API.

The Cadence `psf-parser` (pure Python) is slow and memory-hungry — it OOM-crashes on large
files because it stores every sample as an individual Python object. `psfox` is a Rust core
(PyO3 + maturin) that reads the same files **much faster and with low RAM**, returning numpy
arrays directly.

## Performance

Measured against the pinned pure-Python `psf-parser` (each parser in a fresh subprocess, median
of repeated runs):

| Dataset | Size | psfox | psf-parser | speedup | psfox peak RSS |
|---------|------|-------|------------|---------|----------------|
| noise (struct-typed) | ~634 MB | **~1.2 s** | ~60 s | **~49×** | ~1.3 GB |
| AC (complex) | ~133 MB | **~0.34 s** | ~17 s | **~51×** | ~0.27 GB |

Small files (S-parameter, DC) parse in **single-digit milliseconds** (tens of times faster). The
output is **bit-identical** to `psf-parser` (exact, NaN/Inf-aware) on every tested file, and the
memory ceiling is low enough to parse a 600 MB+ file on a 4 GB machine without OOM.

## What it reads

psfbin **1.1**, big-endian: swept and non-swept analyses; real and complex vectors; and
**struct/composite types** — swept struct traces (noise) become numpy structured arrays, and
non-swept struct values (e.g. `dcOpInfo` per-device operating points, which mix `float64` with an
`Int8` region field) become Python dicts. Common analysis types (noise, AC, DC, S-parameter, dc-info)
are supported. Out of scope (for now): PSF ASCII, PSF-XL/transient, and multi-sweep / family / corner
files.

## Requirements

- **Python ≥ 3.9**
- **numpy ≥ 1.23** — a runtime dependency (the reader returns numpy arrays). It is declared in the
  package metadata, so installing psfox **pulls numpy in automatically**.

## Install

Install from PyPI — psfox ships **abi3 wheels** (Linux x86_64, Windows x86_64) plus an sdist, and
numpy is pulled in automatically:

```bash
uv add psfox      # or: pip install psfox
```

No matching wheel for your platform/architecture? Build one with maturin (no Rust toolchain is
needed to *install* the resulting wheel), then install it:

```bash
maturin build --release
# -> target/wheels/psfox-0.2.0-cp39-abi3-<platform>.whl
pip install target/wheels/psfox-0.2.0-cp39-abi3-<platform>.whl
```

Notes:
- The wheel is **abi3** (`cp39-abi3`): one wheel works for Python **3.9, 3.10, … 3.13+** — no
  rebuild per Python version.
- A wheel is **platform-specific** (Linux / macOS / Windows × architecture). For **Linux x86_64 and
  Windows x86_64** the published wheels cover you (`uv add psfox`); for other platforms (macOS, Linux
  aarch64) build on the target platform or **cross-build a Linux wheel from any host** (see below).

To build from source instead (needs the Rust toolchain), see `CONTRIBUTING.md` in the repository.

### Cross-building a Linux wheel

psfox has no system C dependencies — the only native code is the Rust core, built as an `abi3`
extension — so a `manylinux` wheel can be cross-compiled from **any** host (Windows, macOS, Linux)
using [zig](https://ziglang.org/) as the linker. No Docker or virtual machine required:

```bash
rustup target add x86_64-unknown-linux-gnu   # one-time: add the Linux target
pip install ziglang                           # the zig toolchain, shipped as a Python package
maturin build --release --target x86_64-unknown-linux-gnu --zig --compatibility manylinux2014
# -> target/wheels/psfox-0.2.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
```

The `manylinux2014` tag links against an old glibc, so the wheel installs on essentially any modern
Linux distribution.

On **Windows**, maturin may not auto-detect the zig that ships inside the `ziglang` package (it fails
with `Failed to find zig`). If so, put that package's folder on `PATH` for the build:

```powershell
$env:PATH = "$PWD\.venv\Lib\site-packages\ziglang;$env:PATH"
```

Because the wheel is cross-compiled, smoke-test `import psfox` on an actual Linux host (or WSL) — it
cannot run on the machine that built it.

## Usage

```python
import psfox

psf = psfox.read_psf("path/to/data.sp")
print(psf)                         # <Psf sweeps=1 traces=16 values=0>
print(psf.header["PSFversion"])    # header is a plain dict

# Sweep axes and traces are numpy arrays
for sw in psf.sweeps:
    print(sw.name, sw.data.dtype, sw.data.shape)   # e.g. freq float64 (700,)

t = psf.traces[0]
print(t.name, t.data.dtype, t.data.shape)          # e.g. s11 complex128 (700,)

# Non-swept DC -> .values (Python scalars), no traces/sweeps
dc = psfox.read_psf("path/to/data.dc")
print(len(dc.values), dc.values[0].name, dc.values[0].data)   # e.g. 5349 'net1' -7.7e-04

# Non-swept struct values (e.g. dcOpInfo) -> .values with dict data, one dict per device
info = psfox.read_psf("path/to/dcOpInfo.info")
v = info.values[0]
print(v.name, type(v.data).__name__)   # e.g. 'Q1'  dict   (one struct per device instance)
print(v.data["region"], v.data["temp"])  # region -> int, other members -> float

# Struct traces -> numpy structured array, one float64 field per contribution
noise = psfox.read_psf("path/to/data.noise")
tr = noise.traces[0]
if tr.data.dtype.names is not None:                # struct vs flat discriminator
    print(tr.data.dtype.names[:3])                 # e.g. ('<dev>.thermal', '<dev>.shot', ...)
    print(tr.data[tr.data.dtype.names[0]])         # float64[N] for that contribution
```

Run a script with the project environment: `uv run python your_script.py` (or activate `.venv`).

## API

| Object | Attributes |
|--------|------------|
| `read_psf(path) -> Psf` | parse a psfbin file |
| `Psf` | `.header` (dict), `.sweeps` (list[`Sweep`]), `.traces` (list[`Trace`]), `.values` (list[`Value`]) |
| `Sweep`, `Trace` | `.name` (str), `.data` (numpy array) |
| `Value` | `.name` (str), `.data` (Python scalar, or a `dict` `{field: scalar}` for a struct-typed value) |
| `psfox.__version__` | version string |

- **flat vs struct trace:** `trace.data.dtype.names is None` → flat (`float64[N]` / `complex128[N]`);
  not `None` → struct (one named `float64` field per contribution).
- **swept vs non-swept:** swept analyses populate `.traces` (+ `.sweeps`); non-swept analyses populate
  `.values` — each `.data` is a Python scalar, or a `dict` of `{field: scalar}` for a struct-typed
  value (e.g. `dcOpInfo`), mixing `float`/`int`/`complex`/`str` per the struct's members.
- **real vs complex** is inferred **per trace** from the file, not assumed file-wide.

Type stubs and a `py.typed` marker ship with the package, so editors give autocomplete and type
checking; `help(psfox.read_psf)` shows the inline docstrings.

## Distribution

psfox is built with **maturin + PyO3** — the same stack as polars and pydantic-core — and is
distributed on PyPI as **abi3 wheels** (Linux x86_64, Windows x86_64) plus an sdist. Thanks to abi3,
that is one wheel per *platform* rather than one per platform × Python version. macOS and Linux
aarch64 wheels aren't published yet — build those locally (see *Install*).

## Contributing

Build, test, and benchmark instructions live in `CONTRIBUTING.md`.

