Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 21 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ python exp_s01_analyze_spectrum_simplest.py
from adctoolbox import analyze_spectrum

# Analyze signal spectrum
result = analyze_spectrum(signal, fs=800e6, show_plot=True)
print(f"ENOB: {result['enob']:.2f} bits, SNDR: {result['sndr_db']:.2f} dB")
result = analyze_spectrum(signal, fs=800e6, create_plot=True)
print(f"ENOB: {result['enob']:.2f} bits, SNDR: {result['sndr_dbc']:.2f} dB")
```

See [Usage Examples](#usage-examples) section below for detailed code examples.
Expand Down Expand Up @@ -167,8 +167,8 @@ Helper functions for unit conversions and metric calculations.
from adctoolbox import analyze_spectrum

# Single-tone analysis
result = analyze_spectrum(signal, fs=800e6, harmonic=5, show_plot=True)
print(f"ENOB: {result['enob']:.2f} bits, SNDR: {result['sndr_db']:.2f} dB")
result = analyze_spectrum(signal, fs=800e6, max_harmonic=5, create_plot=True)
print(f"ENOB: {result['enob']:.2f} bits, SNDR: {result['sndr_dbc']:.2f} dB")
```
</details>

Expand All @@ -184,17 +184,17 @@ from adctoolbox import (
)

# Error PDF
result = analyze_error_pdf(signal, resolution=12, show_plot=True)
result = analyze_error_pdf(signal, resolution=12, create_plot=True)
print(f"Std: {result['sigma']:.2f} LSB, KL div: {result['kl_divergence']:.4f}")

# Error autocorrelation
result = analyze_error_autocorr(signal, max_lag=100, show_plot=True)
result = analyze_error_autocorr(signal, max_lag=100, create_plot=True)

# Error spectrum
result = analyze_error_spectrum(signal, fs=800e6, show_plot=True)
result = analyze_error_spectrum(signal, fs=800e6, create_plot=True)

# Error envelope spectrum (AM detection)
result = analyze_error_envelope_spectrum(signal, fs=800e6, show_plot=True)
result = analyze_error_envelope_spectrum(signal, fs=800e6, create_plot=True)
```
</details>

Expand All @@ -208,8 +208,8 @@ from adctoolbox import fit_sine_4param, analyze_decomposition_time
result = fit_sine_4param(signal, frequency_estimate=0.1)
print(f"Freq: {result['frequency']:.6f}, Amp: {result['amplitude']:.4f}")

# Harmonic decomposition
result = analyze_decomposition_time(signal, fs=800e6, harmonic=5, show_plot=True)
# Harmonic decomposition (does not take fs)
result = analyze_decomposition_time(signal, harmonic=5, create_plot=True)
```
</details>

