# Author: LKouadio <etanoyau@gmail.com>
# License: LGPL-3.0
"""SEG-EDI schema (keywords, options, ordering).
This module centralizes *tags* (block keywords), *families*,
allowed *options*, and the top-level BNF skeleton for SEG-EDI
files. It is intentionally light-weight and import-safe: there
is no I/O here — just constants and a couple of helpers. Use
it from readers/writers (e.g. :mod:`pycsamt.seg.edi`).
Notes
-----
- Keywords and option names are **UPPERCASE** in the standard.
We keep tags exactly as they appear (e.g. ``">ZXXR"``).
- Sets are :class:`frozenset` to make them hashable and safe
for reuse in dataclasses and default arguments.
- The schema aggregates both MT and EMAP blocks, including
filtered EMAP forms (``F*``) and auxiliary diagnostics.
- The *option* tables here are **non-exhaustive** for some of
the highly system-dependent blocks (``>TSERIES``, spectra).
They list the stable core options we validate against; the
reader may accept extras as opaque ``KEY=VALUE`` pairs.
References
----------
SEG MT/EMAP EDI v1.0 (1991) — consolidated into
:mod:`pycsamt.seg.schema` as constants for reuse.
"""
from __future__ import annotations
from collections.abc import Mapping
# ---------------------------------------------------------------------------
# Top-level file structure (BNF skeleton)
# ---------------------------------------------------------------------------
BNF_EDI_SKELETON: str = (
"<edi_file> ::=\n"
" <head_block>\n"
" <info_block> <info_text>\n"
" <def_meas_section>\n"
" { <tseries_section> | <spectra_section> |\n"
" <mt_section> | <emap_section> | <other_section> }\n"
" <end_block>\n"
)
# ---------------------------------------------------------------------------
# Canonical section openers / structural tags
# ---------------------------------------------------------------------------
HEAD_TAGS: frozenset[str] = frozenset(
{
">HEAD",
">INFO",
">FREQ",
">ZROT",
">RHOROT",
">END",
}
)
SECTION_OPENERS: frozenset[str] = frozenset(
{
">=DEFINEMEAS",
">=TSERIESSECT",
">=SPECTRASECT",
">=MTSECT",
">=EMAPSECT",
">=OTHERSECT",
}
)
# Comments ("pseudo-tag" used by the format). Kept for completeness.
COMMENT_TAG: str = ">!"
MEAS_DEF_BLOCKS: frozenset[str] = frozenset({">EMEAS", ">HMEAS"})
TSERIES_BLOCKS: frozenset[str] = frozenset({">TSERIES"})
SPECTRA_BLOCKS: frozenset[str] = frozenset({">SPECTRA"})
# ---------------------------------------------------------------------------
# Option sets for stable blocks (used by validators)
# ---------------------------------------------------------------------------
HEAD_ALLOWED: frozenset[str] = frozenset(
{
"DATAID",
"ACQBY",
"FILEBY",
"ACQDATE",
"ENDDATE",
"FILEDATE",
"COUNTRY",
"STATE",
"COUNTY",
"PROSPECT",
"LOC",
"LAT",
"LONG",
"ELEV",
"UNITS",
"STDVERS",
"PROGVERS",
"PROGDATE",
"MAXSECT",
"BINDATA",
"EMPTY",
# Some writers also include non-standard free fields; we ignore.
}
)
HEAD_REQUIRED: frozenset[str] = frozenset(
{
"DATAID",
"ACQBY",
"FILEBY",
"ACQDATE",
"FILEDATE",
"STDVERS",
"PROGVERS",
"PROGDATE",
}
)
INFO_ALLOWED: frozenset[str] = frozenset({"MAXINFO"})
FREQ_ALLOWED: frozenset[str] = frozenset({"NFREQ", "ORDER", "CHKSUM"})
FREQ_REQUIRED: frozenset[str] = frozenset({"NFREQ"})
FREQ_ENUMS: Mapping[str, frozenset[str]] = {
"ORDER": frozenset({"INC", "DEC"}),
}
ZROT_ALLOWED: frozenset[str] = frozenset({"NFREQ", "CHKSUM"})
ZROT_REQUIRED: frozenset[str] = frozenset({}) # dataset length must==NFREQ
RHOROT_ALLOWED: frozenset[str] = frozenset({"NFREQ", "CHKSUM"})
RHOROT_REQUIRED: frozenset[str] = frozenset({})
DEFINEMEAS_ALLOWED: frozenset[str] = frozenset(
{
"MAXCHAN",
"MAXRUN",
"MAXMEAS",
"UNITS",
"REFTYPE",
"REFLOC",
"REFLAT",
"REFLONG",
"REFELEV",
}
)
# Required per standard: for measured data REFLAT/REFLONG/REFELEV;
# we treat them as conditionally required in validators.
DEFINEMEAS_REQUIRED: frozenset[str] = frozenset({})
EMEAS_ALLOWED: frozenset[str] = frozenset(
{
"ID",
"CHTYPE",
"X",
"Y",
"Z",
"X2",
"Y2",
"Z2",
"ACQCHAN",
"FILTER",
"SENSOR",
"GAIN",
"MEASDATE",
}
)
EMEAS_REQUIRED_CORE: frozenset[str] = frozenset(
{
"ID",
"CHTYPE",
"X",
"Y",
"X2",
"Y2",
}
)
EMEAS_ENUMS: Mapping[str, frozenset[str]] = {
"CHTYPE": frozenset({"EX", "EY"}),
}
HMEAS_ALLOWED: frozenset[str] = frozenset(
{
"ID",
"CHTYPE",
"X",
"Y",
"Z",
"AZM",
"DIP",
"ACQCHAN",
"FILTER",
"SENSOR",
"GAIN",
"MEASDATE",
}
)
HMEAS_REQUIRED_CORE: frozenset[str] = frozenset(
{
"ID",
"CHTYPE",
"X",
"Y",
"AZM",
}
)
HMEAS_ENUMS: Mapping[str, frozenset[str]] = {
"CHTYPE": frozenset({"HX", "HY", "HZ"}),
}
TSERIESSECT_ALLOWED: frozenset[str] = frozenset(
{
"SECTID",
"NCHAN",
"MAXBLKS",
"CHKSUM",
}
)
TSERIES_ALLOWED: frozenset[str] = frozenset(
{
# Core, kept minimal — systems add many more (e.g. NC, DT,...)
"SECTID",
"NPTS",
"SR",
"CHKSUM",
}
)
SPECTRASECT_ALLOWED: frozenset[str] = frozenset(
{
"SECTID",
"NC",
"MAXBLKS",
"CHKSUM",
}
)
SPECTRA_ALLOWED: frozenset[str] = frozenset(
{
# Intentional minimal core; writers often include BW/NAV/DT, etc.
"FREQ",
"BWIDTH",
"AVGF",
"AVGT",
"NC",
"CHKSUM",
}
)
MTSECT_ALLOWED: frozenset[str] = frozenset(
{
"SECTID",
"NFREQ",
"MAXBLKS",
"HX",
"HY",
"HZ",
"EX",
"EY",
"RX",
"RY",
}
)
EMAPSECT_ALLOWED: frozenset[str] = frozenset(
{
"SECTID",
"NFREQ",
"MAXBLKS",
"NDIPOLE",
"TYPE",
"HX",
"HY",
"RX",
"RY",
"CHKSUM",
}
)
OTHERSECT_ALLOWED: frozenset[str] = frozenset({"SECTID", "MAXBLKS"})
# Convenience registry (options we care about by tag)
BLOCK_OPTIONS: Mapping[str, Mapping[str, frozenset[str]]] = {
">HEAD": {"allowed": HEAD_ALLOWED, "required": HEAD_REQUIRED},
">INFO": {"allowed": INFO_ALLOWED, "required": frozenset()},
">FREQ": {"allowed": FREQ_ALLOWED, "required": FREQ_REQUIRED},
">ZROT": {"allowed": ZROT_ALLOWED, "required": ZROT_REQUIRED},
">RHOROT": {"allowed": RHOROT_ALLOWED, "required": RHOROT_REQUIRED},
">=DEFINEMEAS": {
"allowed": DEFINEMEAS_ALLOWED,
"required": DEFINEMEAS_REQUIRED,
},
">EMEAS": {"allowed": EMEAS_ALLOWED, "required": EMEAS_REQUIRED_CORE},
">HMEAS": {"allowed": HMEAS_ALLOWED, "required": HMEAS_REQUIRED_CORE},
">=TSERIESSECT": {
"allowed": TSERIESSECT_ALLOWED,
"required": frozenset(),
},
">TSERIES": {"allowed": TSERIES_ALLOWED, "required": frozenset()},
">=SPECTRASECT": {
"allowed": SPECTRASECT_ALLOWED,
"required": frozenset(),
},
">SPECTRA": {"allowed": SPECTRA_ALLOWED, "required": frozenset()},
">=MTSECT": {"allowed": MTSECT_ALLOWED, "required": frozenset()},
">=EMAPSECT": {"allowed": EMAPSECT_ALLOWED, "required": frozenset()},
">=OTHERSECT": {"allowed": OTHERSECT_ALLOWED, "required": frozenset()},
}
# ---------------------------------------------------------------------------
# Data block keywords by family
# ---------------------------------------------------------------------------
# Impedance (Z) blocks — real/imag, variance, covariance
Z_REAL_IMAG: frozenset[str] = frozenset(
{
">ZXXR",
">ZXXI",
">ZXYR",
">ZXYI",
">ZYXR",
">ZYXI",
">ZYYR",
">ZYYI",
}
)
Z_VAR_COV: frozenset[str] = frozenset(
{
">ZXXR.VAR",
">ZXXI.VAR",
">ZXX.VAR",
">ZXX.COV",
">ZXYR.VAR",
">ZXYI.VAR",
">ZXY.VAR",
">ZXY.COV",
">ZYXR.VAR",
">ZYXI.VAR",
">ZYX.VAR",
">ZYX.COV",
">ZYYR.VAR",
">ZYYI.VAR",
">ZYY.VAR",
">ZYY.COV",
}
)
# EMAP-filtered impedances
FZ_REAL_IMAG: frozenset[str] = frozenset(
{
">FZXXR",
">FZXXI",
">FZXYR",
">FZXYI",
}
)
# Apparent resistivity / phase
RHO_BLOCKS: frozenset[str] = frozenset(
{
">RHOXX",
">RHOXY",
">RHOYX",
">RHOYY",
">FRHOXX",
">FRHOXY", # filtered (EMAP)
}
)
RHO_STATS: frozenset[str] = frozenset(
{
">RHOXX.VAR",
">RHOXX.ERR",
">RHOXX.FIT",
">RHOXY.VAR",
">RHOXY.ERR",
">RHOXY.FIT",
">RHOYX.VAR",
">RHOYX.ERR",
">RHOYX.FIT",
">RHOYY.VAR",
">RHOYY.ERR",
">RHOYY.FIT",
">FRHOXX.VAR",
">FRHOXX.ERR",
">FRHOXX.FIT",
">FRHOXY.VAR",
">FRHOXY.ERR",
">FRHOXY.FIT",
}
)
PHS_BLOCKS: frozenset[str] = frozenset(
{
">PHSXX",
">PHSXY",
">PHSYX",
">PHSYY",
">FPHSXX",
">FPHSXY",
}
)
PHS_STATS: frozenset[str] = frozenset(
{
">PHSXX.VAR",
">PHSXX.ERR",
">PHSXX.FIT",
">PHSXY.VAR",
">PHSXY.ERR",
">PHSXY.FIT",
">PHSYX.VAR",
">PHSYX.ERR",
">PHSYX.FIT",
">PHSYY.VAR",
">PHSYY.ERR",
">PHSYY.FIT",
">FPHSXX.VAR",
">FPHSXX.ERR",
">FPHSXX.FIT",
">FPHSXY.VAR",
">FPHSXY.ERR",
">FPHSXY.FIT",
}
)
# Tipper and tipper-related experiment outputs
TIPPER_BLOCKS: frozenset[str] = frozenset(
{
">TIPMAG",
">TIPPHS",
}
)
TIPPER_AUX: frozenset[str] = frozenset(
{
">TIPMAG.ERR",
">TIPMAG.FIT",
">TIPPHS.FIT",
">TXR.EXP",
">TXI.EXP",
">TXVAR.EXP",
">TYR.EXP",
">TYI.EXP",
">TYVAR.EXP",
}
)
# Coherency, signal, and noise diagnostics
COH_BLOCKS: frozenset[str] = frozenset(
{
">COH",
">EPREDCOH",
">HPREDCOH",
">SIGAMP",
">SIGNOISE",
}
)
# Strike / skew / ellipticity
GEOM_BLOCKS: frozenset[str] = frozenset(
{
">ZSTRIKE",
">ZSKEW",
">ZELLIP",
">TSTRIKE",
">TSKEW",
">TELLIP",
}
)
# Spatial filters (EMAP)
SPATIAL_FILTER_BLOCKS: frozenset[str] = frozenset(
{
">FILWIDTH",
">FILANGLE",
">EQUIVLEN",
}
)
# 1D continuous inversion summaries
INV1D_BLOCKS: frozenset[str] = frozenset(
{
">RES1DXX",
">DEP1DXX",
">RES1DXY",
">DEP1DXY",
">RES1DYX",
">DEP1DYX",
">RES1DYY",
">DEP1DYY",
">FRES1DXX",
">FDEP1DXX",
">FRES1DXY",
">FDEP1DXY",
}
)
# Union of all known data keywords
DATA_KEYWORDS: frozenset[str] = frozenset().union(
Z_REAL_IMAG,
Z_VAR_COV,
FZ_REAL_IMAG,
RHO_BLOCKS,
RHO_STATS,
PHS_BLOCKS,
PHS_STATS,
TIPPER_BLOCKS,
TIPPER_AUX,
COH_BLOCKS,
GEOM_BLOCKS,
SPATIAL_FILTER_BLOCKS,
INV1D_BLOCKS,
)
# Everything we recognize as a tag/keyword in EDI files
ALL_KEYWORDS: frozenset[str] = frozenset().union(
HEAD_TAGS,
SECTION_OPENERS,
MEAS_DEF_BLOCKS,
TSERIES_BLOCKS,
SPECTRA_BLOCKS,
DATA_KEYWORDS,
)
# Families (category names used by parsers/writers)
FAMILY_BY_TAG: dict[str, str] = {}
for t in Z_REAL_IMAG | Z_VAR_COV | FZ_REAL_IMAG:
FAMILY_BY_TAG[t] = "impedance"
for t in RHO_BLOCKS | RHO_STATS:
FAMILY_BY_TAG[t] = "apparent_resistivity"
for t in PHS_BLOCKS | PHS_STATS:
FAMILY_BY_TAG[t] = "phase"
for t in TIPPER_BLOCKS | TIPPER_AUX:
FAMILY_BY_TAG[t] = "tipper"
for t in COH_BLOCKS:
FAMILY_BY_TAG[t] = "coherency"
for t in GEOM_BLOCKS:
FAMILY_BY_TAG[t] = "geometry"
for t in SPATIAL_FILTER_BLOCKS:
FAMILY_BY_TAG[t] = "spatial_filter"
for t in INV1D_BLOCKS:
FAMILY_BY_TAG[t] = "inversion1d"
for t in MEAS_DEF_BLOCKS:
FAMILY_BY_TAG[t] = "measurements"
for t in TSERIES_BLOCKS:
FAMILY_BY_TAG[t] = "time_series"
for t in SPECTRA_BLOCKS:
FAMILY_BY_TAG[t] = "spectra"
for t in {">=MTSECT"}:
FAMILY_BY_TAG[t] = "mt_section"
for t in {">=EMAPSECT"}:
FAMILY_BY_TAG[t] = "emap_section"
for t in {">=TSERIESSECT"}:
FAMILY_BY_TAG[t] = "tseries_section"
for t in {">=SPECTRASECT"}:
FAMILY_BY_TAG[t] = "spectra_section"
for t in {">=DEFINEMEAS"}:
FAMILY_BY_TAG[t] = "define_meas"
for t in {">HEAD", ">INFO", ">FREQ", ">ZROT", ">RHOROT", ">END"}:
FAMILY_BY_TAG[t] = "structure"
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
[docs]
def normalize_tag(tag: str) -> str:
"""Return canonical upper-case tag with leading '>' preserved.
Accepts things like ``"zxxr"`` or ``">Zxxr"`` and returns
``">ZXXR"``. Does **not** try to normalize section openers
(``">=..."``) beyond case.
"""
t = tag.strip()
if not t:
return t
if t[0] in ">=":
head, rest = t[0], t[1:]
return head + rest.upper()
if t[0] != ">":
return ">" + t.upper()
return ">" + t[1:].upper()
[docs]
def tag_family(tag: str) -> str | None:
"""Return family/category name for *tag* if known."""
return FAMILY_BY_TAG.get(normalize_tag(tag))
__all__ = [
# BNF
"BNF_EDI_SKELETON",
# structure
"HEAD_TAGS",
"SECTION_OPENERS",
"COMMENT_TAG",
# options
"HEAD_ALLOWED",
"HEAD_REQUIRED",
"INFO_ALLOWED",
"FREQ_ALLOWED",
"FREQ_REQUIRED",
"FREQ_ENUMS",
"ZROT_ALLOWED",
"ZROT_REQUIRED",
"RHOROT_ALLOWED",
"RHOROT_REQUIRED",
"DEFINEMEAS_ALLOWED",
"DEFINEMEAS_REQUIRED",
"EMEAS_ALLOWED",
"EMEAS_REQUIRED_CORE",
"EMEAS_ENUMS",
"HMEAS_ALLOWED",
"HMEAS_REQUIRED_CORE",
"HMEAS_ENUMS",
"TSERIESSECT_ALLOWED",
"TSERIES_ALLOWED",
"SPECTRASECT_ALLOWED",
"SPECTRA_ALLOWED",
"MTSECT_ALLOWED",
"EMAPSECT_ALLOWED",
"OTHERSECT_ALLOWED",
"BLOCK_OPTIONS",
# families & keywords
"Z_REAL_IMAG",
"Z_VAR_COV",
"FZ_REAL_IMAG",
"RHO_BLOCKS",
"RHO_STATS",
"PHS_BLOCKS",
"PHS_STATS",
"TIPPER_BLOCKS",
"TIPPER_AUX",
"COH_BLOCKS",
"GEOM_BLOCKS",
"SPATIAL_FILTER_BLOCKS",
"INV1D_BLOCKS",
"DATA_KEYWORDS",
"ALL_KEYWORDS",
"FAMILY_BY_TAG",
# helpers
"normalize_tag",
"tag_family",
]