A modern AGS4 toolkit for the AGS4 geotechnical data format: validate, read as born-typed data, query, build, fix, diff, certify, and convert ↔ Excel.
One Rust engine, surfaced natively for Python, Node.js, the CLI, DuckDB and the browser — the same rules, the same bytes, the same findings on every one.
Files come back born-typed — a 2DP heading is a float, a DT a datetime, an
ID a string — so polars, SQL and the typed graph see real types, not text.
📖 Documentation · 🌐 Browser validator + data explorer · 📓 Cookbook
Every surface is the same engine — no surface re-implements a rule, and scriptable output is byte-identical across them.
| Surface | Get it | Use it for | Docs |
|---|---|---|---|
| Python | pip install laterite |
polars frames, the typed graph, the python-ags4 drop-in |
guide |
| Node.js | npm install laterite |
apache-arrow tables, server-side validation | guide |
CLI — lat |
Releases, or pip install laterite / npx laterite |
pipelines, CI gates, one-off checks | guide |
| DuckDB | INSTALL laterite_ags4 FROM community; |
SQL straight over .ags files, no conversion step |
guide |
| Browser | open the app | validate + explore without uploading a file anywhere | guide |
import laterite
report = laterite.validate("delivery.ags") # errors + warnings (FYI is opt-in)
report.is_valid
ags = laterite.read("delivery.ags") # born-typed polars frames
ags["LOCA"]["LOCA_GL"][0] # → 12.3, a float — not "12.30"Node.js
import { read, validate, buildAgs4 } from "laterite";
const ags = read("delivery.ags"); // or read(bytes) / read(undefined, { text })
ags.table("LOCA").getChild("LOCA_GL")?.get(0); // → 12.3, born-typed apache-arrow
validate("delivery.ags").toJson(); // byte-identical to `lat validate --json`
buildAgs4(new Map([
["PROJ", [{ PROJ_ID: "P1", PROJ_NAME: "Demo" }]],
["LOCA", [{ LOCA_ID: "BH01", LOCA_GL: 12.3 }]],
])).save("out.ags");Cross-group SQL (ags.sql(...) / ags.at(...)) needs the optional peer @duckdb/node-api.
CLI — lat
lat delivery.ags # shorthand for `lat validate`
lat validate delivery.ags --json # machine-readable findings
lat validate delivery.ags --no-warnings # errors only
lat fix delivery.ags # repair → sibling .fixed.ags (safe fixes)
lat diff old.ags new.ags # KEY-aware revision delta
lat certify delivery.ags # mint delivery.ags.idx if clean
lat rules # the AGS4 rule catalogueExit codes: 0 clean · 1 findings · 3 unreadable · 4 not AGS4 · 5 bad args · 6 schema.
lat --readme prints the full guide.
DuckDB
INSTALL laterite_ags4 FROM community;
LOAD laterite_ags4;
-- every row carries an `_id` / `_parent_id` (UUIDv8 over the AGS key), so
-- parent↔child joins need no hand-written key list
SELECT l.loca_id, s.samp_ref, s.samp_top
FROM read_ags('delivery.ags', 'SAMP') s
JOIN read_ags('delivery.ags', 'LOCA') l ON s._parent_id = l._id;Python — typed graph and the python-ags4 drop-in
# Typed graph: PROJ → LOCA → SAMP → …
from laterite.ags4 import read_typed
for loca in read_typed("delivery.ags").locas:
print(loca.loca_id, loca.loca_gl)
# python-ags4 drop-in — swap the import, keep your code
from laterite import compat as AGS4
tables, headings = AGS4.AGS4_to_dataframe("delivery.ags")The drop-in mirrors python-ags4's library API name for name. Its CLI is deliberately not mirrored — see Parity.
The cookbook shows each task side by side across all five surfaces.
The closest open-source tool, python-ags4,
focuses on Python validation and pandas I/O — and inspired this project. laterite
matches that and adds a cross-surface toolchain on top:
laterite |
python-ags4 |
|
|---|---|---|
| Runs on | Python · Node · CLI · DuckDB · browser | Python |
| Validate — numbered AGS4 rules | ✅ | ✅ |
| Read → typed data | ✅ born-typed | strings; opt-in convert_to_numeric |
| Build / write AGS4 · Excel ↔ AGS4 | ✅ | ✅ |
Repair engine (fix) |
✅ | — |
| SQL across groups · revision diff | ✅ | — |
Validity certificates (.ags.idx) |
✅ | — |
| Transport — compress + encrypt | ✅ | — |
| Typed PROJ → LOCA → SAMP graph | ✅ | — |
Synthetic, spec-valid AGS4 from ags4-forge — the wide scaffold: 123
groups, realistic type mix, zero findings. macOS arm64, hot files, mean of 5
warm runs, against python-ags4 1.2.0. Each cell is laterite's time and its
speedup.
| File (123 groups) | validate |
read → typed | read → strings (compat) |
|---|---|---|---|
| 4.9 MB · 459 BH | 50 ms · 30.0× | 26 ms · 7.2× | 49 ms · 2.9× |
| 24.9 MB · 2,219 BH | 266 ms · 13.9× | 136 ms · 6.0× | 206 ms · 3.5× |
| 102.7 MB · 8,872 BH | 1.1 s · 11.7× | 541 ms · 6.3× | 870 ms · 3.2× |
| 275.5 MB · 22,813 BH | 2.6 s · 13.0× | 1.4 s · 6.4× | 2.2 s · 3.3× |
| 549.7 MB · 45,107 BH | 5.4 s · 12.9× | 2.9 s · 6.0× | 4.6 s · 3.3× |
Reproduce with uv run python tools/bench-vs-python-ags4.py. It generates the
rungs, verifies each against a pinned SHA-256 so a change to the generator can't
move the numbers unnoticed, and prints these tables. node tools/bench-node.mjs
does the same for the Node surface.
Read-typed is the honest comparison for real work: python-ags4 needs
AGS4_to_dataframe + convert_to_numeric on every group to get there, and still
skips dates — laterite.read is born-typed, dates included.
121 / 131 of python-ags4 1.2.0's own test suite passes through
laterite.compat (92 %). The 10 remaining are deliberate non-closures,
documented rule by rule in COMPAT.md and the
parity coverage map. A weekly job compares the two
public surfaces, so a function added upstream can't quietly go missing here.
The CLI is deliberately not mirrored. laterite ships lat instead, with its
own JSON / NDJSON shapes — so a script that shells out to ags4_cli needs
porting rather than an import swap. Your Python code is unaffected.
Every validator rule is written from the published AGS4 specification, not adapted from another library's source — which is what lets laterite ship under a permissive MIT licence.
See CONTRIBUTING.md. Short version: PRs welcome; CI gates
cargo test + pytest + the python-ags4 parity oracle.
