Orientation for anyone (human or agent) landing in the binary-ensemble workspace. It explains what
the project is, how the code is shaped, and the invariants that aren't obvious from any single file.
binary-ensemble compresses ensembles of districting plans. A redistricting sampler (MCMC
ReCom, SMC, etc.) emits thousands to millions of plans as canonicalized JSONL: one
{"assignment": [...], "sample": n} line per draw. Those files are enormous and highly redundant.
This workspace turns them into compact binary formats and provides the tooling to encode, decode,
inspect, relabel, and bundle them.
It is the spiritual successor to PCompress and interoperates with it.
The formats, in increasing capability:
.ben: a banner plus bit-packed, run-length-encoded frames. One frame per sample..xben: a BEN stream's payload wrapped in LZMA2 for maximum size reduction..bendl: a self-describing bundle holding a header, optional assets (dual graph, metadata, node-permutation map), an embedded BEN/XBEN assignment stream, and a trailing directory. Feels like one file; supports interrupted writes and post-finalize appends.
docs/glossary.md is the source of truth for terminology and is worth reading before making
changes. The essentials:
- Plan: a partition of dual-graph nodes into districts (the mathematical object).
Assignment: its vector encoding,
Vec<u16>where index i is the district id of node i. One plan has many assignments. - Sample:
(sample_index, assignment), with a zero-based index. Ensemble: an ordered stream of samples from one sampler run; the thing every format wraps. - Variant:
Standard|MkvChain|TwoDelta. Fixed per stream by its banner.Standardstores each sample independently;MkvChaincollapses repeated consecutive samples with a count;TwoDeltadelta-encodes single-ReCom-step transitions. Variant fitness depends on the sampler; see the glossary. - Dual graph: the geographic adjacency graph that gives a node ordering meaning. Relabeling/reordering operations are defined against it.
The glossary also nails down deliberately-disambiguated words ("header", "extract", "payload", "canonical*") and the relabeling taxonomy. Honor those distinctions in code and prose.
A Cargo workspace with two members:
ben/: packagebinary-ensemble, librarybinary_ensemble, plus two thin CLI binaries.ben-py/: PyO3 bindings (cdylib) published as thebinary_ensemblePython package. Depends onben/by path; the core library has no Python dependency.
Each is a one-line wrapper over cli::<tool>::run(). ben is a subcommand tree; bendl owns the
bundle container role:
| Binary | Role | Does | | ------- | -------- |
------------------------------------------------------------------------- | | ben | codec |
encode/decode BEN/XBEN + xz; relabel/canonicalize/reencode; pcompress bridge | | bendl | bundle |
create / inspect / extract / append .bendl containers |
ben subcommands: encode, xencode, decode, xdecode, lookup, xz-compress,
xz-decompress, relabel, canonicalize, reencode, sort-graph, and pcompress (from-ben /
to-ben / to-xben). The relabel pipeline (decode → transform → re-encode) backs
relabel/canonicalize/reencode; the PCompress bridge backs pcompress.
codec/: the heart.encode,decode,frames(BenEncodeFrame/BenDecodeFrame), andtranslate(BEN ↔ ben32 wire form). Frames keep theirraw_bytesso they can be moved/subsampled without eager unpacking.io/: streamingreader/writerover buffered, generic IO, andbundle(the.bendlreader/writer/verify/format machinery).format/: on-disk metadata shared across streams (banners andFormatError).ops/: the higher-level operationsrelabel(the singlerelabel_ben_filedriver parameterised byRelabelOptions) andextract.json/: dual-graph utilities (NetworkX-adjacency IO, MLC and RCM node-ordering algorithms) used by the relabel pipeline.progress/,logging/,util/: spinners (indicatif),tracingsetup, and small shared helpers (RLE).
flowchart LR
JSONL -->|encode| RLE
RLE -->|bit-pack| frame
frame -->|concat| ben["stream(.ben)"]
ben -->|LZMA2 wrap| xben[".xben"]
ben -->|bundle| bendl[".bendl"]
Decode reverses this. The relabel subcommands run decode → transform → re-encode in one streaming pass. The encoding stack has five named layers (bit-packing, RLE, frame, stream, container); see the glossary's "Encoding Stack" table.
These hold across the codebase and are easy to violate by accident:
- Format stability is a contract. Committed fixtures under
ben/tests/fixtures/v<n>/must keep decoding forever within a major version. Never regenerate fixtures in place. Seedocs/format-stability.md. - Frames decode lazily. Keeping
raw_byteswithout unpacking runs is what makes subsample-by-skip and random-access reads fast. Don't force eager bit-unpacking on read. - Integrity is checked with CRC32C. Verifying read paths are the default; checksum-skipping
variants are explicitly named with an
_unverifiedsuffix. - Terminology is disciplined. The glossary governs identifiers and prose; when they disagree, the glossary wins and the code changes.
- Streaming, not slurping. Ensembles are too large to hold in memory.
- 64-bit only (enforced with
compile_error!inlib.rs). - Illegal states are unrepresentable where practical: e.g.
XBenVariantcannot holdTwoDelta, so BEN32-only paths reject it at compile time.
The workspace uses a Taskfile.yml (the task / go-task runner) as the single entry point for
local workflows. CI runs the lightweight gates (formatting + lints) on every PR; the heavy gates
(full test suites, big-endian emulation) run on demand via the Actions tab or a /ci-full /
/ci-endian PR comment from a collaborator. The wheel-publishing workflow is separate and
tag-triggered.
task test: Rust fast suite +#[ignore]-gated slow/stress suite + Pythonpytest.task format:cargo fmt --all+ruff format.task lint:cargo clippy --workspace --all-targets(warnings denied) +ruff check.task coverage-summary: combined Rust + Python coverage.task test-endian: full ben suite on one big-endian and one little-endian target viacross(Docker + QEMU), proving wire-format endianness regardless of the development machine.task check-endianis the no-Docker compile-only tier.task fuzz: time-boxed coverage-guided fuzzing (cargo-fuzz/libFuzzer, nightly) of every read surface, seeded from the committed fixtures.FUZZ_SECONDSbounds each target (default 60).
Python development uses uv + maturin (task ben-py-develop).
docs/glossary.md: terminology, the source of truth.docs/coding-standards.md: how code in this repo is written (errors, logging, naming, testing, modules, PyO3).docs/bendl-format-spec.md: the.bendlon-disk binary layout.docs/format-stability.md: the wire-format stability policy.README.md: user-facing CLI and library usage.docs/*-plan.md: active design plans, written before implementation.