diff --git a/CHANGELOG.md b/CHANGELOG.md index e7fbc4a..6aac466 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`. @@ -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 @@ -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 diff --git a/CITATION.cff b/CITATION.cff index 8fb427d..f185e2f 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -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 diff --git a/README.md b/README.md index ebd8439..aea881b 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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) ``` @@ -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 @@ -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: diff --git a/ROADMAP.md b/ROADMAP.md index a7df76a..1169397 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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. @@ -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. --- diff --git a/docs/methodology.md b/docs/methodology.md index 301db15..bd2646f 100644 --- a/docs/methodology.md +++ b/docs/methodology.md @@ -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 diff --git a/docs/phase5-phase6-checklist.md b/docs/phase5-phase6-checklist.md index dd9e155..7a67a90 100644 --- a/docs/phase5-phase6-checklist.md +++ b/docs/phase5-phase6-checklist.md @@ -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 diff --git a/docs/publishing.md b/docs/publishing.md index 8803c24..52452b7 100644 --- a/docs/publishing.md +++ b/docs/publishing.md @@ -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 @@ -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 @@ -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: @@ -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 diff --git a/docs/release-notes/v0.5.0.md b/docs/release-notes/v0.5.0.md index 42a4bf2..d2af39f 100644 --- a/docs/release-notes/v0.5.0.md +++ b/docs/release-notes/v0.5.0.md @@ -1,55 +1,193 @@ -# iqforge v0.5.0 release notes +## iqforge v0.5.0 -**Status: unpublished draft.** There is no `v0.5.0` git tag and no GitHub -release. These notes describe work currently listed under `[Unreleased]` in -[CHANGELOG.md](../../CHANGELOG.md). Do not treat this file as a shipped version. +`0.4.0` was what happened when `iqforge audit` was pointed at eight public +datasets. This release is what happened when the repository was pointed at +itself: a migration that had already shipped was found to have quietly reduced +every published measurement to a sample size of one, the gate that existed to +catch exactly that had approved it, and three separate reports were making +claims their own output contradicted. -## Highlights +`measure-leakage` also stops being a refuse path that only classifies. It +trains. -- Phase 5 migration completed: leakage experiment scripts now run measured cells - via `iqforge measure-leakage --format json` rather than carrying a second - measurement execution path. -- `iqforge measure-leakage` gained `--balance-by`, enabling the synthetic - published-table path to stay on the command route. -- `--sweep snr` remains intentionally unsupported. Only `--sweep stride` is - supported by command design. +### Correctness: the published grids were being measured once -## Migration details +The Phase 5 migration moved the leakage experiment onto the shipped command. +In doing so it hardcoded `[42]` and `[0]` as the seed lists, cutting every grid +from **15 seed pairs to 1**. -- Updated scripts: - - `scripts/leakage_experiment.py` - - `scripts/leakage_real.py` - - `scripts/leakage_loraiq.py` -- Updated command surface: - - `src/iqforge/cli.py` (`measure-leakage` adds `--balance-by`) -- Added acceptance gate: - - `scripts/parity_gate.py` +A grid reduced that way reproduces the first pair exactly. Nothing that +compared values noticed, the numbers looked right, and the table it produces +reports a standard error of **zero** — the strongest claim the format can make, +printed exactly where the evidence is weakest. It was found by reading the +code, not by reading a result. -## Validation +Three things changed because of it, and the second one matters more than the +first: -The migration is gated by `scripts/parity_gate.py`, which re-measures **three -cells per published table** and compares each against the checked-in artifact: +- `measure-leakage` takes `--split-seeds` and `--train-seeds`, defaulting to + the five split seeds by three training seeds every published table used. They + are flags rather than constants so a cheaper run is a visible choice, and the + pair count is printed with the result. +- `guard_artifact_rows` refuses, **before anything is trained**, to overwrite a + file under `artifacts/` with fewer runs than it already holds. +- `check_environment` was part of the same failure. It returned quietly when a + checkpoint recorded no environment at all — which is the state every + published grid is in, since they predate environment stamping — so the guard + had never once protected the files it was written for. It now refuses that + case instead of waving it through. -- synthetic SNR table (`artifacts/leakage_runs.json`) -- synthetic stride table (`artifacts/leakage_stride_runs.json`) -- DASH7 stride table (`artifacts/leakage_real_stride_runs.json`) -- LoRaIQ stride table (`artifacts/leakage_loraiq_runs.json`) +A result measured once no longer prints an interval either. `inflation=+53.8 pp +± 0.0` now reads `(uncertainty not estimated, n=1)`, the JSON carries +`stderr_pp: null` rather than `0.0`, and the markdown tables leave the `±` off +a single-pair row. -For each cell, three things are compared, and a pass needs all three: +### The gate that approved it -- **the number of runs** -- 30, being 15 seed pairs over two arms -- **the seed pairs themselves** -- the same fifteen, not merely fifteen -- **every run's values**, matched by (strategy, split seed, train seed) rather - than by position: test accuracy, train accuracy, and train/test window counts +`scripts/_phase5_sample_checks.py` was the acceptance gate for that migration. +It read one recording-level row and one window-level row out of each artifact +and compared those. Every assertion it made was true. -The sample-size and seed-pair checks are not decoration. An earlier version of -this gate compared one recording-level row and one window-level row per table. -Every assertion it made was true of a grid that had been cut from 15 seed pairs -to 1, because the surviving pair reproduced exactly. +It had also stopped running altogether at some point, reading payload keys the +JSON no longer emitted, so it would have died on its first cell. -## Notes +It is now `scripts/parity_gate.py` and compares three things per cell, all of +which must hold: how many runs came back, which seed pairs they came from, and +every run's accuracies and window counts — matched by +`(strategy, split seed, train seed)` rather than by position, by exact +equality. Coverage went from one cell per table to three. -- No docs sweep behavior change beyond synchronization with shipped command - behavior and reproducing guidance. -- No push-time behavior changes are expected for existing users of - `iqforge audit` and `iqforge build`. +**All four published tables were re-measured and passed.** Roughly 105 minutes +of training across twelve cells: + +| table | result | +|---|---| +| `leakage_runs.json` | OK (16.3 min) | +| `leakage_stride_runs.json` | OK (11.9 min) | +| `leakage_real_stride_runs.json` | OK (32.4 min) | +| `leakage_loraiq_runs.json` | OK (44.8 min) | + +`PARITY_GATE_PASSED` now has a written meaning (SPEC §5.10.1): it asserts the +**numbers**, not merely that the run used the same configuration. Changing one +`test_accuracy` by 1e-12 fails a cell. What it does not compare — the +environment block, and the fields that select a cell rather than being measured +by it — is documented too, because an undocumented exclusion reads as coverage. + +### Reports that contradicted themselves + +Three, all user-facing, all of them the tool describing a run it was not +making: + +- **`started no. This version of the command stops before training`** was + printed unconditionally, and on a machine with `torch` the measurement it + denied followed four lines later in the same block. The line is now derived: + `yes` when the measurement follows, `no` with the actual obstacle when it + does not. +- **`--force` accepted requests it could not fulfil.** It overrode every refuse + category, including the two where there is nothing to overrule — category 1 + means the reader could not open the files, category 6 means `build` would + refuse the split. Those now stay refused, with the reason printed rather than + the `FORCED` header merely being absent. Categories 2–5 are inferences about + what recordings mean, and remain forcible. +- **The Vega-C pattern was reported as `LEAK`.** Every other LEAK in this tool + means the same material is on both sides of a split. That pattern is the + opposite — a *different* satellite pass in each split — which is distribution + shift, and the two failures move a score in opposite directions. It is now + `RISK`. `measure-leakage` still refuses the set as category 2 and + `audit --strict` still exits non-zero; what changes is that plain `audit` no + longer exits 1 on something not proven to leak. + +### Reproducible by someone who is not the author + +`docs/methodology.md` opens with "everything below is reproducible from this +repository". Two experiment scripts and two test files could only find their +recordings inside one developer's temporary directory, via a fallback path. + +The fallback is what made it invisible: without one, an absent dataset is an +error the first person to run the script sees. With one, the author sees +nothing and everyone else sees a skip that reads like a fact about their +machine. + +Datasets are now named by `IQFORGE_DASH7` and `IQFORGE_LORAIQ`, with no +fallback; every path keeps its command-line flag; and skips say which variable +to set, distinguishing "never configured" from "configured and mistyped". A new +`tests/test_repo_hygiene.py` scans `src/`, `scripts/` and `tests/` for +committed machine paths, because this is the convention with no natural failure +mode — the mistake works perfectly for whoever makes it. + +### sigmf 1.13.0, and a measurement that did not move + +[sigmf-python#159](https://github.com/sigmf/sigmf-python/issues/159), reported +from this project, was fixed upstream and released in sigmf **1.13.0**: +`SigMFFile(metadata=...)` no longer rewrites the caller's dict. The version +tripwire in the test suite fired on the release with the "behaviour CHANGED" +message rather than the routine one, which is the distinction it exists to +make. + +Two details came out of measuring the release rather than reading the pull +request: the accessor shipped as a public `declared_version` property, not the +`__original_version` the PR described; and `get_global_info()` still returns +the library's spec version, so `iqforge info` correctly keeps printing +`1.0.0 (file); 1.2.6 (reader)`. + +Nothing in `iqforge` changed — `load()` reads `core:version` before handing the +dict over, which is right under both behaviours — and the `sigmf>=1.11.1` floor +deliberately stays where it is. + +Separately, and only possible because the parity gate does not compare +environments: `artifacts/leakage_real_stride_runs.json` was produced under +sigmf 1.11.1 and came back **bit-identical** when re-measured under 1.13.0, on +a `torch`, `numpy` and `scipy` that had also moved. One table, three cells, one +direction of upgrade, on CPU — that is the width of the claim. + +### Smaller things + +- **`measure-leakage --balance-by`.** The synthetic published tables balance a + nuisance variable across splits during `build`; without this flag the command + path could not reproduce them, which is why the scripts had kept a second + measurement path at all. +- **`measurement_schema` in the JSON payload**, read by a single parser. A + reader that guesses at a shape it does not recognise produces a plausible + wrong answer — the same reason `read_manifest` already refuses a + `manifest_schema` newer than it understands. Both the single-cell and the + stride-sweep payloads declare it; the sweep payload had been omitting it, and + was therefore indistinguishable from one written before the field existed. +- **`TrainingResult.environment` records `numpy`, `scipy` and `sigmf` + versions** alongside device and torch. Windowing and normalisation are numpy + arithmetic and the spectrogram is scipy, so either can move a result without + a line of this repository changing. + +### Opt-in CUDA + +`iqforge train` and `iqforge measure-leakage` both take `--device`, defaulting +to `cpu`. A GPU being present is not an opt-in, `--device cuda` errors rather +than falling back when torch reports no device, and the resolved device is +recorded in `TrainingResult.environment` so two tables can be told apart. The +sweep scripts and the parity gate do not pass `--device`: published tables stay +on CPU, and a CUDA run is a new labelled measurement rather than a replacement. + +### Documentation + +`docs/methodology.md` §6 now carries a cross-reference table mapping the +command's `category N` numbers to the cases it cites, because the numbering +matches for §6.1–§6.4 and stops there — `category 5` is a refusal while `§6.5` +is LoRaIQ, the one dataset that section did *not* eliminate. A test pins the +code, SPEC and methodology together. + +The document also gains a question-to-section guide; it is over 1100 lines and +had no contents. The README had been documenting the single-seed-pair +regression as the design, and is corrected. + +### Install + +```bash +pip install --upgrade 'iqforge[torch]' +``` + +Python 3.11+. `torch` is optional; `info`, `inspect`, `build`, `stats` and +`audit` all work without it. No dataset format change: datasets built by +`0.1.0`–`0.4.0` are still readable, and `manifest_schema` is unchanged. + +If you have quoted a leakage number produced by a build between the Phase 5 +migration and this release, check its `n`. A table reporting `± 0.0` was +measured once. diff --git a/src/iqforge/__init__.py b/src/iqforge/__init__.py index a4e9711..809c9d1 100644 --- a/src/iqforge/__init__.py +++ b/src/iqforge/__init__.py @@ -4,7 +4,7 @@ from iqforge.io import Annotation, IQForgeError, Recording, load -__version__ = "0.4.0" +__version__ = "0.5.0" #: Message shown when an optional-torch entry point is reached without torch. #: Kept here so the CLI and the library say the same thing; `{what}` names the