Expand All @@ -219,7 +219,7 @@ result = analyze_decomposition_time(signal, fs=800e6, harmonic=5, show_plot=True
```python
from adctoolbox import analyze_inl_from_sine

result = analyze_inl_from_sine(signal, resolution=12, show_plot=True)
result = analyze_inl_from_sine(signal, num_bits=12, create_plot=True)
print(f"INL: [{result['inl'].min():.2f}, {result['inl'].max():.2f}] LSB")
print(f"DNL: [{result['dnl'].min():.2f}, {result['dnl'].max():.2f}] LSB")
```
Expand All @@ -229,11 +229,15 @@ print(f"DNL: [{result['dnl'].min():.2f}, {result['dnl'].max():.2f}] LSB")
<summary><b>Digital Calibration</b></summary>

```python
from adctoolbox import calibrate_weight_sine
from adctoolbox import calibrate_weight_sine, analyze_spectrum

# Weight calibration
result = calibrate_weight_sine(digital_codes, order=5)
print(f"SNR: {result['snr_db']:.2f} dB, THD: {result['thd_db']:.2f} dB")
# bits: (N_samples, N_bits) of {0, 1}; freq is normalized (Fin / Fs)
cal = calibrate_weight_sine(bits, freq=Fin / Fs, harmonic_order=5)
calibrated_signal = cal["calibrated_signal"] # also: cal["weight"], cal["offset"]

# Verify the calibration by analyzing the corrected waveform
metrics = analyze_spectrum(calibrated_signal, fs=Fs, create_plot=False)
print(f"SNR: {metrics['snr_dbc']:.2f} dBc, THD: {metrics['thd_dbc']:.2f} dBc")
```
</details>

Expand All @@ -254,10 +258,10 @@ A, DC, noise_rms = 0.49, 0.5, 100e-6
signal = A * np.sin(2*np.pi*Fin*t) + DC + np.random.randn(N) * noise_rms

# Analyze
result = analyze_spectrum(signal, fs=Fs, harmonic=5, show_plot=True)
result = analyze_spectrum(signal, fs=Fs, max_harmonic=5, create_plot=True)
snr_theory = amplitudes_to_snr(sig_amplitude=A, noise_amplitude=noise_rms)

print(f"Measured SNR: {result['snr_db']:.2f} dB")
print(f"Measured SNR: {result['snr_dbc']:.2f} dBc")
print(f"Theoretical SNR: {snr_theory:.2f} dB")
```
</details>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,31 +36,47 @@ Do NOT use for:

## 2. Critical conventions (read first — these are the common bug sources)

### Names

- **`bits`** is the per-sample binary decision matrix shape `(N_samples, N_bits)`,
values in `{0, 1}` — used by every digital-calibration / bit-level helper.
- **`aout`** is the analog output (1D `float` array) — used by spectrum and
error-analysis helpers.
- ADC integer codes are *not* `bits`; convert separately if your data is
packed as integers.

### Frequency units

- `fs`, `Fin`, and plotting frequencies are in **Hz**.
- `fit_sine_4param(...)['frequency']` returns **normalized `Fin/Fs`**, not Hz.
- `calibrate_weight_sine`, `calibrate_weight_sine_lite`, and most
`dout` helpers expect **normalized `freq = Fin/Fs`**.
- `fs`, `Fin`, plotting frequencies: **Hz**.
- `fit_sine_4param(...)["frequency"]`: **normalized** `Fin/Fs` (range 0–0.5),
**not** Hz.
- `calibrate_weight_sine`, `calibrate_weight_sine_lite`, `analyze_enob_sweep`,
`generate_dout_dashboard`: `freq` parameter is **normalized** `Fin/Fs`.
- `generate_aout_dashboard`: `freq` is in **Hz** (it normalizes internally).
- `analyze_spectrum` does NOT take `Fin` — it auto-detects the fundamental
from the FFT.

### Return shapes are not uniform
### Return shapes

Most analysis functions return `dict`. Exceptions:
Most analysis functions return `dict`. Notable exceptions and dict-key gotchas:

| Function | Return |
|---|---|
| `find_coherent_frequency` | `tuple (fin_hz, bin_idx)` |
| `analyze_bit_activity` | `ndarray` |
| `analyze_overflow` | `tuple` |
| `analyze_spectrum`, `analyze_spectrum_polar` | `dict` — keys: `enob`, `sndr_dbc`, `sfdr_dbc`, `snr_dbc`, `thd_dbc`, `sig_pwr_dbfs`, `noise_floor_dbfs`, `nsd_dbfs_hz`, `harmonics_dbc` |
| `compute_spectrum` | `dict` — top-level keys `metrics` (same as above) and `plot_data` (`freq`, `power_spectrum_db_plot`, `complex_spectrum`, `fundamental_bin`, …) |
| `fit_sine_4param` | `dict` — `frequency` (normalized), `amplitude`, `phase`, `dc_offset`, `rmse`, `fitted_signal`, `residuals` |
| `find_coherent_frequency` | `tuple (fin_actual_hz, best_bin)` |
| `calibrate_weight_sine` | `dict` — `weight`, `offset`, `calibrated_signal`, `ideal`, `error`, `refined_frequency` |
| `calibrate_weight_sine_lite` | `ndarray` (weights only) |
| `analyze_bit_activity` | `ndarray` (% of 1's per bit, length = N_bits) |
| `analyze_overflow` | `tuple` of 4 ndarrays `(range_min, range_max, ovf_pct_zero, ovf_pct_one)` |
| `analyze_enob_sweep` | `tuple (enob_sweep, n_bits_vec)` |
| `fit_static_nonlin` | `tuple` |
| `calibrate_weight_sine_lite` | `ndarray` |
| `analyze_weight_radix` | `dict` — `radix`, `wgtsca`, `effres` |
| `fit_static_nonlin` | `tuple (k2, k3, fitted_sine, fitted_transfer)` |
| `convert_cap_to_weight` | `tuple (weights, c_total)` |
| `analyze_weight_radix` | `dict` (was `ndarray` in old versions) |
| `compute_spectrum` | both metrics and plot data |

When docs conflict, trust the current `__init__.py` exports and
packaged examples over older README text.
When docs conflict, trust the current `__init__.py` exports + the
`tests/integration/test_user_guide_skill_examples.py` smoke tests.

## 3. Basic workflow — spectrum

Expand All @@ -69,46 +85,65 @@ from adctoolbox import (
analyze_spectrum, analyze_spectrum_polar,
find_coherent_frequency, fit_sine_4param,
)
from adctoolbox.fundamentals import validate_aout_data, validate_dout_data
from adctoolbox.fundamentals import validate_aout_data

validate_dout_data(dout)
fin_hz, k = find_coherent_frequency(fs=fs, n=len(dout), fin_target=fin_target_hz)
metrics = analyze_spectrum(dout, fs=fs, Fin=fin_hz, n_bits=N)
print(metrics["SNDR"], metrics["SFDR"], metrics["ENOB"])
validate_aout_data(aout)
metrics = analyze_spectrum(aout, fs=fs, create_plot=False)
print(metrics["sndr_dbc"], metrics["sfdr_dbc"], metrics["enob"])
```

To set up a coherent capture *upstream* (where you control the stimulus
frequency), snap `Fin` to an FFT bin first:

```python
fin_hz, k_bin = find_coherent_frequency(fs, fin_target_hz, n_fft=len(aout))
# now drive the test with fin_hz
```

Pick the variant by output:
- `analyze_spectrum` — standard magnitude spectrum + SNDR/SFDR/ENOB/THD
- `analyze_spectrum_polar` — complex/phase-aware spectrum (I/Q or mixer contexts)
- `compute_spectrum` — both metrics and plot-ready data (use when you
want to customize plotting)
- `find_coherent_frequency` — pre-step to align `Fin` to an FFT bin
- `analyze_spectrum` — magnitude spectrum + SNDR/SFDR/ENOB/THD metrics dict
- `analyze_spectrum_polar` — phase-aware (I/Q or mixer contexts); same keys
- `compute_spectrum` (from `adctoolbox.spectrum`) — both metrics and plot-ready
data (access via `result["plot_data"]["freq"]` etc.)
- `find_coherent_frequency` — pre-step at *signal generation* time, not analysis
- `fit_sine_4param` — pre-step for nonlinearity work; remember its
`'frequency'` key is normalized
`"frequency"` key is normalized `Fin/Fs`

## 4. Basic workflow — digital calibration

```python
from adctoolbox import calibrate_weight_sine
from adctoolbox.calibration import calibrate_weight_sine_lite
from adctoolbox.fundamentals import validate_dout_data

validate_dout_data(bits) # bits: (N_samples, N_bits) in {0, 1}

freq_norm = fin_hz / fs # normalized — not Hz
weights_full = calibrate_weight_sine(dout, freq=freq_norm, n_bits=N)
weights_fast = calibrate_weight_sine_lite(dout, freq=freq_norm, n_bits=N)
freq_norm = fin_hz / fs # normalized — not Hz
result = calibrate_weight_sine(bits, freq=freq_norm)
weights = result["weight"]
calibrated = result["calibrated_signal"]

weights_fast = calibrate_weight_sine_lite(bits, freq_norm) # ndarray, no dict
```

Pick `_lite` when you need a fast estimate; use the full variant when
you need convergence quality or diagnostic fields.
`calibrate_weight_sine` returns a dict with `weight`, `offset`,
`calibrated_signal`, `ideal`, `error`, `refined_frequency`. The `_lite` variant
returns just the weights ndarray and is positional (no `freq=` kw).

## 5. Import rules (compressed)

| Kind | Use |
|---|---|
| Anything re-exported by `adctoolbox.__init__` | `from adctoolbox import X` |
| Submodule-only public tool (e.g. `siggen`, `toolset`, `aout`, `calibration`, `fundamentals`) | `from adctoolbox.<submodule> import X` |
| Submodule-only public tool (`siggen`, `toolset`, `aout`, `calibration`, `fundamentals`, `spectrum`) | `from adctoolbox.<submodule> import X` |

If a flat import fails, check the submodule's `__init__.py` before
concluding the tool is gone.
concluding the tool is gone. Common submodule-only names:
`ADC_Signal_Generator` (siggen), `compute_spectrum` (spectrum),
`calibrate_weight_sine_lite` (calibration), `validate_aout_data` /
`validate_dout_data` / `convert_cap_to_weight` (fundamentals),
`analyze_phase_plane` / `analyze_error_phase_plane` (aout),
`generate_aout_dashboard` / `generate_dout_dashboard` (toolset).

## 6. Going further

Expand All @@ -121,3 +156,7 @@ concluding the tool is gone.
+ plot template, adapt `02_spectrum/exp_s03_analyze_spectrum_savefig.py`
(see `references/example-map.md` for the path). The packaged CLI
`adctoolbox-get-examples [dest]` dumps the full example tree.

Every code block in this file (and in `references/advanced-debug.md`) is
exercised by `python/tests/integration/test_user_guide_skill_examples.py`
— if a future edit breaks one, that test fails.
Original file line number Diff line number Diff line change
@@ -1,56 +1,90 @@
# ADCToolbox — Advanced Debug Reference

Load this file when the basic spectrum/calibration tier in `SKILL.md`
is not enough. Each section below is keyed by the user's likely
question, not by file layout.
is not enough. Each section is keyed by the user's likely question,
not by file layout.

Import conventions follow `SKILL.md` §5. Frequency conventions follow
`SKILL.md` §2.
`SKILL.md` §2 — pay particular attention: `generate_aout_dashboard`
takes `freq` in **Hz**, while `generate_dout_dashboard` takes `freq`
**normalized** to `Fin/Fs`.

## "I want one image showing all aout/dout diagnostics"

Goal: generate a multi-plot dashboard (time-domain + spectrum + INL/DNL + extras).

```python
from adctoolbox.toolset import generate_aout_dashboard, generate_dout_dashboard

generate_aout_dashboard(aout, fs=fs, savepath="aout_dash.png")
generate_dout_dashboard(dout, n_bits=N, fs=fs, savepath="dout_dash.png")
# aout dashboard: freq in Hz
fig, axes = generate_aout_dashboard(
aout, fs=fs, freq=fin_hz, output_path="aout_dash.png"
)

# dout dashboard: freq normalized (Fin/Fs)
fig, axes = generate_dout_dashboard(
bits, freq=fin_hz / fs, output_path="dout_dash.png"
)
```

Use `_aout_` when you have reconstructed analog output (floats); use
`_dout_` when you only have raw digital codes.
Use `generate_aout_dashboard` when you have reconstructed analog output
(floats); use `generate_dout_dashboard` when you only have the per-bit
decision matrix `bits`. Both return `(fig, axes_array)` and write a PNG
when `output_path=` is given.

## "I need to see nonlinearity structure, not just a single INL/DNL number"

```python
from adctoolbox.aout import analyze_phase_plane, analyze_error_phase_plane

analyze_phase_plane(aout, fs=fs, Fin=Fin) # full signal phase trajectory
analyze_error_phase_plane(aout, fs=fs, Fin=Fin) # error-only phase plane
pp = analyze_phase_plane(aout, fs=fs) # full signal trajectory
epp = analyze_error_phase_plane(aout, fs=fs) # error-only (sine subtracted first)
```

Use `analyze_error_phase_plane` after `fit_sine_4param` to isolate
nonlinearity from the fundamental.
Neither helper takes `Fin` — `analyze_phase_plane` infers the lag from
the FFT (`lag='auto'` default), and `analyze_error_phase_plane` calls
`fit_sine_4param` internally. Returns are dicts: `pp` has
`{lag, outliers}`, `epp` has `{residual, fitted_params, trend_coeffs,
hysteresis_gap}`. Use `epp` to isolate harmonic structure from the
fundamental.

## "I want per-bit behavior — activity, overflow, or ENOB vs bit depth"
## "I want per-bit behavior — activity, overflow, ENOB vs bit depth"

```python
from adctoolbox import analyze_bit_activity, analyze_overflow, analyze_enob_sweep, analyze_weight_radix
from adctoolbox import (
analyze_bit_activity, analyze_overflow,
analyze_enob_sweep, analyze_weight_radix,
calibrate_weight_sine,
)

activity = analyze_bit_activity(bits) # ndarray, length N_bits
weights = calibrate_weight_sine(bits, freq=fin_hz/fs)["weight"]
range_min, range_max, ovf_pct_zero, ovf_pct_one = analyze_overflow(bits, weights)
enob_sweep, n_bits_vec = analyze_enob_sweep(bits, freq=fin_hz/fs)
radix_info = analyze_weight_radix(weights) # dict: radix, wgtsca, effres
```

- `analyze_bit_activity(dout, n_bits=N)` → `ndarray`
- `analyze_overflow(dout, n_bits=N)` → `tuple`
- `analyze_enob_sweep(dout, n_bits=N)` → `tuple (enob_sweep, n_bits_vec)`
- `analyze_weight_radix(weights)` → `dict` (was a bare array in old versions — now a dict)
Notes on shapes:
- `analyze_bit_activity` returns an ndarray of length `N_bits`, not a dict.
- `analyze_overflow` is a 4-tuple of ndarrays — needs the calibrated
`weights` as its second argument (it's measuring digital-domain over-range,
not raw saturation).
- `analyze_enob_sweep` is a 2-tuple `(enob_sweep, n_bits_vec)` and runs
`calibrate_weight_sine` once internally.
- `analyze_weight_radix(weights)` returns a `dict` (was an ndarray in
pre-`v0.6` versions).

## "I have static INL/DNL data and want a nonlinearity fit"
## "I have a distorted sine and want to extract HD2/HD3 coefficients"

```python
from adctoolbox import fit_static_nonlin
coef, residual = fit_static_nonlin(inl_or_dnl, order=3) # returns tuple

k2, k3, fitted_sine, fitted_transfer = fit_static_nonlin(sig_distorted, order=3)
# fitted_transfer is a tuple (x_smooth, y_smooth) of the fitted nonlinear
# transfer characteristic. k2/k3 are normalized to k1 (unity-gain).
```

Note: input is a *distorted signal* (post-nonlinearity), not INL/DNL data.
`order` must be `>=2`; `k3` is `NaN` when `order < 3`.

## "I want to decompose total error into component contributions"

```python
Expand All @@ -72,21 +106,30 @@ Pick by the error view you need:

- by phase / by value → `analyze_error_by_phase`, `analyze_error_by_value`
- harmonic decomposition → `decompose_harmonic_error`
- spectral view of the error → `analyze_error_spectrum`, `analyze_error_envelope_spectrum`
- spectral view of the error → `analyze_error_spectrum`,
`analyze_error_envelope_spectrum`
- statistical → `analyze_error_pdf`, `analyze_error_autocorr`
- polar / time decomposition views → `analyze_decomposition_polar`, `analyze_decomposition_time`
- polar / time decomposition views → `analyze_decomposition_polar`,
`analyze_decomposition_time`
- INL from sine test → `analyze_inl_from_sine`

All of the above expect `fs` and `Fin` in Hz and assume a sine test
that has already passed `validate_aout_data` / `validate_dout_data`.
All of these expect `aout` (or fall back to `signal`) plus `fs` in Hz.
They each fit a sine internally if no `frequency` kwarg is supplied.

## "I need cap array → weight conversion for CDAC modeling"

```python
from adctoolbox.fundamentals import convert_cap_to_weight
weights, c_total = convert_cap_to_weight(cap_array) # returns tuple

weights, c_total = convert_cap_to_weight(caps_bit, caps_bridge, caps_parasitic)
```

Three input arrays (LSB-to-MSB ordering, all the same length):
`caps_bit` — main capacitor sizes, `caps_bridge` — bridge caps between
sub-DAC sections (use 0 where there is no bridge), `caps_parasitic` —
parasitic caps per node. Returns a `(weights, c_total)` tuple — never
a dict.

## When to fall back to `SKILL.md`

If the task is plain spectrum analysis (SNDR / SFDR / ENOB), basic
Expand Down
Loading
Loading