Skip to content

Repository files navigation

Windcheck

Self-intersection checks for Herculaneum surface traces

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.

What is here

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.

Where this is useful

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.

The benchmark — measure a different method on the same problem

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.sh

That 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.


Install

uv sync
clang++ -O3 -std=c++17 -pthread -o engines/selfcross engines/selfcross.cpp
uv run pytest -q

The 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 --help

Quick start

The tool has two commands. check never modifies anything, and it is the default posture. Transformation only happens when you ask for it by name.

1. Audit a segment (report-only)

uv run windcheck check data/scroll5_tifxyz/20251205115859-w094_20251205115859448_flatboi \
    --volume 20241024131839 --out out/check/w094

This 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.

2. Transform a segment (explicit)

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/20231005123336

Both 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.

3. Test candidate merge edges (report-only)

uv run windcheck check-pairs --root patches/ --edges edges.csv --out out/pairs

A 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.


Why both operators are necessary — pinned inventory

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.


Certification

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.

Verifying provenance without the private history

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 hashed

The 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 corpus — the pinned half

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.


Reproducing

Every published trace

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.

The topology transaction

uv run windcheck transaction candidate.tifxyz --out final.tifxyz

One 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

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.

Auditing someone else's data

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.

Upstream

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.

The audit record

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.


Limitations

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_grown outputs plus the named expansion reference 20251217234605-w2_20251217234605189 fail 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.

Layout

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.

Licence

MIT.

About

Certified topology auditing and transverse-clean tifxyz outputs for a five-scroll corpus. Report-only by default; finds where a traced surface passes through itself.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages