Skip to content

Commit d424b70

Browse files
authored
Merge pull request #5 from peterrrock2/1.0.0
1.0.0
2 parents c92ba2c + 91eda43 commit d424b70

313 files changed

Lines changed: 65822 additions & 8559 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
example/100k_CO_chain.jsonl.xben filter=lfs diff=lfs merge=lfs -text
2+
example/50k_CO_chain.xben filter=lfs diff=lfs merge=lfs -text

.github/workflows/ci.yml

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
name: CI
22

3-
# Lightweight quality gates on every PR: formatting and lints for both languages. The heavier
4-
# gates (full test suite, big-endian emulation) live in full-tests.yml and run on demand — either
5-
# from the Actions tab or via a `/ci-full` / `/ci-endian` PR comment.
3+
# Quality gates on every PR: formatting, lints, and the fast test suites for both languages. The
4+
# heavier gates (the `#[ignore]`-gated stress suite, big-endian emulation, fuzzing) live in
5+
# full-tests.yml and run on demand: from the Actions tab or via a `/ci-full` / `/ci-endian` /
6+
# `/ci-fuzz` PR comment.
67
#
7-
# These mirror `task format` / `task lint`; keep the two in sync.
8+
# These mirror `task format` / `task lint` / `task test-rust-fast` / `task test-python`; keep them in
9+
# sync.
810

911
on:
1012
pull_request:
@@ -45,3 +47,31 @@ jobs:
4547
- name: ruff check
4648
working-directory: ben-py
4749
run: uvx ruff check .
50+
51+
rust-test:
52+
name: rust tests (fast)
53+
runs-on: ubuntu-latest
54+
steps:
55+
- uses: actions/checkout@v4
56+
- uses: dtolnay/rust-toolchain@stable
57+
- uses: Swatinem/rust-cache@v2
58+
- name: cargo test
59+
run: cargo test
60+
61+
python-test:
62+
name: python tests
63+
runs-on: ubuntu-latest
64+
steps:
65+
- uses: actions/checkout@v4
66+
- uses: dtolnay/rust-toolchain@stable
67+
- uses: Swatinem/rust-cache@v2
68+
- uses: astral-sh/setup-uv@v5
69+
- name: sync environment
70+
working-directory: ben-py
71+
run: uv sync --all-groups
72+
- name: build extension
73+
working-directory: ben-py
74+
run: uv run maturin develop
75+
- name: pytest
76+
working-directory: ben-py
77+
run: uv run pytest tests/

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ dev_files
77
demo
88

99
__pycache__
10-
*.so
10+
*.so
11+
/ben-py/docs/code-theme-preview.md

.readthedocs.yaml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,13 @@ build:
99
python:
1010
install:
1111
- method: pip
12-
path: ./pyben
12+
path: ./ben-py
1313
extra_requirements:
1414
- docs
1515

1616
sphinx:
1717
builder: dirhtml
18-
configuration: pyben/docs/conf.py
18+
configuration: ben-py/docs/conf.py
19+
# Notebook execution stays off here (NB_EXECUTION_MODE defaults to "off"), so the
20+
# hosted build renders the committed notebook outputs. CI executes the notebooks.
21+
fail_on_warning: true

