diff --git a/README.md b/README.md
index 2475c81..ae51c5b 100644
--- a/README.md
+++ b/README.md
@@ -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.
@@ -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")
```
@@ -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)
```
@@ -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)
```
@@ -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")
```
@@ -229,11 +229,15 @@ print(f"DNL: [{result['dnl'].min():.2f}, {result['dnl'].max():.2f}] LSB")
Digital Calibration
```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")
```
@@ -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")
```
diff --git a/python/src/adctoolbox/_bundled_skills/skills/adctoolbox-user-guide/SKILL.md b/python/src/adctoolbox/_bundled_skills/skills/adctoolbox-user-guide/SKILL.md
index c485007..582422c 100644
--- a/python/src/adctoolbox/_bundled_skills/skills/adctoolbox-user-guide/SKILL.md
+++ b/python/src/adctoolbox/_bundled_skills/skills/adctoolbox-user-guide/SKILL.md
@@ -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
@@ -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. import X` |
+| Submodule-only public tool (`siggen`, `toolset`, `aout`, `calibration`, `fundamentals`, `spectrum`) | `from adctoolbox. 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
@@ -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.
diff --git a/python/src/adctoolbox/_bundled_skills/skills/adctoolbox-user-guide/references/advanced-debug.md b/python/src/adctoolbox/_bundled_skills/skills/adctoolbox-user-guide/references/advanced-debug.md
index 867f8ac..ac01141 100644
--- a/python/src/adctoolbox/_bundled_skills/skills/adctoolbox-user-guide/references/advanced-debug.md
+++ b/python/src/adctoolbox/_bundled_skills/skills/adctoolbox-user-guide/references/advanced-debug.md
@@ -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
@@ -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
diff --git a/python/src/adctoolbox/_bundled_skills/skills/adctoolbox-user-guide/references/api-quickref.md b/python/src/adctoolbox/_bundled_skills/skills/adctoolbox-user-guide/references/api-quickref.md
index f8a2f35..c685cc5 100644
--- a/python/src/adctoolbox/_bundled_skills/skills/adctoolbox-user-guide/references/api-quickref.md
+++ b/python/src/adctoolbox/_bundled_skills/skills/adctoolbox-user-guide/references/api-quickref.md
@@ -89,7 +89,9 @@ from adctoolbox import (
```python
from adctoolbox.toolset import generate_aout_dashboard, generate_dout_dashboard
from adctoolbox.fundamentals import convert_cap_to_weight
-from adctoolbox.aout import analyze_phase_plane, analyze_error_phase_plane
+from adctoolbox.aout import (
+ analyze_phase_plane, analyze_error_phase_plane, decompose_harmonic_error,
+)
```
### Default Entry Points
@@ -119,16 +121,48 @@ adctoolbox-install-skill --dev
## Key Conventions
-- `fit_sine_4param(... )["frequency"]` is normalized `Fin/Fs`, not Hz.
-- Calibration helpers such as `calibrate_weight_sine(..., freq=...)` and
- `calibrate_weight_sine_lite(..., freq=...)` expect normalized `Fin/Fs`.
-- `find_coherent_frequency(...)` returns a tuple:
- `(fin_actual_hz, best_bin)`.
-- `analyze_overflow(...)` returns a tuple.
-- `analyze_enob_sweep(...)` returns a tuple:
- `(enob_sweep, n_bits_vec)`.
-- `calibrate_weight_sine_lite(...)` returns weights only.
-- `analyze_weight_radix(...)` returns a dict.
-- `compute_spectrum(...)` returns both `metrics` and `plot_data`.
+### Frequency
+
+- `fit_sine_4param(...)["frequency"]` is normalized `Fin/Fs` (range 0–0.5),
+ not Hz.
+- `calibrate_weight_sine(bits, freq=...)` and `calibrate_weight_sine_lite(bits,
+ freq)` expect normalized `freq = Fin/Fs`. The `_lite` variant takes `freq`
+ positionally (not as a keyword) and is required (no auto-search).
+- `analyze_enob_sweep(bits, freq=...)` and `generate_dout_dashboard(bits,
+ freq=...)` also expect normalized `freq`.
+- `generate_aout_dashboard(aout, fs=..., freq=...)` takes `freq` in Hz (it
+ normalizes internally).
+- `find_coherent_frequency(fs, fin_target, n_fft)` returns a tuple
+ `(fin_actual_hz, best_bin)`. Argument order matters: `fs` first, then
+ `fin_target`, then `n_fft`.
+- `analyze_spectrum` does NOT take `Fin` — the fundamental is auto-detected.
+
+### Return shapes
+
+- Spectrum metrics dicts (`analyze_spectrum`, `analyze_spectrum_polar`,
+ `compute_spectrum["metrics"]`) use lowercase `_dbc` keys: `enob`,
+ `sndr_dbc`, `sfdr_dbc`, `snr_dbc`, `thd_dbc`, `sig_pwr_dbfs`,
+ `noise_floor_dbfs`, `nsd_dbfs_hz`, `harmonics_dbc` — **not**
+ uppercase `SNDR` / `SFDR` / `ENOB`.
+- `compute_spectrum(...)` returns a dict with top-level keys `metrics` and
+ `plot_data` (the latter has `freq`, `power_spectrum_db_plot`,
+ `complex_spectrum`, `fundamental_bin`, …).
+- `calibrate_weight_sine(...)` returns a dict: `weight`, `offset`,
+ `calibrated_signal`, `ideal`, `error`, `refined_frequency`.
+ `calibrate_weight_sine_lite(...)` returns just the weights ndarray.
+- `analyze_bit_activity(bits)` returns an ndarray (% of 1's per bit).
+- `analyze_overflow(bits, weight)` returns a 4-tuple of ndarrays
+ `(range_min, range_max, ovf_pct_zero, ovf_pct_one)`. The second argument
+ is the calibrated weights vector — not optional.
+- `analyze_enob_sweep(bits, freq=...)` returns `(enob_sweep, n_bits_vec)`.
+- `analyze_weight_radix(weights)` returns a dict (`radix`, `wgtsca`, `effres`).
+- `fit_static_nonlin(sig_distorted, order)` returns
+ `(k2, k3, fitted_sine, fitted_transfer)`. Input is a distorted signal,
+ not INL/DNL data; `order >= 2`.
+- `convert_cap_to_weight(caps_bit, caps_bridge, caps_parasitic)` takes
+ three same-length arrays (LSB→MSB; pass zeros where there is no
+ bridge / parasitic) and returns `(weights, c_total)`.
+- `analyze_phase_plane(aout, fs=...)` and `analyze_error_phase_plane(aout,
+ fs=...)` do NOT take `Fin` — they self-fit the fundamental.
If unsure which file to copy from, open `example-map.md`.
diff --git a/python/tests/integration/test_user_guide_skill_examples.py b/python/tests/integration/test_user_guide_skill_examples.py
new file mode 100644
index 0000000..6015afa
--- /dev/null
+++ b/python/tests/integration/test_user_guide_skill_examples.py
@@ -0,0 +1,290 @@
+"""Smoke tests that actually run every code example shown in the bundled
+`adctoolbox-user-guide` skill.
+
+Background — Issue #11 + the user-guide-skill-revamp PR (#13) shipped doc
+examples that didn't match the real API (made-up params like `Fin=`,
+`n_bits=`; wrong return keys like `metrics["SNDR"]` instead of
+`metrics["sndr_dbc"]`). The eval used during the revamp only scored text
+similarity from subagents — nothing actually imported and called the
+code.
+
+These tests are the real fence: if a future SKILL.md / advanced-debug.md
+example diverges from the API again, this file fails.
+
+Each test mirrors a code block from one of:
+- `python/src/adctoolbox/_bundled_skills/skills/adctoolbox-user-guide/SKILL.md`
+- `.../adctoolbox-user-guide/references/advanced-debug.md`
+- `.../adctoolbox-user-guide/references/api-quickref.md`
+"""
+
+import numpy as np
+import pytest
+
+
+# ----------------------------------------------------------------------
+# Shared fixture — a small synthetic dout/aout pair used by most tests
+# ----------------------------------------------------------------------
+
+@pytest.fixture(scope="module")
+def synth_capture():
+ """8-bit, 4096-sample coherent sine — small enough to be fast,
+ large enough that all analysis APIs accept it."""
+ from adctoolbox.siggen import ADC_Signal_Generator
+ from adctoolbox import find_coherent_frequency
+
+ n_samples = 4096
+ fs = 100e6
+ n_bits = 8
+ fin_target = 5e6
+ fin_actual, _ = find_coherent_frequency(fs, fin_target, n_samples)
+
+ gen = ADC_Signal_Generator(N=n_samples, Fs=fs, Fin=fin_actual, A=0.45, DC=0.5)
+ aout = gen.get_clean_signal()
+ aout = gen.apply_thermal_noise(noise_rms=1e-4, input_signal=aout)
+ dout_codes_signal = gen.apply_quantization_noise(
+ n_bits=n_bits, quant_range=(0.0, 1.0), input_signal=aout
+ )
+
+ code_levels = np.round((dout_codes_signal - 0.0) / (1.0 / (2 ** n_bits))).astype(int)
+ code_levels = np.clip(code_levels, 0, 2 ** n_bits - 1)
+ bits = np.array([
+ [(c >> b) & 1 for b in range(n_bits - 1, -1, -1)]
+ for c in code_levels
+ ])
+
+ return {
+ "aout": aout.astype(float),
+ "bits": bits,
+ "fs": fs,
+ "fin_actual": fin_actual,
+ "n_bits": n_bits,
+ "n_samples": n_samples,
+ "freq_norm": fin_actual / fs,
+ }
+
+
+# ----------------------------------------------------------------------
+# SKILL.md §3 — basic spectrum workflow
+# ----------------------------------------------------------------------
+
+def test_skill_md_section_3_spectrum_workflow(synth_capture):
+ 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.spectrum import compute_spectrum
+
+ aout = synth_capture["aout"]
+ bits = synth_capture["bits"]
+ fs = synth_capture["fs"]
+ n_samples = synth_capture["n_samples"]
+
+ validate_aout_data(aout)
+ validate_dout_data(bits)
+
+ fin_hz, k = find_coherent_frequency(fs, 5e6, n_samples)
+ assert isinstance(k, (int, np.integer))
+ assert fin_hz > 0
+
+ metrics = analyze_spectrum(aout, fs=fs, create_plot=False)
+ assert {"sndr_dbc", "sfdr_dbc", "enob"} <= set(metrics.keys())
+ assert metrics["enob"] > 0
+
+ polar = analyze_spectrum_polar(aout, fs=fs, create_plot=False)
+ assert "sndr_dbc" in polar
+
+ full = compute_spectrum(aout, fs=fs)
+ assert {"metrics", "plot_data"} <= set(full.keys())
+ assert "freq" in full["plot_data"]
+
+ fit = fit_sine_4param(aout)
+ assert 0.0 <= fit["frequency"] <= 0.5
+ assert {"amplitude", "phase", "dc_offset", "rmse"} <= set(fit.keys())
+
+
+# ----------------------------------------------------------------------
+# SKILL.md §4 — basic digital calibration
+# ----------------------------------------------------------------------
+
+def test_skill_md_section_4_calibration(synth_capture):
+ from adctoolbox import calibrate_weight_sine
+ from adctoolbox.calibration import calibrate_weight_sine_lite
+
+ bits = synth_capture["bits"]
+ freq_norm = synth_capture["freq_norm"]
+
+ full = calibrate_weight_sine(bits, freq=freq_norm)
+ assert "weight" in full
+ assert isinstance(full["weight"], np.ndarray)
+
+ fast = calibrate_weight_sine_lite(bits, freq_norm)
+ assert isinstance(fast, np.ndarray)
+ assert fast.ndim == 1
+ assert len(fast) == bits.shape[1]
+
+
+# ----------------------------------------------------------------------
+# advanced-debug.md — dashboards
+# ----------------------------------------------------------------------
+
+def test_advanced_dashboards(synth_capture, tmp_path):
+ import matplotlib
+ matplotlib.use("Agg")
+ from adctoolbox.toolset import generate_aout_dashboard, generate_dout_dashboard
+
+ aout = synth_capture["aout"]
+ bits = synth_capture["bits"]
+ fs = synth_capture["fs"]
+ fin_actual = synth_capture["fin_actual"]
+ freq_norm = synth_capture["freq_norm"]
+
+ fig_a, axes_a = generate_aout_dashboard(
+ aout, fs=fs, freq=fin_actual, output_path=str(tmp_path / "aout.png")
+ )
+ fig_d, axes_d = generate_dout_dashboard(
+ bits, freq=freq_norm, output_path=str(tmp_path / "dout.png")
+ )
+ assert (tmp_path / "aout.png").exists()
+ assert (tmp_path / "dout.png").exists()
+
+
+# ----------------------------------------------------------------------
+# advanced-debug.md — phase-plane
+# ----------------------------------------------------------------------
+
+def test_advanced_phase_plane(synth_capture):
+ import matplotlib
+ matplotlib.use("Agg")
+ from adctoolbox.aout import analyze_phase_plane, analyze_error_phase_plane
+
+ aout = synth_capture["aout"]
+ fs = synth_capture["fs"]
+
+ pp = analyze_phase_plane(aout, fs=fs, create_plot=False)
+ assert "lag" in pp and "outliers" in pp
+
+ epp = analyze_error_phase_plane(aout, fs=fs, create_plot=False)
+ assert {"residual", "fitted_params"} <= set(epp.keys())
+
+
+# ----------------------------------------------------------------------
+# advanced-debug.md — bit-level / overflow / enob sweep / radix
+# ----------------------------------------------------------------------
+
+def test_advanced_bit_level(synth_capture):
+ import matplotlib
+ matplotlib.use("Agg")
+ from adctoolbox import (
+ analyze_bit_activity,
+ analyze_overflow,
+ analyze_enob_sweep,
+ analyze_weight_radix,
+ calibrate_weight_sine,
+ )
+
+ bits = synth_capture["bits"]
+ freq_norm = synth_capture["freq_norm"]
+
+ activity = analyze_bit_activity(bits, create_plot=False)
+ assert isinstance(activity, np.ndarray)
+ assert activity.shape == (bits.shape[1],)
+
+ weight_dict = calibrate_weight_sine(bits, freq=freq_norm)
+ weights = weight_dict["weight"]
+
+ rmin, rmax, ovf0, ovf1 = analyze_overflow(bits, weights, create_plot=False)
+ assert all(isinstance(arr, np.ndarray) for arr in (rmin, rmax, ovf0, ovf1))
+
+ enob_sweep, n_bits_vec = analyze_enob_sweep(
+ bits, freq=freq_norm, create_plot=False
+ )
+ assert enob_sweep.shape == n_bits_vec.shape
+
+ radix_info = analyze_weight_radix(weights, create_plot=False)
+ assert {"radix", "wgtsca", "effres"} <= set(radix_info.keys())
+
+
+# ----------------------------------------------------------------------
+# advanced-debug.md — static nonlinearity
+# ----------------------------------------------------------------------
+
+def test_advanced_static_nonlinearity(synth_capture):
+ from adctoolbox import fit_static_nonlin
+
+ aout = synth_capture["aout"]
+
+ k2, k3, fitted_sine, fitted_transfer = fit_static_nonlin(aout, order=3)
+ assert isinstance(fitted_sine, np.ndarray)
+ assert isinstance(fitted_transfer, tuple)
+ assert len(fitted_transfer) == 2
+
+
+# ----------------------------------------------------------------------
+# advanced-debug.md — cap-to-weight
+# ----------------------------------------------------------------------
+
+def test_advanced_cap_to_weight():
+ from adctoolbox.fundamentals import convert_cap_to_weight
+
+ n_bits = 6
+ caps_bit = np.array([2 ** i for i in range(n_bits)], dtype=float)
+ caps_bridge = np.zeros(n_bits, dtype=float)
+ caps_parasitic = np.zeros(n_bits, dtype=float)
+
+ weights, c_total = convert_cap_to_weight(caps_bit, caps_bridge, caps_parasitic)
+ assert isinstance(weights, np.ndarray)
+ assert c_total > 0
+
+
+# ----------------------------------------------------------------------
+# api-quickref.md — flat-export presence (one big import sanity check)
+# ----------------------------------------------------------------------
+
+def test_api_quickref_flat_imports_resolve():
+ from adctoolbox import (
+ analyze_spectrum, analyze_spectrum_polar,
+ find_coherent_frequency, fit_sine_4param,
+ calibrate_weight_sine,
+ analyze_error_by_value, analyze_error_by_phase,
+ analyze_error_pdf, analyze_error_spectrum,
+ analyze_error_autocorr, analyze_error_envelope_spectrum,
+ analyze_inl_from_sine,
+ analyze_decomposition_time, analyze_decomposition_polar,
+ fit_static_nonlin,
+ analyze_bit_activity, analyze_overflow,
+ analyze_weight_radix, analyze_enob_sweep,
+ plot_residual_scatter,
+ calculate_walden_fom, calculate_schreier_fom,
+ calculate_thermal_noise_limit, calculate_jitter_limit,
+ db_to_mag, mag_to_db, db_to_power, power_to_db,
+ snr_to_enob, enob_to_snr, snr_to_nsd, nsd_to_snr,
+ bin_to_freq, freq_to_bin, fold_frequency_to_nyquist,
+ ntf_analyzer,
+ )
+ # Each should be a callable / class
+ locals_dict = locals()
+ for name, obj in locals_dict.items():
+ assert callable(obj), f"{name} is not callable"
+
+
+def test_api_quickref_submodule_imports_resolve():
+ from adctoolbox.siggen import ADC_Signal_Generator
+ from adctoolbox.calibration import calibrate_weight_sine_lite
+ from adctoolbox.fundamentals import (
+ validate_aout_data, validate_dout_data, convert_cap_to_weight
+ )
+ from adctoolbox.aout import analyze_phase_plane, analyze_error_phase_plane
+ from adctoolbox.spectrum import compute_spectrum
+ from adctoolbox.toolset import (
+ generate_aout_dashboard, generate_dout_dashboard
+ )
+ for obj in (
+ ADC_Signal_Generator, calibrate_weight_sine_lite,
+ validate_aout_data, validate_dout_data, convert_cap_to_weight,
+ analyze_phase_plane, analyze_error_phase_plane,
+ compute_spectrum, generate_aout_dashboard, generate_dout_dashboard,
+ ):
+ assert callable(obj)