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
40 changes: 34 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ can tell whether the format it is looking at is one it understands.

## [Unreleased]

Nothing yet.

## [0.5.0] — 2026-09-14

### Added

- **`--device cpu|cuda` on `iqforge measure-leakage`**, defaulting to `cpu`.
Expand Down Expand Up @@ -86,11 +90,34 @@ can tell whether the format it is looking at is one it understands.
checkpoint recorded no environment at all, which is the state every published
grid is in, so the guard had never protected one. It now refuses that case
instead of waving it through.
- **`docs/release-notes/v0.5.0.md` says it is an unpublished draft.** The file
read as a shipped release while `__version__`, `CITATION.cff` and the newest
released CHANGELOG section all said `0.4.0` and no `v0.5.0` tag existed. It
now names that state at the top and points at `[Unreleased]`, so the four
places that carry a version agree about which one is real.
- **The refuse categories are documented with the same numbers everywhere.**
`measure-leakage` prints `category 4 ceiling (methodology 6.4)` and a
reader follows that citation into `docs/methodology.md` §6, which did not
contain the word "category" at all. The numbering matches §6.1–§6.4 by
construction and does not extend past it — `category 5` is a refusal while
`§6.5` is LoRaIQ, the one dataset that section did *not* eliminate. §6 now
opens with a cross-reference table covering all six categories, names the two
that cite something other than §6, and states that collision outright. A test
pins the code, SPEC and methodology together so they cannot drift apart again.
- **`PARITY_GATE_PASSED` says what it asserts.** `scripts/parity_gate.py` was
documented in two lines that did not say what it checks, so a reader could
not tell "ran with the same configuration" from "produced the same numbers".
It is the second one: run count, seed-pair set, and `test_accuracy`,
`train_accuracy`, `train_windows`, `test_windows` compared by exact equality
row by row. SPEC §5.10.1 and methodology §8 now say so, along with what it
deliberately does not compare.
- **README, ROADMAP and `docs/methodology.md` brought back in line with what
shipped.** The README documented `measure-leakage` as running "at split seed
42 and train seed 0" — the regression above, described as the design — and
omitted `--force`'s limits and `iqforge train`. methodology gained a
question-to-section guide; it is 1100 lines with no contents.
- **The four places that carry a version agree again.** `docs/release-notes/
v0.5.0.md` had read as a shipped release while `__version__`, `CITATION.cff`
and the newest released CHANGELOG section all said `0.4.0` and no `v0.5.0`
tag existed; it was labelled an unpublished draft for as long as that was
true. With this release it is the release notes for `0.5.0`, and
`__version__`, `CITATION.cff`, the CHANGELOG heading and the built wheel's
METADATA all report the same number.
- **The experiment scripts and their tests no longer carry a hardcoded path.**
`scripts/leakage_real.py`, `scripts/leakage_loraiq.py`, `tests/test_preflight.py`
and `tests/test_measurement.py` all fell back to an absolute path inside one
Expand Down Expand Up @@ -404,7 +431,8 @@ First release.
`info`, `inspect`, `build` and `stats` work without it.
- 16 example recordings, so the whole pipeline runs without hardware.

[Unreleased]: https://github.com/emrefbulut/iqforge/compare/v0.4.0...HEAD
[Unreleased]: https://github.com/emrefbulut/iqforge/compare/v0.5.0...HEAD
[0.5.0]: https://github.com/emrefbulut/iqforge/compare/v0.4.0...v0.5.0
[0.4.0]: https://github.com/emrefbulut/iqforge/compare/v0.3.0...v0.4.0
[0.3.0]: https://github.com/emrefbulut/iqforge/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/emrefbulut/iqforge/compare/v0.1.0...v0.2.0
Expand Down
4 changes: 2 additions & 2 deletions CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ authors:
repository-code: "https://github.com/emrefbulut/iqforge"
url: "https://github.com/emrefbulut/iqforge"
license: MIT
version: "0.4.0"
version: "0.5.0"
# Carries the date the version above is intended to be tagged. It was once left
# absent until the tag existed, on the reasoning that the file should never date
# a release nobody can fetch -- but that put the correct value in a commit AFTER
# the tag, so the tagged tree always shipped a citation with no date, and 0.2.0
# went two days that way before anyone noticed. Pre-filling is the lesser
# problem: if the tag slips, correct this line before pushing it.
date-released: "2026-08-19"
date-released: "2026-09-14"
keywords:
- software-defined-radio
- sigmf
Expand Down
34 changes: 25 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,15 @@

