Build with Verity
Everything to use Verity in your own work: the calibrated comparison REST API, the native X3P codec for Rust, Python, and R, the data catalog, and the concepts behind the numbers. For the science narrative, see the method and why Verity exists.
Overview
Verity turns a pair of 3-D surface-topography scans into a calibrated, bounded likelihood ratio — an auditable weight of evidence — with a characterized cost (Cllr) and region-level attribution. One method spans striated marks (bullet lands, toolmarks) and impressed marks (cartridge breech faces).
There are three ways in: the hosted web app at verity.codes, the REST API below, and the open-source Rust/Python/R libraries. The decision always stays behind a glass-box statistical firewall — the representation only produces a score; the reportable LR is a monotone, bounded transform of it.
Quickstart
The fastest path is the hosted API. GET /health lists the calibrated mark types; POST /compare returns the report. For bullets, upload allof each bullet’s land scans — aggregating the lands is the strong path.
# Which mark types have a calibrated reference?
curl -s https://api.verity.codes/health
# Compare two bullets — upload every land scan of each.
curl -s -X POST https://api.verity.codes/compare \
-F domain=striated \
-F mark_a=@bulletA_land1.x3p -F mark_a=@bulletA_land2.x3p \
-F mark_b=@bulletB_land1.x3p -F mark_b=@bulletB_land2.x3pNo .x3p files handy? The web app’s Compare toolhas a “Load a sample” button that runs a real comparison.
Core concepts
Likelihood ratio (LR).The reportable answer is not a “match” — it is how much more probable the observed similarity is if the marks share a source than if they do not. Verbal equivalents (“moderately strong support”) map directly from the LR.
Calibration & Cllr. A raw score is not evidence until it is calibrated against a namedreference of known same-source (KM) and different-source (KNM) pairs. Cllr is the cost of the calibrated LRs (lower is better; <1 is informative); the Cllr−Cllrmin gap is the calibration loss. Verity also reports an empirical cap (ELUB-inspired) — the strongest claim the reference data can support.
Congruent Matching Regions (CMR).Verity’s domain-general similarity principle: partition a mark into regions, register each against the other mark, and count the regions that agree on one common geometry. It generalizes Song’s Congruent Matching Cells (CMC) from 2-D cells to regions of any dimension — the set of congruent regions is the attribution map.
| Modality | Region | Transform group | Reduces to |
|---|---|---|---|
| Striated | 1-D profile window | 1-D translation | Chumbley / CMS |
| Impressed | 2-D grid cell | 2-D translation + rotation | = CMC |
| Fractured | 3-D mesh patch | 3-D rigid pose | research |
Full write-up: docs/congruent-matching-regions.md ↗
REST API
Base URL https://api.verity.codes. The API is OpenAPI-described — an interactive reference (Scalar) lives at /scalar (Swagger UI at /docs, ReDoc at /redoc).
/health{ "status": "ok", "engine_version": "0.1.0", "domains": ["impressed", "striated", "toolmark"] }/detectscan), from striation anisotropy. The UI pre-selects it; you confirm.curl -s -X POST https://api.verity.codes/detect -F scan=@mark.x3p
# { "domain": "striated", "coherence": 0.72 }/comparedomain (striated, impressed, or toolmark), plus repeated mark_a / mark_b file fields. For striated bullets, send every land scan of each bullet; for impressed, one breech-face scan per mark; for toolmark, one striated profile scan per mark.Response — the ComparisonReport
{
"domain": "striated",
"likelihood_ratio": 146.0,
"log10_lr": 2.16,
"direction": "same source",
"verbal": "moderately strong support for same source",
"lr_bound_log10": 2.16,
"log10_lr_ci_lo": 1.74, "log10_lr_ci_hi": 2.16, // 95% credible interval (clustered bootstrap)
"lr_ci_method": "bootstrap-clustered",
"score": 0.153,
"score_kind": "bullet-contrast",
"reference": { // diagnostics are the in-sample fit;
"name": "pooled bullet-land reference (Hamby-252 & 173, …)", // source-disjoint Cllr ≈ 0.19
"n_km": 146, "n_knm": 1755, "auc": 0.984, "cllr": 0.193, "cllr_min": 0.168
},
"attribution": [{ "x_frac": 0.0, "w_frac": 0.167, "corr": 0.91, "…": "…" }],
"attribution_b": [{ "x_frac": 0.008,"w_frac": 0.167, "corr": 0.91, "…": "…" }],
"previews": { "a": "[[…]] downsampled height grid", "b": "[[…]]" },
"scope_note": "A calibrated weight of evidence on the named reference; not a
claim about the error rate of examination, which remains unknown."
}likelihood_ratio | The calibrated LR — the reportable weight of evidence (LR < 1 supports different sources). |
log10_lr · lr_bound_log10 | log₁₀ of the LR, and the empirical cap — the strongest claim the reference supports. |
verbal · direction | Verbal equivalent of the LR, and which source hypothesis it favors. |
score · score_kind | The raw similarity score and which scorer produced it. |
reference | The named calibration population and its diagnostics (n_km, n_knm, auc, cllr, cllr_min). |
attribution · attribution_b | The matched regions on Mark A and Mark B — the explanation of the score. |
previews | Downsampled height grids of the rendered surfaces, for overlaying attribution. |
scope_note | An explicit statement of what the number does and does not claim. |
CORS: the API answers the web origin (configurable via VERITY_CORS_ORIGINS). Errors return a JSON detail with a 400 status for bad uploads or an uncalibrated domain.
Reproducibility & the glass-box API
Every /v1/compare carries a recipe— the methods section as JSON: every pipeline step and its parameters, the engine version, the SHA-256 of each input scan, and the reference’s provenance — stamped with a content handle. Same inputs + scorer config + reference + engine produce the same handle, so verifying a published likelihood ratio is a hash-equality check, not an act of trust.
{
"handle": "sha256:632d8a8b9943fb71…", // the content address of this computation
"engine_version": "0.1.0",
"scorer_config_hash": "ea4ddd513b57…",
"inputs": { "mark_a": ["8ab45f56…"], "mark_b": ["ed232b90…"] }, // SHA-256 of each scan
"reference": {
"name": "Fadul cartridge cases", "scorer_config_hash": "ea4ddd513b57…",
"diagnostics": { "n_km": 10, "n_knm": 180, "auc": 0.997, "cllr_min": 0.07 }
},
"result": { "likelihood_ratio": 10.0, "log10_lr": 1.0, "verbal": "moderate support…" },
"steps": [
{ "step": "decode", "code": "verity_x3p.read_x3p" },
{ "step": "preprocess", "code": "verity.preprocess", "params": { "lambda_s": 4e-6, "lambda_c": 250e-6 } },
{ "step": "areal-signature", "code": "verity.areal.areal_signature" },
{ "step": "compare", "code": "verity.cmr.cmr_count" },
{ "step": "calibrate", "code": "verity.decision.lr.ScoreLRModel", "params": { "lr_bound": "auto" } },
{ "step": "uncertainty", "code": "verity.decision.uncertainty.lr_credible_interval" }
]
}Every step is addressable. Upload a scan for a content handle, then chain the pipeline — each intermediate is fetchable and links to its inputs, a content-addressed graph from scan to score.
/v1/artifacts/v1/steps/{preprocess,signature,areal-signature,align,features}/v1/artifacts/{handle}/data for the raw .npy (ETag = handle) or /preview for a downsampled view.The calibration firewall. A likelihood ratio is valid only when the score was produced under the same scorer config as the reference. /v1/steps/calibrate — and a scorer_config override on /v1/compare — refuse to emit a calibrated LR when the config hashes disagree, returning the raw score with calibrated: false rather than a mis-scaled number.
/v1/scorer-config/v1/references/v1/steps/calibrateClients. Thin Python and R clients wrap the API; reproduce() re-runs a comparison and checks the handle matches.
from verity_client import VerityClient # clients/python
v = VerityClient("https://api.verity.codes")
r = v.compare("impressed", "breech_a.x3p", "breech_b.x3p")
print(r["likelihood_ratio"], r["handle"]) # the content address
# verify a published LR — a one-line hash-equality check
assert v.reproduce("impressed", "breech_a.x3p", "breech_b.x3p",
expect_handle=r["handle"])Clients: clients/ ↗
X3P codec
Verity reads and writes X3P (ISO 25178-72) — the open container for 3-D surface topography — through a single native Rust core, verity-x3p, with thin per-language bindings. A file written from any binding reads back bit-identically in every other. Heights are an (ny, nx) matrix of f64 (invalid points are NaN) with a parallel validity mask and the X3P axis/provenance metadata.
use verity_x3p::{read_x3p, write_x3p, WriteOptions};
let surface = read_x3p("scan.x3p")?; // verifies the stored MD5
println!("{} x {} points", surface.nx(), surface.ny());
write_x3p(&surface, "copy.x3p", &WriteOptions::default())?;Install: Python builds with maturin develop (PyPI wheels to follow); R via R CMD INSTALL bindings/r/verityx3p. Both require a Rust toolchain today. TypeScript / Swift / Java bindings are planned.
Data catalog
verity-catalogis a normalized catalog + content-addressed (SHA-256) store + manifest-driven ingestion for forensic X3P scans harvested from NIST NBTRD (U.S. public domain), CSAFE-ISU’s cartridge-case scans on Figshare (CC BY 4.0), and the tmaRks toolmark set (MIT). It is local-first (SQLite + a blob directory, no external services) and scales to Postgres + object storage by setting VERITY_CATALOG_* env vars — no code change. Same-source (KM/KNM) labels fall out of the Study → Firearm → Bullet → Land → Scan hierarchy, and every scan is validated with verity-x3p on ingest.
uv run verity-catalog init-db
uv run verity-catalog ingest hamby252-barrel1-sample --limit 2 # ~2.9 MB scans from NBTRD
uv run verity-catalog infoArchitecture
Verity is a polyglot monorepo: one Rust codec core, thin language bindings, and the Python science + service stack on top.
| Package | Lang | Role |
|---|---|---|
| crates/verity-x3p | Rust | Native X3P reader/writer — the format’s single source of truth. |
| bindings/python · bindings/r | PyO3 · extendr | Thin wrappers over the core for bit-identical I/O. |
| services/engine | Python | Metrology preprocessing, registration, CMR, the calibrated-LR decision layer. |
| services/api | FastAPI | The comparison HTTP API serving the ComparisonReport. |
| services/catalog | Python | Catalog + content-addressed store + ingestion. |
| services/web | Next.js | This site and the interactive comparison UI. |
The firewall. A learned or hand-engineered representation produces a score; a transparent, empirically-capped calibration turns that score into the reportable LR. The report is interpretable regardless of how the score was computed — the defense against the black box.
Reference
- Interactive API reference (Scalar)
- Source on GitHub
- How the method works
- Why Verity exists
- CMR write-up
- Try a comparison
Licensed MIT / Apache-2.0. Verity reports a calibrated weight of evidence; it never makes the decision, and makes no claim about the error rate of forensic examination, which remains unknown.