A surface trace can pass through itself without the file saying so. Windcheck detects that hidden defect in one command and has terminally dispositioned all 284 traces in the audited published snapshot, with transverse-clean reference derivatives for all 274 censusable cases.
A tifxyz surface trace can cross itself: non-adjacent parts of the traced sheet pass through one another, so the representation is not an embedded single-sheet surface. Any downstream stage that consumes a dirty trace receives the same inconsistent embedding. Whether removing it improves ink, texturing, merging or tracing has not been measured. Unless the census is run, the surface metadata itself does not disclose it.
| traces indexed, with terminal dispositions | 284 |
| censusable, supplied with transverse-clean references | 274 |
| of which transformed / already clean | 184 / 90 |
| combined area-weighted retention | 0.994566 |
| median area cost, over the 184 transformed | 0.083% |
| transformed references retaining ≥ 99% | 161 of 184 (87.5%) |
| below the 5,000-cell census validity floor, excluded | 10 |
The release inventories 284 tifxyz traces across the 12
Herculaneum sample prefixes that publish any. All 284 carry terminal
dispositions: 274 are censusable and have transverse-clean references;
10 are below the census validity floor and have no cleanliness
verdict. Two further sample prefixes contain segment directories but
publish no tifxyz trace.
Verified with the official tool, not only our own. All 274 reference
surfaces censused with vc_tifxyz_selfcross: 274/274 clean under both
canonical triangulations, none vacuous, across 456,475,808 triangles.
Six of the 274 are transverse-clean but fail the preregistered
99.9%-area core fragmentation gate. Worst is
20251217234605-w2_20251217234605189 at 0.827 retained, min core
R_main 0.000; the other five are auto_grown traces retaining 0.947 to
0.989. Named so the aggregate does not hide them.
The v0.3.2 archive's reconciliation.json records the population counts
and the six core-gate failures; it was generated by the public
bench/reconcile.py, which refuses to build when two source records
disagree about a segment. Official cleanliness and the triangle total
come from the archive's B7-acceptance.json.
results/corpus/summary.json records the historical 278-trace,
12-sample discovery census; the release indices and the archive's
reconciliation.json record the current 284 indexed / 274 censusable /
10 excluded reconciliation.
| you are | you can |
|---|---|
| running a tracing/fitting pipeline | census sampled outputs before they go downstream |
| maintaining an unroll/flatten stage | check a compatible export — demonstrated on Fiesta |
| holding a finished dirty export | run a supported dirty export through the certified transaction |
| publishing a corpus | adopt this as release QA |
| building a repair method | be measured on the same 274 hash-bound cases |
| consuming tifxyz today | download format-compatible clean reference derivatives |
| using volume-cartographer | run the merged validator |
Two boundaries. The merged validator makes checking available; it does not enforce anything automatically. And no independent external method has yet been measured on the benchmark — the infrastructure exists, the third-party result does not.
v0.3.2 packages the 274 censusable cases as an evaluation set: the self-intersection witness for every input (5,241,478 contacts, each naming the participating quads and triangle indices), per-cell masks of every transformation, full provenance, an evaluator, and controls.
tar xzf windcheck-v0.3.2-topology-benchmark.tar.gz
cd windcheck-v0.3.2-topology-benchmark
./tools/quickstart.shThat scores one small published trace as a candidate and shows it rejected because it self-intersects, rebuilds the reference from the shipped mask and shows it scored, then tells you where to substitute your own. Both demonstrations are gated: an unreachable validator, a census over an empty surface, or a reconstruction that is not the packaged reference makes the script fail rather than print a passing demo.
The evaluator reports a vector, never a single number — a candidate that still self-intersects is not scored on cost at all, retention is priced on the input so invented geometry cannot inflate it, and the reference is one admissible answer, not the target.
Targets Open Problems §2 and §3 — sheet switches and mesh topology repair — on Vesuvius Challenge open data.
uv sync
clang++ -O3 -std=c++17 -pthread -o engines/selfcross engines/selfcross.cpp
uv run pytest -qThe triangle-intersection kernel is the only compiled component: a
deterministic floating-point triangle-intersection predicate with scale-aware
tolerances, cross-validated against independent implementations — not exact
arithmetic. No GPU, no volume download and no ML model is involved: the
analysis reads only the tifxyz surface itself.
Fetch a segment to work on:
uv run python -m windcheck.fetch --helpThe tool has two commands. check never modifies anything, and it is the
default posture. Transformation only happens when you ask for it by name.
uv run windcheck check data/scroll5_tifxyz/20251205115859-w094_20251205115859448_flatboi \
--volume 20241024131839 --out out/check/w094This runs the both-diagonal non-adjacent transverse-contact census, prints a
verdict, writes a machine-readable certificate, and emits a viewer-loadable
PointCollection of the crossing sites so you can jump straight to them in
VC3D. On this segment: not clean, 4 transverse contacts under diagonal 0 and
7 under diagonal 1, in 0.2 s.
Two worked examples, one per operator.
# w094 — resolved by bounded displacement
uv run windcheck transform data/scroll5_tifxyz/20251205115859-w094_20251205115859448_flatboi \
--volume 20241024131839 --out out/transform/w094
# 20231005123336 — resolved by certified excision
uv run windcheck transform data/scroll1_tifxyz/20231005123336 \
--out out/transform/20231005123336Both finish in under three seconds on a laptop. Across the 185-trace pinned inventory the median segment takes 36.8 s, the 90th percentile 140 s and the slowest 211 s, against a 600 s policy limit — a complete pass over that pinned inventory is an afternoon, not a cluster job.
transform exits non-zero unless the reloaded output censuses clean, which
makes it usable as a post-export regression test or a CI gate.
uv run windcheck check-pairs --root patches/ --edges edges.csv --out out/pairsA per-surface verdict says nothing about assembly: two individually clean
surfaces can still pass through each other, and any merge that keeps both in
full then contains a self-intersection. check-pairs classifies each candidate
edge as transverse_conflict, no_transverse_conflict (deliberately not
"compatible") or not_testable — undecidable edges are reported, never
dropped. One batched census answers a whole edge list: the 459-pair example in
results/pairs/ runs in ~20 s on a laptop.
docs/CHECK-PAIRS.md has the verdict semantics, the
reproducible example and the limits.
The obvious repair is to nudge the mesh: move vertices until the surface stops touching itself. That works, and it is tried first, because it removes no material. But it is not universal.
Across the corpus, 1,477 crossing events carry certificates of rigid infeasibility with explicit witnesses — no bounded displacement under the stated search and budget separates those contacts. Within the declared two-operator pipeline, excision is the remaining certified route for that population. Excision finds a feasible low-area mask. Minimum area is claimed only where the certificate records proven optimality.
So the declared pipeline is bounded displacement first, certified excision for the residual. 103 of the 185 segments enter excision on a displacement-repaired base, each hash-verified against its repair certificate; 82 enter on the original published mesh.
The excision operator searches for low removed area subject to a feasibility constraint, and records per component whether the area it found is proven optimal. It does not optimise connectivity — which is exactly why the five fragmented pinned outputs are reported rather than hidden.
Nothing here is called clean because the optimiser thought it was finished. A
segment is clean only when the emitted tifxyz has been written to disk, read
back from disk, and re-censused from scratch at 0/0 under both canonical
triangulations.
Each certificate records:
- input and output mesh identity as a content manifest, not a hash of the
coordinate planes alone: one explicit row per file a reader consumes
semantically —
x.tif,y.tif,z.tif,mask.tif/mask.png,meta.json— carrying path, byte size and SHA-256, plus a single directory digest over a frozen serialisation of those rows. A declared file that is absent is written in as an absent row rather than skipped, so "no mask" and "a mask I did not look at" hash differently. The mask is what decides which triangles exist, so it is inside the digest; - grid shape, valid-vertex and retained-quad counts, voxel scale;
- the census engine's own binary and source hashes, and every census parameter;
- per-diagonal contact counts before and after, crossing events, maximum penetration;
- the frozen policy version and hash, and the per-component solve method
(
exact_optimal,lp_improved,greedy_feasible) — with no minimum-area claim made unless every component is proven optimal; - whether the cut base was the original published mesh or a displacement-repaired one, decided by semantic manifest equality against the original mesh and never by comparing paths, since paths change between a downloaded archive and a fresh working directory while the bytes do not;
- removed-area accounting under both denominators, and the identity residual;
- explicit caveats.
Geometric certification and optimisation optimality are deliberately kept apart. A certificate can say "this surface is certified transverse-clean under the stated validator" and "this cut is not proven minimal" at the same time, and often does.
Published corpus certificates and indices do not rely on a repository commit
as provenance. A commit SHA alone is not evidence to someone holding only a
release. So
certificates and out/release/index.json record provenance that is
recomputable from the published files alone —
- the code version and the frozen policy version and hash;
- a source-tree digest: a SHA-256 over the canonical file manifest of
every published file that can change a result (
pyproject.toml,uv.lock,src/windcheck/*.py,engines/*.cpp,engines/*.h,bench/*.py), serialised by the same frozen rule the mesh manifests use; - the engine binary and engine source SHA-256, and the lockfile SHA-256.
Recompute the digest and compare it against the one in the index:
uv run python -m windcheck.provenance
uv run python -m windcheck.provenance --verify <source_tree_digest from index.json>
uv run python -m windcheck.provenance --manifest # every file that feeds it
uv run python -m windcheck.provenance --serialisation # the exact bytes hashedThe serialisation is frozen and stated in src/windcheck/manifest.py: rows
sorted by path, each row path\0size\0sha256 (or path\0absent\0absent),
joined by newlines with a trailing newline, hashed as UTF-8. A reader can
reimplement it in a dozen lines and check every digest in the release without
this repository's history.
The 284-trace inventory has two halves. This section describes the 185 pinned artifacts across five scrolls; the other 99 are the expansion inventory, and the combined figures are at the top of this file.
| corpus | artifacts |
|---|---|
| Scroll 1 | 55 |
| Scroll 5 | 53 |
| PHerc0139 | 38 |
| PHerc1667 | 20 |
| PHerc0814 | 19 |
Per-segment results — input hash to output hash, disposition, certificate path,
area retention under both denominators, fragmentation metrics and runtime — are
in docs/CORPUS.md, with the machine-readable index at
out/release/index.json. The six non-censusable inputs are listed there
explicitly, by name and by reason.
The expansion corpus extends the same treatment to the 99 further
traces the full census found across seven more samples: every trace
dispositioned under the identical frozen policy, every transformed output
independently re-censused clean under both triangulations, and the four
census refusals decline-confirmed. Records in
docs/CORPUS-EXPANSION.md, machine-readable
index at out/release/expansion_index.json, meshes and certificates in
the v0.2.0 release archives. Combined over both inventories — 274
censusable of 284 indexed artifacts — the area-weighted retained fraction
is 0.994566, recomputed from per-segment areas.
Twelve samples in the open-data bucket publish surface traces as tifxyz.
All 284 carry terminal census dispositions; 274 were censusable, and the
releases supply transverse-clean references for all 274.
docs/FULL-CORPUS.md preserves the 278-trace
discovery census and size model;
docs/CORPUS-EXPANSION.md reconciles that
census with the current 284-indexed / 274-censusable release inventory,
including the exact reconciliation of the 278-census and 284-index
denominators.
That page also reports what the corpus says about size: self-intersection probability follows a one-parameter independent-cell model at q = 7.2 × 10⁻⁶ per valid cell, within about ten points across five orders of magnitude. A large surface is likely to fold through itself mostly because it is large.
uv run windcheck transaction candidate.tifxyz --out final.tifxyzOne command stages a supported tifxyz, transforms it only if it is
not already transverse-clean (frozen policy, unchanged), verifies the
result including a from-disk reload census under both triangulations,
and commits atomically -- the input is never touched, and nothing is
promoted unless every transformation and verification gate succeeds.
The authoritative transaction report is committed inside the output
(windcheck_transaction/); --report writes a post-commit copy.
--adapter scrollfiesta preserves that pipeline's sidecars; unknown
sidecars are refused, never guessed. --official-validator additionally
requires volume-cartographer's merged vc_tifxyz_selfcross to certify
the staged result (a bare name resolves through PATH once; the resolved
binary is invoked and hashed). Stable exit codes make it a CI gate.
Run it from a source checkout or the container image: the command
drives the frozen operator scripts under bench/, which every published
certificate cites by path, so they are deliberately not repackaged.
reproduce/clean_checkout_smoke.sh proves the whole path end to end
from a pristine copy of the repository, and docker build -t windcheck .
gives a lockfile-frozen image whose entrypoint is the CLI.
case-studies/scrollfiesta-pherc0139-4x5x5
— a pre-registered, replicated measurement of transverse
self-intersection in ScrollFiesta's tifxyz strip export (July 2026 Best
of Month), with a certified opt-in cleanup transaction verified by
volume-cartographer's own merged validator: 327 clustered events on the
canonical fixture, 273–278 on a held-out disjoint window, removed at
99.44% / 99.69% surface retention.
The same census was run over the whole verified_patches dataset for
PHercParis4, published elsewhere on 30 July 2026: 84,316 patches,
96.7 million valid cells, 40 minutes, no GPU. 84,311 are transverse-clean
and five self-intersect, against 527 expected at the rate published traces
of the same scroll self-intersect when cut to the same size.
All five share one cause, and one cell of boundary erosion clears every one
of them. docs/PATCH-AUDIT.md has the method, the
baseline that makes the number mean something, and the limits. The dataset's
publisher reviewed the finding and removed the five flagged patches.
The single-surface census is now part of volume-cartographer as the
standalone app vc_tifxyz_selfcross
(ScrollPrize/villa#1303,
merged 4 August 2026), so a surface can be checked right after export with
no external tooling —
report-only, both triangulations, a byte-reproducible JSON report, crossing
sites as a VC3D-loadable point collection, and an exit-code gate for
scripts. It reproduces this repository's census exactly on the published
trace it was verified against (contact identities, penetrations and
angles, not just counts). windcheck is also listed among the
community projects on
scrollprize.org.
Before any mesh was transformed, this tool audited the 185-trace pinned
corpus and reported what it found. That record is preserved as filed in
docs/submission.md, with the precomputed per-segment certificates and
viewer overlays in results/. The preserved audit measures published
input meshes; the later corpus and benchmark records distinguish
published inputs from emitted reference derivatives.
docs/REPRODUCE.md gives a one-command spot reproduction of
a single segment, the corpus verification command, expected outputs and
runtimes, and how to re-census an emitted artifact independently of this tool.
Stated plainly, because weaker versions of several of these were tested and died.
- No physical branch identity. A transverse contact is a property of the mesh. Nothing here establishes which physical sheet a surface belongs to, or that a flagged region corresponds to a segmentation error.
- No claim that text improves. Removing self-intersections changes the geometry a downstream renderer sees. Whether that helps ink detection or legibility is untested and is not claimed.
- Nothing is claimed about coplanar or grazing contacts. The census certifies non-adjacent transverse contacts under two canonical triangulations. Coplanar overlap and grazing contact lie outside what is certified.
- Material fragmentation in six references. Five pinned
auto_grownoutputs plus the named expansion reference20251217234605-w2_20251217234605189fail the 99.9%-area core gate. The search targets area, not connectivity, so this is a real property of those outputs and not a reporting choice. - "Represented surface retained" is a geometric measure, not a statement about how much papyrus or text survives.
- The corpus is 284 indexed traces — 185 pinned plus 99 expansion — of which 274 are censusable. It is a dated snapshot of what was published at capture time, not the whole open dataset and not a live mirror, and nothing is claimed outside it.
- Ten traces are below the census floor. They carry terminal dispositions, not clean verdicts, and are not part of the 274.
- The repair establishes geometric cleanliness only — not improved downstream ink, texture, merge or tracing quality.
engines/selfcross.cpp deterministic float triangle-contact predicate
src/windcheck/
cli.py the two user commands
manifest.py mesh content manifests, digests, base-kind rule
provenance.py release provenance a public reader can recompute
pipeline.py check / transform, end to end
tifxyz.py reader and writer, written from the format spec
excise.py certified excision under the frozen policy
repair.py bounded displacement
intrinsic.py quad retention and area accounting
certificate.py certificates and viewer PointCollections
catalog.py, fetch.py open-data catalog, download, SHA-256 manifest
bench/ benchmark and corpus drivers (users never need these)
docs/CORPUS.md per-segment corpus results
docs/REPRODUCE.md reproduction instructions
docs/submission.md the July 2026 audit record, as filed
docs/PATCH-AUDIT.md independent audit of 84,316 published patches
docs/CHECK-PAIRS.md pairwise conflict test for candidate merge edges
docs/FULL-CORPUS.md every published trace, and the size model
docs/HISTORY.md what is production, supporting, retired
results/ precomputed pinned-audit results, all 179
censusable pinned segments
results/patches/ the patch audit summary and its five findings
results/corpus/ the full-corpus census records
tifxyz.py deliberately does not use the upstream vesuvius package: this tool
audits that pipeline, so it must be able to disagree with it.
MIT.