---

> **Status: `0.4.0`.** On PyPI, tagged, CI green. The capture → dataset pipeline
> works end to end and is covered by tests. Interfaces may still change within
> `0.x` — see the [Roadmap](#roadmap) for what is planned and what is
> deliberately out of scope.
> **Status: `0.5.0`.** On PyPI, CI green. The capture → dataset pipeline works
> end to end and is covered by tests. Interfaces may still change within `0.x`
> — see the [Roadmap](#roadmap) for what is planned and what is deliberately
> out of scope.
>
> `0.5.0` corrects a measurement bug: between the Phase 5 migration and this
> release, `measure-leakage` ran a single seed pair instead of fifteen, so any
> leakage figure it produced carried `± 0.0`. See
> [the release notes](docs/release-notes/v0.5.0.md).

> [!IMPORTANT]
> **If you built a dataset with `--labels csv` or `--group-by csv:` over a
Expand Down Expand Up @@ -79,6 +84,7 @@ iqforge inspect examples/bpsk_01.sigmf-meta # look at it, in your terminal
iqforge build examples/ -o dataset/ --balance-by core:freq_lower_edge
iqforge stats dataset/ # what did I just build?
iqforge audit dataset/ # what could be wrong with it?
iqforge train dataset/ # is it actually trainable?
iqforge measure-leakage recordings/ # preflight + paired measurement (if allowed)
```

Expand Down Expand Up @@ -317,12 +323,19 @@ unaltered; `--format json` gives the same content, `did_not_check` included.
categories that eliminated four public datasets and let a fifth through
([methodology §6](docs/methodology.md)), then:
- `REFUSED` exits non-zero
- `WOULD MEASURE` runs the paired cell (recording-level vs window-level) at
split seed 42 and train seed 0
- `WOULD MEASURE` runs the paired cell (recording-level vs window-level) over
**15 seed pairs** — five split seeds by three training seeds, the same grid
every published table used. `--split-seeds` and `--train-seeds` make a
cheaper run a visible choice rather than a silent one, and the pair count is
printed with the result.

`--force` overrides a refusal and keeps the overridden category in the header
so a pasted block cannot be mistaken for a clean run. `--sweep stride` runs the
fixed overlap ladder; there is intentionally no `--sweep snr`.
so a pasted block cannot be mistaken for a clean run. It applies to categories
2–5, which are inferences about what the recordings mean; categories 1 and 6 —
the reader cannot open the files, and `build` would refuse the split — say no
measurement can be constructed at all, and are refused with a reason instead.
`--sweep stride` runs the fixed overlap ladder; there is intentionally no
`--sweep snr`.

## Known limitations

Expand Down Expand Up @@ -370,12 +383,15 @@ See [ROADMAP.md](ROADMAP.md) (Now / Next / Later). Short status:
- [x] Windowing, labelling, recording-level splitting, sharded storage
- [x] `torch.utils.data.Dataset` + baseline classifier
- [x] Packaging (wheel + sdist), GitHub Actions CI
- [x] PyPI releases (`0.1.0`, `0.2.0`, `0.3.0`, `0.4.0`)
- [x] PyPI releases (`0.1.0`, `0.2.0`, `0.3.0`, `0.4.0`, `0.5.0`)
- [x] Leakage measurement (recording-level vs window-level), synthetic and on a
real capture
- [x] Real SigMF verification with public captures
- [x] `iqforge audit` — leakage risk and measurability, without training
- [x] `iqforge measure-leakage` — preflight + paired measurement (`--sweep stride` only)
- [x] `--group-by` — hold related recordings together (`path:`, `csv:`, `collection`)
- [x] Opt-in CUDA for new measurements; CPU stays the default and the published tables stay on it
- [ ] Frequency-aware labelling (the fix for the first known limitation below)
- [ ] Verification with own hardware capture

**Not planned for 0.x** — out of scope rather than pending:
Expand Down
32 changes: 27 additions & 5 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,30 @@ Done recently (keep green):
31.8 dB above unannotated ones. Limits found on real data are in the
README.

- [x] **The leakage measurement, repeated on real captures — the overlap half.**
The stride sweep ran on two public datasets. At zero overlap the
inflation is indistinguishable from zero three times over (+0.2 pp
synthetic, −3.7 pp DASH7, +1.5 pp LoRaIQ), and LoRaIQ at 7/8 overlap
reaches **+9.6 pp ± 2.7 (t = 3.5)** — the first individually significant
real-data result here. Tables in `artifacts/leakage_real_stride_table.md`
and `artifacts/leakage_loraiq_table.md`, reasoning in
[docs/methodology.md](docs/methodology.md) §3. What the intermediate
overlaps do is still not resolved: 15 seed pairs per point settle only
the largest effect.

- [x] **The published grids re-measured through the shipped command.**
`scripts/parity_gate.py` re-ran three cells of each of the four tables
and compared run counts, seed pairs and every row's accuracies and window
counts against the recorded runs. All four passed. See SPEC §5.10.1 for
what that does and does not assert.

Do next, in this order:

1. **Repeat the leakage measurement on a real recording.** The current number is
synthetic BPSK/QPSK; the same curve on a real capture is what turns it from
an illustration into a result worth publishing.
1. **The SNR half of the real-capture measurement.** The overlap half is done
(above); the accuracy-against-SNR curve on a real capture is not. The plan
is locked in [docs/leakage-real-snr.md](docs/leakage-real-snr.md) — seed
count, SNR list and stem fixed before any run, so the result cannot choose
its own stopping rule.
2. **Verification with an own hardware capture.** One device end to end. Public
files validated the reader; they cannot validate against the conventions of a
radio nobody here has run.
Expand Down Expand Up @@ -196,8 +215,11 @@ After Now is done — still reliability-first:
Missing users is a product gap; more features will not close it.
- [ ] Docs site (CLI + Python API reference) when the surface stops thrashing

Versioning: cut `0.3.x` / `0.4.0` when useful, on a schedule if needed — not
“only when hardware is done.”
Versioning: `0.5.0` is the latest release. Cut the next one when useful, on a
schedule if needed — not “only when hardware is done.” Work that has landed but
not shipped sits under `[Unreleased]` in [CHANGELOG.md](CHANGELOG.md), and a
release-notes file under `docs/release-notes/` is marked an unpublished draft
until its tag exists.

---

Expand Down
23 changes: 23 additions & 0 deletions docs/methodology.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,29 @@ came from a run whose output is in [`artifacts/`](../artifacts/); where a
statement has no number behind it, it is described as a design decision rather
than a finding.

## Which section answers which question

This document is long because the failures are the point. Start from the
question you have rather than from the top.

| If you want to know | Read |
|---|---|
| Why splitting at the window level is a problem at all | [§1 The problem](#1-the-problem) |
| How much accuracy a leaky split invents, and at which SNR | [§2 Measurement 1 — inflation against SNR](#2-measurement-1--accuracy-inflation-against-snr) |
| Why overlap is the mechanism, and what happens at zero overlap | [§3 Measurement 2 — inflation against overlap](#3-measurement-2--accuracy-inflation-against-overlap) |
| Why the comparison is paired, and what is held fixed between the two arms | [§4 Experimental design](#4-experimental-design) |
| Whether the reader can be trusted on real captures — byte-level checks | [§5 Validation against real recordings](#5-validation-against-real-recordings) |
| Why four public datasets could not carry the measurement, and what the command's `category N` numbers mean | [§6 What it took to find a dataset](#6-what-it-took-to-find-a-dataset-that-could-carry-the-measurement) |
| What went wrong here and was only caught later | [§7 Silent failures found along the way](#7-silent-failures-found-along-the-way) |
| How claims were checked — mutation testing, controls, re-measuring published tables | [§8 Methods](#8-methods) |
| What none of this establishes | [§9 Limits](#9-limits) |
| The commands that regenerate every number above | [Reproducing](#reproducing) |

Two sections are worth reading even if nothing here applies to your data. **§7**
is the list of mistakes this project made and shipped, including one that
published a wrong result; **§9** is what the numbers do not support, which is
more than they do.

---

## 1. The problem
Expand Down
14 changes: 14 additions & 0 deletions docs/phase5-phase6-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ Purpose: prep-only planning artifact for upcoming Phase 5 migration work and Pha
Date checked: 2026-08-19
Branch checked: `cursor/phase-1-independent-gaps-38eb`

> **Superseded — kept as a record, not as a to-do.** This is a snapshot of what
> was true on the date above. Phase 5 has since been executed and re-verified:
> the migration runs through `iqforge measure-leakage`, and
> `scripts/parity_gate.py` has re-measured three cells of each of the four
> published tables against their recorded runs.
>
> Individual readiness lines below have expired and are deliberately left as
> they were written. The one most likely to mislead: the release-note item says
> no next-version draft exists. `docs/release-notes/v0.5.0.md` has since been
> written and is now the release notes for `0.5.0`.
>
> For current state read [CHANGELOG.md](../CHANGELOG.md) `[Unreleased]` and
> [ROADMAP.md](../ROADMAP.md).

---

## Scope guardrails
Expand Down
14 changes: 7 additions & 7 deletions docs/publishing.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
Pre-release checklist, PyPI setup, GitHub release, and repository metadata.

A PyPI version number is permanent. A bad upload can be yanked, but the number
can never be reused — `0.4.0` would be burned and the fix would have to ship as
`0.4.1`. Everything below exists to keep that from happening.
can never be reused — `0.5.0` would be burned and the fix would have to ship as
`0.5.1`. Everything below exists to keep that from happening.

## Pre-release checklist

Expand All @@ -20,7 +20,7 @@ uv build
Confirm:

- [ ] `__version__` in `src/iqforge/__init__.py` matches the tag you are about to
push (`0.4.0` → `v0.4.0`). This is the only place the version is written;
push (`0.5.0` → `v0.5.0`). This is the only place the version is written;
`pyproject.toml` reads it from there and `tests/test_packaging.py` checks
they agree.
- [ ] `CITATION.cff`: `version` matches, and `date-released` is the date you
Expand Down Expand Up @@ -66,9 +66,9 @@ suite, and a check that the tag matches `__version__` before it builds or
uploads anything.

```bash
git tag -a v0.4.0 -m "v0.4.0"
git push origin v0.4.0
gh release create v0.4.0 --title "v0.4.0" --notes-file docs/release-notes/v0.4.0.md
git tag -a v0.5.0 -m "v0.5.0"
git push origin v0.5.0
gh release create v0.5.0 --title "v0.5.0" --notes-file docs/release-notes/v0.5.0.md
```

Watch the run:
Expand All @@ -81,7 +81,7 @@ If the verify job fails, delete the tag before retrying — a tag that never
published is not a release:

```bash
git tag -d v0.4.0 && git push origin :refs/tags/v0.4.0
git tag -d v0.5.0 && git push origin :refs/tags/v0.5.0
```

### Manual upload
Expand Down
Loading
Loading