CONTEXT.md

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# Context
2+
3+
Orientation for anyone (human or agent) landing in the `binary-ensemble` workspace. It explains what
4+
the project is, how the code is shaped, and the invariants that aren't obvious from any single file.
5+
6+
## What this project is
7+
8+
`binary-ensemble` compresses **ensembles of districting plans**. A redistricting sampler (MCMC
9+
ReCom, SMC, etc.) emits thousands to millions of plans as canonicalized JSONL: one
10+
`{"assignment": [...], "sample": n}` line per draw. Those files are enormous and highly redundant.
11+
This workspace turns them into compact binary formats and provides the tooling to encode, decode,
12+
inspect, relabel, and bundle them.
13+
14+
It is the spiritual successor to [PCompress](https://github.com/mggg/pcompress) and interoperates
15+
with it.
16+
17+
The formats, in increasing capability:
18+
19+
- **`.ben`**: a banner plus bit-packed, run-length-encoded frames. One frame per sample.
20+
- **`.xben`**: a BEN stream's payload wrapped in LZMA2 for maximum size reduction.
21+
- **`.bendl`**: a self-describing _bundle_ holding a header, optional assets (dual graph, metadata,
22+
node-permutation map), an embedded BEN/XBEN assignment stream, and a trailing directory. Feels
23+
like one file; supports interrupted writes and post-finalize appends.
24+
25+
## Domain model (in brief)
26+
27+
`docs/glossary.md` is the **source of truth** for terminology and is worth reading before making
28+
changes. The essentials:
29+
30+
- **Plan**: a partition of dual-graph nodes into districts (the mathematical object).
31+
**Assignment**: its vector encoding, `Vec<u16>` where index _i_ is the district id of node _i_.
32+
One plan has many assignments.
33+
- **Sample**: `(sample_number, assignment)`. **Ensemble**: an ordered stream of samples from one
34+
sampler run; the thing every format wraps.
35+
- **Variant**: `Standard` | `MkvChain` | `TwoDelta`. Fixed per stream by its banner. `Standard`
36+
stores each sample independently; `MkvChain` collapses repeated consecutive samples with a count;
37+
`TwoDelta` delta-encodes single-ReCom-step transitions. Variant fitness depends on the sampler;
38+
see the glossary.
39+
- **Dual graph**: the geographic adjacency graph that gives a node ordering meaning.
40+
Relabeling/reordering operations are defined against it.
41+
42+
The glossary also nails down deliberately-disambiguated words ("header", "extract", "payload",
43+
"canonical\*") and the relabeling taxonomy. Honor those distinctions in code and prose.
44+
45+
## Architecture
46+
47+
A Cargo workspace with two members:
48+
49+
- **`ben/`**: package `binary-ensemble`, library `binary_ensemble`, plus two thin CLI binaries.
50+
- **`ben-py/`**: PyO3 bindings (cdylib) published as the `binary_ensemble` Python package. Depends
51+
on `ben/` by path; the core library has no Python dependency.
52+
53+
### CLI binaries (`ben/src/bin/*.rs`)
54+
55+
Each is a one-line wrapper over `cli::<tool>::run()`. `ben` is a subcommand tree; `bendl` owns the
56+
bundle container role:
57+
58+
| Binary | Role | Does | | ------- | -------- |
59+
------------------------------------------------------------------------- | | `ben` | codec |
60+
encode/decode BEN/XBEN + xz; relabel/canonicalize/reencode; pcompress bridge | | `bendl` | bundle |
61+
create / inspect / extract / append `.bendl` containers |
62+
63+
`ben` subcommands: `encode`, `xencode`, `decode`, `xdecode`, `lookup`, `xz-compress`,
64+
`xz-decompress`, `relabel`, `canonicalize`, `reencode`, `sort-graph`, and `pcompress` (`from-ben` /
65+
`to-ben` / `to-xben`). The relabel pipeline (decode → transform → re-encode) backs
66+
`relabel`/`canonicalize`/`reencode`; the PCompress bridge backs `pcompress`.
67+
68+
### Library modules (`ben/src/`)
69+
70+
- **`codec/`**: the heart. `encode`, `decode`, `frames` (`BenEncodeFrame` / `BenDecodeFrame`), and
71+
`translate` (BEN ↔ ben32 wire form). Frames keep their `raw_bytes` so they can be moved/subsampled
72+
without eager unpacking.
73+
- **`io/`**: streaming `reader` / `writer` over buffered, generic IO, and `bundle` (the `.bendl`
74+
reader/writer/verify/format machinery).
75+
- **`format/`**: on-disk metadata shared across streams (banners and `FormatError`).
76+
- **`ops/`**: the higher-level operations `relabel` (the single `relabel_ben_file` driver
77+
parameterised by `RelabelOptions`) and `extract`.
78+
- **`json/`**: dual-graph utilities (NetworkX-adjacency IO, MLC and RCM node-ordering algorithms)
79+
used by the relabel pipeline.
80+
- **`progress/`**, **`logging/`**, **`util/`**: spinners (`indicatif`), `tracing` setup, and small
81+
shared helpers (RLE).
82+
83+
### Data flow
84+
85+
```mermaid
86+
flowchart LR
87+
JSONL -->|encode| RLE
88+
RLE -->|bit-pack| frame
89+
frame -->|concat| ben["stream(.ben)"]
90+
ben -->|LZMA2 wrap| xben[".xben"]
91+
ben -->|bundle| bendl[".bendl"]
92+
```
93+
94+
Decode reverses this. The relabel subcommands run decode → transform → re-encode in one streaming
95+
pass. The encoding stack has five named layers (bit-packing, RLE, frame, stream, container); see the
96+
glossary's "Encoding Stack" table.
97+
98+
## Invariants and cross-cutting concerns
99+
100+
These hold across the codebase and are easy to violate by accident:
101+
102+
- **Format stability is a contract.** Committed fixtures under `ben/tests/fixtures/v<n>/` must keep
103+
decoding forever within a major version. Never regenerate fixtures in place. See
104+
`docs/format-stability.md`.
105+
- **Frames decode lazily.** Keeping `raw_bytes` without unpacking runs is what makes
106+
subsample-by-skip and random-access reads fast. Don't force eager bit-unpacking on read.
107+
- **Integrity is checked with CRC32C.** Verifying read paths are the default; checksum-skipping
108+
variants are explicitly named with an `_unverified` suffix.
109+
- **Terminology is disciplined.** The glossary governs identifiers and prose; when they disagree,
110+
the glossary wins and the code changes.
111+
- **Streaming, not slurping.** Ensembles are too large to hold in memory.
112+
- **64-bit only** (enforced with `compile_error!` in `lib.rs`).
113+
- **Illegal states are unrepresentable where practical**: e.g. `XBenVariant` cannot hold `TwoDelta`,
114+
so BEN32-only paths reject it at compile time.
115+
116+
## Building and testing
117+
118+
The workspace uses a `Taskfile.yml` (the `task` / `go-task` runner) as the single entry point for
119+
local workflows. CI runs the lightweight gates (formatting + lints) on every PR; the heavy gates
120+
(full test suites, big-endian emulation) run on demand via the Actions tab or a `/ci-full` /
121+
`/ci-endian` PR comment from a collaborator. The wheel-publishing workflow is separate and
122+
tag-triggered.
123+
124+
- `task test`: Rust fast suite + `#[ignore]`-gated slow/stress suite + Python `pytest`.
125+
- `task format`: `cargo fmt --all` + `ruff format`.
126+
- `task lint`: `cargo clippy --workspace --all-targets` (warnings denied) + `ruff check`.
127+
- `task coverage-summary`: combined Rust + Python coverage.
128+
- `task test-endian`: full ben suite on one big-endian and one little-endian target via `cross`
129+
(Docker + QEMU), proving wire-format endianness regardless of the development machine.
130+
`task check-endian` is the no-Docker compile-only tier.
131+
- `task fuzz`: time-boxed coverage-guided fuzzing (cargo-fuzz/libFuzzer, nightly) of every read
132+
surface, seeded from the committed fixtures. `FUZZ_SECONDS` bounds each target (default 60).
133+
134+
Python development uses `uv` + `maturin` (`task ben-py-develop`).
135+
136+
## Document map
137+
138+
- **`docs/glossary.md`**: terminology, the source of truth.
139+
- **`docs/coding-standards.md`**: how code in this repo is written (errors, logging, naming,
140+
testing, modules, PyO3).
141+
- **`docs/bendl-format-spec.md`**: the `.bendl` on-disk binary layout.
142+
- **`docs/format-stability.md`**: the wire-format stability policy.
143+
- **`README.md`**: user-facing CLI and library usage.
144+
- **`docs/*-plan.md`**: active design plans, written before implementation.

0 commit comments

Comments
 (0)