Loimis — Soil Texture Parser¶
What is the Loimis field?¶
The Loimis (loimis) field encodes the particle-size composition of a soil profile across up to four depth layers, all in a single compact string. It tells you what the soil is physically made of — how much sand, silt, clay, and rock fragment content there is — and how that composition changes with depth.
A simple example:
l40-70/ls₂30/+ls₂
This reads as three layers:
| Layer | Notation | Meaning |
|---|---|---|
| 1 | l40-70 |
Sand (liiv), from 40 to 70 cm depth |
| 2 | ls₂30 |
Sandy clay (liivsavi) with moderate rock content, upper boundary at 30 cm |
| 3 | +ls₂ |
Sandy clay, continuing deeper |
The notation system¶
Fine earth (peenes)¶
The dominant soil material in each layer is described by a texture code:
| Code | Estonian name | International class |
|---|---|---|
l |
liiv | Sand |
pl |
peenliiv | Fine sand |
sl |
saviliiv | Loamy sand / clayey sand |
ls |
liivsavi | Sandy loam / sandy clay loam |
s |
savi | Clay |
tsl |
tolmjas saviliiv | Silt loam |
tls |
tolmjas liivsavi | Silty clay loam |
dk |
liivakivirähk | Sandy grit / sandstone debris |
Rock skeleton (kores)¶
Coarse fragments mixed into the layer are described by a skeleton code:
| Code | Estonian name | Description |
|---|---|---|
r |
rähk | Gravel (2–60 mm fragments) |
v |
veeris | Cobbles (>60 mm rounded fragments) |
k |
kruus | Coarse gravel / limestone fragments |
kb |
killustik | Angular rubble |
p |
paas | Limestone bedrock plate |
lu |
lupjakivi | Calcareous / carbonate material |
ck |
kiltkivirähk | Slate or schist debris |
Organic material (turfs)¶
| Code | Estonian name | Description |
|---|---|---|
th |
toorhuumus | Raw/mor humus horizon (> 10 cm) |
t₁ / t1 |
turvas I aste | Weakly decomposed peat |
t₂ / t2 |
turvas II aste | Moderately decomposed peat |
t₃ / t3 |
turvas III aste | Strongly decomposed peat |
Amplitude subscripts ₁–₅¶
An amplitude subscript immediately after a code indicates abundance or intensity of that component. The scale runs from 1 (weak / minor presence) to 5 (very strong / dominant). For example:
r₁ls— slight gravel content in a sandy clay layerr₃ls— substantial gravel in sandy clayls₂— moderately gravelly sandy clay (peenes with skeleton modifier)
Depth ranges¶
Depth is written in centimetres, either as a single value or a range:
| Notation | Meaning |
|---|---|
l40 |
Sand layer with upper boundary at 40 cm |
l40-70 |
Sand layer from 40 to 70 cm |
+ls₂ |
Sandy clay continues deeper (no explicit lower boundary) |
The + prefix means deeper than the previous layer — no explicit depth given.
Carbonate flag¶
A + immediately before a component code (not a depth marker) means the material
is calcareous (e.g. +ls = calcareous sandy clay). Context distinguishes this
from the depth-continuation +.
Layer separator¶
Layers are separated by /. Up to four layers per polygon.
The 7-step parsing pipeline¶
Because the loimis notation is complex and the raw data contains many encoding errors, parsing proceeds through seven stages:
- Normalise and split — apply error-correction lookups, discard secondary siffer annotations, and split the string into individual layer tokens.
- Bracket fix — validate and remove parenthesised numeric artefacts in each token.
- Grammar test — try each of ten grammar variants (covering all possible orderings of kores, peenes, and turfs components) and produce a canonical normalised string.
- Reconstituate — reassemble the normalised layers into a clean slash-separated string.
- Visitor — apply a structured tree-walker to extract a nested dictionary of all soil constituents, their codes, amplitudes, carbonate flags, and depth ranges.
- Layer depths — read the depth information from the structured dictionary to determine the number of layers and the depth of each layer boundary.
- Texture fractions — look up each constituent code in the texture rules table to derive clay, silt, sand, and rock-fragment percentages, and map to international texture classes.
Output fields¶
Depth and layer summary (11 values)¶
| Field | Type | Description |
|---|---|---|
nlayers |
int | Number of distinct texture layers in this profile (1–4) |
ZMX |
float or NaN | Total encoded profile depth (cm); NaN if nothing was encoded |
Z1 |
float or NaN | Lower boundary of layer 1 (cm); NaN if unencoded |
Z2 |
float or NaN | Lower boundary of layer 2 (cm); NaN if fewer than 2 layers or unencoded |
Z3 |
float or NaN | Lower boundary of layer 3 (cm); NaN if fewer than 3 layers or unencoded |
Z4 |
float or NaN | Lower boundary of layer 4 (cm); NaN if fewer than 4 layers or unencoded |
zmx_model |
float | For modellers, not scientists — modelled total profile depth, always numeric |
z1_model |
float | Modelled lower boundary of layer 1 (cm); 0 if fewer than 1 layer |
z2_model |
float | Modelled lower boundary of layer 2 (cm); 0 if fewer than 2 layers |
z3_model |
float | Modelled lower boundary of layer 3 (cm); 0 if fewer than 3 layers |
z4_model |
float | Modelled lower boundary of layer 4 (cm); 0 if fewer than 4 layers |
Per-layer texture (×4 layers, 32 values + 1 diagnostic)¶
For each layer number n (1 to 4):
| Field | Type | Description |
|---|---|---|
EST_TXTn |
str | Estonian texture code for this layer (e.g. sl, ls) |
EST_CRSn |
str | Coarse fraction type code (e.g. r, k, v); empty if no skeleton |
LXTYPEn |
str | International texture class: SAND, LOAM, CLAY, PEAT, GRAVELS, … |
CLAYn |
float | Clay fraction (0–100 %) |
SILTn |
float | Silt fraction (0–100 %) |
SANDn |
float | Sand fraction (0–100 %) |
ROCKn |
float | Rock / skeleton fragment fraction (0–100 %) |
KARBn |
int | Carbonate flag: 1 if any constituent in the layer is calcareous (+ prefix), 0 otherwise |
Diagnostic fields¶
| Field | Type | Description |
|---|---|---|
parse_ok_l |
bool | True if all layers were parsed successfully. Used in the map viewer error-review style together with parse_ok_s and parse_ok_h. |
parse_info |
str | Parse status: "successful", "empty_loimis", "partial_no_info", "parse_error" |
has_no_info |
bool | True if one or more layers could not be parsed |
loimis_grammar |
dict | Full structured parse result (stored in DB but excluded from map viewer popup) |
loimis_search |
dict | Internal search-parameter snapshot (stored in DB but excluded from map viewer popup) |
NaN values in texture fractions
For gravel- or rock-dominated layers (LXTYPE = "GRAVELS") and for organic
layers (LXTYPE = "PEAT" or "HUMUS"), the clay, silt, and sand fractions
are NaN — organic material has no mineral fine-earth fraction, and METK's
loimis_vastavustabel_v1 leaves these blank (issue #19). The rock fragment content is recorded in
ROCK instead. Guard for this when computing weighted averages.
Z1–Z4/ZMX: NULL means no value (issue #19, 2026-08-31)
Z1–Z4/ZMX are NaN (SQL NULL) both when the profile has fewer
layers (n > nlayers) and when a layer exists but its thickness was
never encoded — nlayers is what tells the two cases apart, not the
value itself.
Since issue #19 a layer that carries no thickness in the source no longer
has one invented for these columns — previously the remainder of a
hardcoded 100 cm was spread equally over such layers, so v⁰₁ls/l
reported 50 cm and 50 cm from a value that states no depth at all.
Z1–Z4 are cumulative lower boundaries, so a boundary is only
meaningful when every layer above it has a known thickness. One unknown
layer therefore makes every deeper boundary NaN as well. ZMX counts
only thicknesses that were actually encoded, and is NaN only when
none were.
zmx_model/z1_model–z4_model: for modellers, not scientists
These reproduce the pre-issue-#19 / EstSoil-EH behaviour: an unencoded
layer's thickness is estimated by spreading the remaining depth over it,
so v⁰₁ls/l gives z1_model=50, z2_model=100 even though Z1/Z2 are
both NaN. Always numeric up to nlayers, 0 beyond it — never NaN.
Do not use these for scientist-facing analysis or texture-fraction
weighting; use Z1–Z4.
Worked example¶
Raw field value: "sl60/r₁l"
| Layer | Token | Meaning |
|---|---|---|
| 1 | sl60 |
Clayey sand (saviliiv), upper boundary at 60 cm |
| 2 | r₁l |
Sand with slight gravel content |
Parsed output (selected fields):
nlayers=2, ZMX=60, Z1=60, Z2=NaN
zmx_model=100, z1_model=60, z2_model=100
EST_TXT1="sl", LXTYPE1="LOAM", CLAY1=15, SILT1=20, SAND1=65, ROCK1=0, KARB1=0
EST_TXT2="l", LXTYPE2="SAND", CLAY2=4, SILT2=6, SAND2=90, ROCK2=5, KARB2=0