Skip to content

Born-typed reads

Every AGS group ships a TYPE row — 2DP, ID, DT, … — declaring what each column is. laterite reads that row and hands each column the matching polars dtype, so the frame is typed at the door. No .cast(), no pd.to_numeric, no guessing.

import laterite

# Read an AGS4 file. `read` takes a path, or text=… / data=… (the three doors).
ags = laterite.read("examples/sample_site.ags")

# A group comes back as a born-typed polars frame — the dtype *is* the TYPE row.
loca = ags["LOCA"]
print(loca.select("LOCA_ID", "LOCA_NATE", "LOCA_GL").head(2))
print({h: str(loca[h].dtype) for h in ("LOCA_ID", "LOCA_NATE", "LOCA_GL")})

assert str(loca["LOCA_GL"].dtype) == "Float64"  # 2DP  → Float64 (no manual cast)
assert str(loca["LOCA_NATE"].dtype) == "Float64"  # 2DP → Float64
assert str(loca["LOCA_ID"].dtype) == "String"  # ID → String
shape: (2, 3)
┌─────────┬───────────┬─────────┐
│ LOCA_ID ┆ LOCA_NATE ┆ LOCA_GL │
│ ---     ┆ ---       ┆ ---     │
│ str     ┆ f64       ┆ f64     │
╞═════════╪═══════════╪═════════╡
│ BH01    ┆ 451105.75 ┆ 23.68   │
│ BH02    ┆ 451235.21 ┆ 32.49   │
└─────────┴───────────┴─────────┘
{'LOCA_ID': 'String', 'LOCA_NATE': 'Float64', 'LOCA_GL': 'Float64'}

The 2DP columns (LOCA_NATE, LOCA_GL) come back as Float64; the ID column (LOCA_ID) stays String. The mapping follows the TYPE row — a DT heading like TRAN_DATE reads as Datetime(time_unit='us') (LOCA has no DT column, so it doesn't appear here).

Why it matters

A born-typed frame means arithmetic, sorting, and joins just work. Add a depth to a ground level, sort boreholes by easting, join SAMP to LOCA on LOCA_ID — no per-column casting, and no silent string-vs-number bugs from AGS data arriving as text.

← Back to Read