csemx 0.2 — Specification (BETA)

Status: Beta. Breaking changes may occur between 0.x releases (§12); the format freezes at v1.0.

1. Identity and Purpose

A schema-validated, vendor-neutral interchange format for frequency-domain controlled-source electromagnetic (CSEM) data. A bundle carries calibrated, normalized response functions (§3.6–§3.7), the transmitter and receiver geometry needed to model them, the producer’s data-quality recommendation (use, §9), and optionally the element group membership of §10. It carries no other normative structured payload. (Rationale: csemx-rationale.md.)

It is deliberately not a container for:

Non-normative provenance goes in notes.md (bundle-level narrative), notes columns (per-source or per-sensor free text), or ext_* extension columns. Standard readers may ignore ext_*, so producers must not place information required for standard interpretation in ext_* columns.

Because all geometry is explicit 3D vertices (wires, loops) or oriented point elements at arbitrary (easting, northing, elev), one data model covers surface CSEM, marine towed-dipole, borehole induction logging, and crosswell EM alike: these differ only in where the vertices sit, not in the schema.

Reading guide (non-normative)

Producers creating a bundle should begin with the plain-language quickstart, the table definitions (§5–§10), and the worked example (§13). The global conventions (§3) are essential reference material, but they are primarily written to make reader, writer, and validator implementations unambiguous. This guidance does not weaken or replace any normative requirement.

2. Bundle Structure

A csemx bundle is a ZIP archive containing a single top-level directory. The directory name is producer-chosen but must be filesystem-safe: characters from [a-zA-Z0-9_.-], starting with a letter, digit, or underscore (never . or -, so ., .., hidden directories, and option-like names are excluded), and not ending with . (stripped by Windows). Inside that directory, files appear at fixed names:

<bundle-name>/
├── manifest.yaml                [required]
├── tx.{csv|parquet}             [required]
├── tx_vertices.{csv|parquet}    [required]
├── rx.{csv|parquet}             [required]
├── rx_vertices.{csv|parquet}    [required]
├── data.{csv|parquet}           [required]
├── groups.{csv|parquet}         [optional]
└── notes.md                     [optional]

Each table is delivered as either <name>.csv or <name>.parquet: exactly one per table, chosen independently (e.g. small geometry tables as CSV, a large data table as Parquet). The optional groups table (§10), when present, follows the same rules as the required tables. A Parquet table holds the same columns, constraints, and foreign keys (§5–§9) as its CSV form, with the column types of those sections as Parquet types; missing measurement values are the native floating-point NaN (§3.8). manifest.yaml is always YAML. String ID columns must remain string-typed in Parquet: producers must not let table writers infer integer types that strip leading zeros (e.g. 0011). Floating-point columns must be Parquet DOUBLE / float64, not float32, so coordinate and response precision are preserved.

Every CSV table has a header row naming its columns (RFC 4180); columns are addressed by name, not by position (Parquet tables carry the same column names in their schema).

Producers should not include other files inside the bundle directory. Readers must warn and ignore unknown files there.

3. Conventions

All files in a bundle obey these conventions. They are stated here once and apply globally.

3.1 Coordinate System

Declared in manifest.yaml under coordinate_system.epsg_horizontal: an EPSG code for a projected coordinate reference system (e.g. 32612 = WGS84 / UTM zone 12N, 32712 = 12S, or any projected national grid). Coordinates are always projected (easting, northing), in meters, the unit for all geometry operations. The horizontal CRS must use meter axis units; projected CRSs in feet (for example, State Plane ftUS zones) are invalid.

A geographic (longitude/latitude) CRS such as 4326 is not permitted. Absolute latitude/longitude is recovered by reprojecting easting/northing through epsg_horizontal and is never stored. A local survey grid must be reprojected to a projected EPSG CRS before delivery; its original coordinates may be carried as ext_* columns in the vertex tables (§6, §8) or in notes.md, but the projected EPSG geometry is always authoritative. One coordinate system per bundle.

Write easting/northing/elev with enough decimal places to resolve the shortest element’s orientation, which is the difference of its vertices: a ~1 m dipole at centimeter precision (2 decimals) fixes azimuth only to ~0.5°, so use millimeter precision (3 decimals) or finer.

3.2 Elevation and Altitude

Elevation (elev, required). Declared in manifest.yaml under elevation.epsg_vertical, an EPSG code defining the height coordinate: 4979 or a registered vertical CRS with a meter height axis. The two recommended choices:

Other registered EPSG vertical CRS codes (e.g. a national geoid) are permitted but discouraged for portability. The elev coordinate in every vertex file is height, positive up, in meters, relative to this datum. There is no default; producers must declare.

Altitude (altitude, optional). Each vertex may additionally carry altitude: vertical height relative to the local earth-model surface, positive up, in meters, with 0 on the surface. The surface is declared under altitude.reference in manifest.yaml:

The surface itself is not part of the bundle; a consumer obtains absolute height as altitude plus the surface height at the vertex in its own model. altitude.reference is required whenever any vertex table includes an altitude column, and absent otherwise.

3.3 Azimuth and Dip

Azimuth and dip apply only to point-geometry transmitters and receivers, where they give the coil/dipole axis as a directed unit vector, not an undirected line. With α = azimuth and δ = dip,

= ( E: cos δ·sin α, N: cos δ·cos α, Up: −sin δ ),

so (0°, 0°) points true north, (90°, 0°) east, (0°, +90°) straight down, and (0°, −90°) straight up. This axis is the element’s positive reference direction (§3.4). The direction is the polarity (there is no separate sign field), so choose azimuth and dip to match the element’s positive source/sensor axis. For a coil, this is the right-hand normal of its winding current. Reversing the physical leads or sensor polarity negates the response and is corrected before delivery. For wire and loop geometries azimuth and dip are forbidden: orientation is fully encoded in the vertices (§3.4).

3.4 Vertex Ordering and Positive Reference Direction

vertex_index is 0-based and contiguous per element. Readers sort an element’s vertices by vertex_index; file row order is irrelevant. Producers exporting from 1-based systems must subtract 1 before writing csemx. Each convention below uses the sorted vertex order: wires use the first and last vertices, while loops use the full vertex circulation.

Every element has one positive reference direction: a transmitter’s source points along it, and a receiver’s datum is taken in that positive sense. It is fixed by vertex order (wire, loop) or by the directed axis of §3.3 (point):

The TX-source and RX-positive-datum directions coincide, so a reciprocal Tx/Rx pair on identical geometry needs no sign flip.

Geometric validity. No two consecutive vertices may coincide (within 1e-6 m). A loop’s first and last vertices must not coincide: closure is implicit (above), never written explicitly. A loop’s edges should not self-intersect; a self-intersecting loop is physically meaningful but rare, so a validator warns rather than rejects it.

3.5 Sign Convention and Phase Reference

Declared in manifest.yaml under sign.time_dependence. One of:

These are the exact ASCII values (lowercase i/w, explicit sign, no spaces); a validator rejects any other string.

No default. Producers must declare. Readers consuming multiple bundles with different conventions must flip the sign of the imaginary part when normalizing to an internal convention.

Phase reference. The complex response (real, imag in the data table) must be delivered in phase relative to the transmitter current spectral component at the reported frequency. If acquisition or processing uses a different internal reference (lock-in phase, GPS time, transmitter voltage, or a reference-clock phase), the producer must correct the delivered response to this current reference before writing the bundle. Beyond this current-reference conversion, no propagation-time correction or additional timing convention is represented in the bundle.

Neither convention affects a DC row (frequency = 0, §3.12), whose present response is purely real.

3.6 Units and the Measured Datum

Each data table row carries a complex response: the receiver’s measured quantity, normalized by the transmitter drive (§3.7). The datum type is determined by the receiver geometry_type: wire and loop receivers report voltage responses, while point (magnetic) receivers report B-field responses.

Units are fixed by this specification; they are not carried in the bundle. csemx is a final processed delivery format: every value is in the canonical unit for its quantity before delivery (§3.7). Because every unit is fixed here, no manifest field declares or selects units, and bundles never require unit reconciliation. All quantities are canonical SI: no SI prefixes (mV, nT, km), no Unicode operators (V/(A·m²)), no scalar multipliers, and no alternate spellings.

Response datum. The data table real/imag (and the err_real/err_imag that share each row) carry the response; its unit follows from the receiver geometry_type. By default the response is the total field; a bundle may instead deliver the secondary field (total minus the free-space primary) by declaring field.content: secondary (§3.11), which changes what real/imag mean but not their unit:

receiver geometry_type datum unit
wire (electric) open-path voltage per TX amp V/A
loop (magnetic) single-turn closed-loop EMF per TX amp V/A
point (magnetic) B-field flux density per TX amp T/A

Error columns err_real/err_imag (§9) have no unit of their own: each inherits the unit of the datum in its row.

All other quantities carry a fixed unit too, stated at each column’s point of use and collected here for reference:

quantity unit defined in
easting, northing meter (m) §3.1, §6, §8
elev meter (m), positive up §3.2, §6, §8
altitude meter (m), positive up §3.2, §6, §8
azimuth_deg degree (°) §3.3, §5, §7
dip_deg degree (°) §3.3, §5, §7
frequency, tx_fundamental hertz (Hz) §9
point_moment_area_m2 square meter (m²) §5

There is deliberately no length or current unit to declare: coordinates are meters, current is normalized away (§3.7), and the one area column carries its unit in its name (point_moment_area_m2, §5).

3.7 Normalization

Every response is normalized so it depends on the earth and the survey geometry, not on how hard the transmitter was driven. There is no un-normalized form; normalize before writing the bundle.

3.8 Missing Values

A missing measurement value is NaN. In a CSV table producers write NaN and readers must accept it case-insensitively (NaN/nan/NAN); in a Parquet table it is the native floating-point NaN. There is no configurable sentinel.

NaN is valid only in the data table measurement columns (real, imag, err_real, err_imag), under the all-or-nothing rule of §9. Required non-measurement fields must carry a non-blank, non-NaN value. Optional or conditionally not-applicable fields may be empty in CSV, or null in Parquet, when omitted for that row; emptiness is not a measurement-missing sentinel.

3.9 Component Naming

A component ID (rx_component_id or tx_component_id) is an opaque, case-sensitive label matching [A-Za-z0-9_-]{1,32}. It is a join key: it must match exactly between the corresponding element table (tx or rx), its vertex table, and data rows that reference it.

Orientation is never read from a label. A label is a stable join/grouping key, not a promise of direction: the format never defines what an Ex points at, the same name may point differently at two stations, and x/y/z never denote the global easting/northing/elev axes. To select or group channels by orientation (e.g. every near-inline dipole), use azimuth_deg/dip_deg (point) or the vertices (wire/loop), which are exact, per-channel, and survive reprojection.

Receivers have a conventional vocabulary, used where it genuinely applies: Ex/Ey/Ez (electric) and Bx/By/Bz (magnetic; always B labels, never a separate H set). It is recommended, not required: a non-orthogonal or grid-relative layout may use a clearer label such as Einline or Ecross. When one of these exact conventional IDs is used, geometry_type must be consistent with it: Ex/Ey/Ez require wire, and Bx/By/Bz require point (a loop receiver reports EMF in V/A, not a point B-component, so it takes a distinct label such as Bloop). A channel’s field type is always derived from geometry_type (§3.10), for conventional and custom labels alike, never from the label.

Transmitters have no conventional set; source elements are labeled at the producer’s discretion (e.g. E1, E2, or descriptive names). A tensor source’s elements share one tx_station_id with distinct tx_component_ids.

3.10 Geometry and Field Type

Geometry represents the acquisition element: finite wires/loops as vertices, small magnetic coils as oriented points. geometry_type is the stored discriminator; the physical field type (electric vs magnetic) is derived from it, not stored:

geometry_type field type element
wire electric an electric dipole (grounded or capacitive); length & shape from vertices
loop magnetic a closed loop source/receiver; path and vector area from vertices
point magnetic a point magnetic dipole/coil; axis, plus area for TX points

So wire ⟹ electric and loop | point ⟹ magnetic. There is no electric loop or magnetic wire, and no electric point: csemx represents electric data as open-path wire voltages/line integrals over finite baselines. A point is always magnetic and carries azimuth_deg/dip_deg for its axis; a point magnetic transmitter additionally carries point_moment_area_m2, while a point magnetic receiver and any loop carry no further columns. The datum unit follows from geometry_type (§3.6), not from the field type: a loop and a point are both magnetic but report V/A and T/A.

3.11 Field Content: Total or Secondary

By default real/imag carry the total per-amp response (§3.6). A bundle may instead carry the secondary response by declaring in manifest.yaml:

field:
  content: secondary # total | secondary

When the field block is absent, content is total and every value is the total field. When content is secondary, real/imag carry the total field minus the free-space primary in the same V/A/T/A unit (§3.6). The primary is needed only to define secondary response and recover total response; no primary value is stored in the bundle.

Primary field. For secondary-field bundles, readers compute the primary from the encoded transmitter and receiver geometry: the response the transmitter would produce at the receiver in a non-conducting whole space, taken in the receiver’s positive datum sense (§3.4). For a pointpoint pair it is the analytic dipole field of point_moment_area_m2 at the transmitter vertex, evaluated at the receiver vertex and projected on the receiver axis ; for finite wire/loop elements it is the corresponding free-space line or loop integral.

Its phase depends on the receiver datum. For a point (B-field, T/A) receiver the primary is in phase with the transmitter current, so it is real under either sign convention (§3.5) and removing it changes only the in-phase part. For a loop (EMF, V/A) receiver the datum is −dΦ/dt, so the free-space primary is in quadrature with the current (−iωΦ under exp(+iwt), +iωΦ under exp(-iwt), where Φ is the free-space flux per amp through the loop). Removing it changes the quadrature part. For wire receiver rows, the primary is the corresponding free-space electric line integral for the encoded source geometry and sign convention.

Recovering total. For secondary-field bundles, consumers recover the total response as total = secondary + primary, with primary computed as above. ppm is not stored; if needed for display or comparison with legacy FDEM deliveries, it is derived outside the csemx datum.

field.content applies to the whole bundle. It must not be secondary when any data row has frequency = 0: the free-space secondary-field convention is not defined at DC in v0.2 (§3.12).

3.12 DC and Static-Limit Data

A data-row frequency of exactly 0 denotes a DC or non-inductive static-limit response, so DC resistivity and magnetometric resistivity (MMR) data are represented without an artificial nonzero frequency. frequency = 0 must not be used as a placeholder for an unknown frequency. -0.0, if encountered during numeric parsing, is equivalent to 0.0; writers should emit canonical 0.

For a present data row with frequency = 0 (§9):

A missing DC datum follows the unchanged all-or-nothing rule of §9: real, imag, err_real, and err_imag are all NaN. The imag = 0/err_imag = 0 requirement applies only to present DC data.

Field content. DC data is supported only for total-field bundles. If any data row has frequency = 0, the manifest must not declare field.content: secondary (§3.11). A bundle containing both DC and nonzero-frequency rows is permitted when field.content is total or omitted under the existing default.

Geometry and polarity. DC transmitter and receiver geometry uses the existing element definitions and vertex-order conventions (§3.4). For a wire transmitter, the ordered endpoints define the signed source orientation and electrode polarity. For a wire receiver, the datum follows the first-to-last convention: V(first) − V(last). For an MMR point receiver, the datum is the magnetic flux-density component along the receiver’s directed positive axis, normalized by transmitter current. Group or array labels (§10) may organize DC elements but do not alter the physical meaning or polarity of a datum.

Apparent resistivity is a derived display/interpretation quantity: it depends on array geometry and interpretation convention, and consumers derive it from the encoded transmitter geometry, receiver geometry, and response. It must not replace the canonical V/A or T/A response in real; a producer may include it as an ext_* column for convenience.

3.13 Navigation, Attitude, and Time (moving platforms)

tx.csv and rx.csv (and their Parquet equivalents) may carry optional navigation/attitude metadata and a UTC timestamp for moving-platform surveys (towed, airborne, surface-vessel, autonomous). The metadata is advisory: it supports QA/QC — excessive pitch or roll, an inverted platform, rapid attitude changes, position discontinuities, elevated navigation uncertainty, motion-correlated artifacts — and does not alter reported element geometry, orientation, datum value, or the data-row uniqueness tuple (§9).

column type constraint meaning
nav_heading_deg float [0, 360) platform heading, clockwise from true north
nav_pitch_deg float [-90, 90] platform pitch; positive nose-up
nav_roll_deg float (-180, 180] platform roll; positive starboard-down
nav_heading_sd_deg float >= 0 one-sigma heading uncertainty
nav_pitch_sd_deg float >= 0 one-sigma pitch uncertainty
nav_roll_sd_deg float >= 0 one-sigma roll uncertainty
nav_pos_sd_m float >= 0 one-sigma radial horizontal-position uncertainty
nav_elev_sd_m float >= 0 one-sigma uncertainty of the reported vertical coordinate
time_utc string grammar below UTC time of the reported position and attitude

Each column is optional. A blank value means the quantity is unavailable or not applicable (§3.8); bundles may contain any mixture of fixed and moving elements. When uncertainty was originally reported at a confidence level other than one standard deviation, the producer must convert it to a one-sigma value before writing these columns; the original convention and conversion may be documented in notes.md. A validator warns when an attitude uncertainty column is populated but its corresponding value column is blank (nav_heading_sd_deg without nav_heading_deg, likewise pitch and roll) — almost certainly a producer bug; the reverse (value without uncertainty) is normal. nav_pos_sd_m and nav_elev_sd_m qualify the vertex position and elevation rather than a navigation value column and may appear alone.

Platform frame and attitude convention. Right-handed platform body frame: x forward, y starboard, z down. Heading is yaw about the down axis, clockwise from true geographic north; any magnetic-declination correction is applied by the producer before delivery (§3.3). Pitch is rotation about the starboard axis, positive nose-up. Roll is rotation about the forward axis, positive starboard-down. The attitude convention is an intrinsic ZYX Tait–Bryan sequence: yaw, then pitch, then roll. These quantities describe the platform frame, not the positive axis of an individual transmitter or receiver component (§3.3).

Authoritative geometry. The element vertices and, for point elements, azimuth_deg/dip_deg remain the authoritative modeling geometry (§3.3, §3.4). Standard readers must not silently recompute or replace element geometry from navigation columns. Consumers may use the metadata to flag, filter, classify, or investigate observations per application-specific QA/QC policies. An advanced consumer may deliberately use it in a documented reprocessing or recalibration workflow; any revised positions or orientations constitute new derived geometry and should be written to a revised bundle.

Position. Best-estimate positions are not duplicated; element positions remain encoded by the vertices (§6, §8). nav_pos_sd_m records the one-sigma radial uncertainty of the horizontal position represented by the vertices, and nav_elev_sd_m the one-sigma uncertainty of the vertex elevation. Axis-specific position uncertainties, position-error covariance, and complete navigation solutions are outside the normative v0.2 model; carry them in ext_* columns or external provenance.

Time (time_utc). The UTC time associated with the element’s reported position and attitude — normally the sounding time, or the time at which navigation and attitude were evaluated or interpolated for that observation. Grammar:

YYYY-MM-DDTHH:MM:SS[.fffffffff]Z

Multiple components at one station. Navigation metadata is stored on element rows and may repeat across components at one station. When multiple components share one platform observation, their navigation and time values should be identical; components observed at meaningfully different positions, attitudes, or times may carry different values, and producers should use distinct station IDs when those differences represent distinct sounding positions. Consumers must not assume two components have identical navigation metadata solely because they share a station ID.

Derived motion quantities. Velocity, speed over ground, course over ground, angular rates, and similar quantities are not normative v0.2 columns. Consumers may derive them from consecutive positions, attitudes, and time_utc values; implementations must account for angular wraparound (a heading change from 359° to 1° is 2°, not −358°). Producer-supplied motion quantities may be carried in ext_* columns.

Scope. In (optional, advisory): platform heading/pitch/roll; attitude uncertainty; horizontal and vertical position uncertainty; per-element UTC time. Out of the normative v0.2 model: raw navigation or IMU streams; mounting calibration; lever-arm and layback models; position/attitude covariance matrices; velocity, speed over ground, course over ground; heave and other derived vehicle-motion quantities; automatic geometry reconstruction from navigation measurements. Mounting, layback, and processing details go in notes.md; additional machine-readable quantities in ext_* columns.

4. File: manifest.yaml

format:
  name: csemx
  version: "0.2"

domain: frequency # v0.2 defines only `frequency`

survey:
  name: "Example Survey"
  revision: 1 # integer, incremented per re-ship; higher = newer
  acquired_start: "2026-02-28" # quoted date or UTC timestamp; see rules below
  acquired_end: "2026-03-15" # same precision as acquired_start; may equal start
  contractor: "Synthetic Producer" # required; part of re-ship identity
  contractor_reference: "Example 26011" # required; part of re-ship identity

coordinate_system:
  epsg_horizontal: 32615 # projected EPSG code; here WGS84 / UTM zone 15N (Gulf of Mexico, marine)

elevation:
  epsg_vertical: 4979 # required: vertical coordinate code (§3.2)

altitude: # optional (§3.2); required only if a vertex table has an `altitude` column
  reference: seafloor # seafloor | ground

sign:
  time_dependence: "exp(+iwt)" # exp(+iwt) | exp(-iwt)

field: # optional (§3.11); absent ⇒ content: total
  content: total # total | secondary

All keys above are required unless noted. v0.2 defines only domain: frequency; readers that implement only v0.2 reject any other domain. Bundle-level free-form contractor notes belong in notes.md (§11), not in the manifest.

The survey mapping has the following required keys:

key type constraint
name string non-blank survey name
revision integer >= 1; incremented for corrected re-ships
acquired_start acquisition endpoint exact grammar below
acquired_end acquisition endpoint exact grammar below; >= acquired_start
contractor string non-blank producer name
contractor_reference string non-blank producer job/contract/reference ID

survey.acquired_start and survey.acquired_end are the acquisition interval endpoints. Each value must be exactly one of:

Both are written as quoted strings (e.g. "2026-05-01"); an unquoted YAML date parses as a date object, not a string, and is rejected.

Both endpoints must use the same precision: either both date-only or both UTC timestamps. survey.acquired_end must be on or after survey.acquired_start. UTC timestamps use literal trailing Z; timezone offsets, local times, fractional seconds, partial times, slashes, month names, and free text are invalid. Date-only values are day-level acquisition bounds, not implied local-midnight instants.

Bundle composition and re-ships. A bundle is self-contained: all data foreign keys resolve within it. A survey identity is (contractor, contractor_reference, survey.name). One survey may be delivered as multiple bundles or re-shipped. Across bundles with the same survey identity, a datum key is the §9 uniqueness tuple (tx_station_id, tx_component_id, rx_station_id, rx_component_id, frequency). If the same datum key appears in more than one bundle, the row from the highest survey.revision supersedes the others; if the same datum key appears more than once at the same survey.revision, the survey delivery is ambiguous and non-conformant.

5. File: tx.csv

One row per transmitter element: one (tx_station_id, tx_component_id) pair. CSV (UTF-8, RFC 4180) or Parquet, per §2.

A tx_station_id groups one or more co-located source elements, each identified by a tx_component_id (e.g. the two crossed bipoles of a tensor source). Orientation comes from geometry, not the label (§3.9).

Required columns

column type constraint
tx_station_id string [A-Za-z0-9_-]{1,64}
tx_component_id string source-element label; [A-Za-z0-9_-]{1,32}
geometry_type enum point | wire | loop

(tx_station_id, tx_component_id) is the unique key.

Geometry follows §3.10; a point transmitter is therefore always magnetic.

Conditionally required

column required when type constraint
azimuth_deg geometry_type=point float [0, 360); positive source axis
dip_deg geometry_type=point float [-90, 90]; positive source axis
point_moment_area_m2 geometry_type=point float > 0; effective single-turn moment area A (m²)

point_moment_area_m2 is the point coil’s effective single-turn area, not N·A; turns are normalized out (§3.7). It is required only for point transmitters because they have no vertices from which to compute loop area.

Optional

column type purpose
notes string free text about this source element; max 1024 characters
nav_heading_deg, nav_pitch_deg, nav_roll_deg, nav_heading_sd_deg, nav_pitch_sd_deg, nav_roll_sd_deg, nav_pos_sd_m, nav_elev_sd_m, time_utc per §3.13 advisory navigation/attitude metadata and UTC time for moving platforms
ext_* any producer-specific extension columns; not required for standard interpretation

6. File: tx_vertices.csv

One row per vertex. Required columns:

column type constraint
tx_station_id string FK to tx.csv.tx_station_id
tx_component_id string FK (joint with tx_station_id)
vertex_index int contiguous ordering key, 0-based per element (§3.4)
easting float meters, per coordinate_system
northing float meters, per coordinate_system
elev float height, positive up, meters, per elevation

Optional columns:

column type purpose
altitude float meters, positive up; vertical height relative to the local earth-model surface declared by altitude.reference (§3.2)
ext_* any producer-specific extension columns; not required for standard interpretation

The ext_* columns may carry per-vertex provenance, e.g. each vertex’s original local-grid coordinates ext_grid_x/ext_grid_y alongside the projected easting/northing (§3.1).

(tx_station_id, tx_component_id, vertex_index) must be unique. Vertex count per (tx_station_id, tx_component_id):

7. File: rx.csv

One row per (rx_station_id, rx_component_id) pair. CSV (UTF-8, RFC 4180) or Parquet, per §2.

Required columns

column type constraint
rx_station_id string [A-Za-z0-9_-]{1,64}
rx_component_id string [A-Za-z0-9_-]{1,32} (§3.9); conventional Ex/Ey/Ez/Bx/By/Bz
geometry_type enum point | wire | loop

(rx_station_id, rx_component_id) is the unique key. A channel’s field type (electric/magnetic) is derived from geometry_type (§3.10), never declared separately. Conventional IDs must be consistent with geometry: Ex/Ey/Ez require wire; Bx/By/Bz require point (§3.9).

Geometry follows §3.10. point and loop receivers carry no moment column; the datum is already calibrated (B-field for point, loop EMF for loop), units per §3.6.

Conditionally required

column required when type constraint
azimuth_deg geometry_type=point float [0, 360); positive sensor axis
dip_deg geometry_type=point float [-90, 90]; positive sensor axis

Optional

column type purpose
notes string free text about this sensor element; max 1024 characters
nav_heading_deg, nav_pitch_deg, nav_roll_deg, nav_heading_sd_deg, nav_pitch_sd_deg, nav_roll_sd_deg, nav_pos_sd_m, nav_elev_sd_m, time_utc per §3.13 advisory navigation/attitude metadata and UTC time for moving platforms
ext_* any producer-specific extension columns; not required for standard interpretation

8. File: rx_vertices.csv

One row per receiver vertex. Required columns:

column type constraint
rx_station_id string FK to rx.csv.rx_station_id
rx_component_id string FK (joint with rx_station_id)
vertex_index int contiguous ordering key, 0-based per element (§3.4)
easting float meters, per coordinate_system
northing float meters, per coordinate_system
elev float height, positive up, meters, per elevation

Optional columns altitude (§3.2) and ext_* extension columns, exactly as for tx_vertices (§6).

(rx_station_id, rx_component_id, vertex_index) must be unique. Vertex count per (rx_station_id, rx_component_id):

9. File: data.csv

One row per measurement. CSV (UTF-8, RFC 4180) or Parquet, per §2.

Required columns (always)

column type constraint
tx_station_id string FK to tx.csv.tx_station_id
tx_component_id string FK (joint with tx_station_id)
rx_station_id string FK to rx.csv.rx_station_id
rx_component_id string FK (joint with rx_station_id)
frequency float >= 0, finite; in Hz; 0 = DC / static limit (§3.12)
real float real part of complex response
imag float imaginary part of complex response
err_real float absolute error on real part; ≥ 0
err_imag float absolute error on imag part; ≥ 0

Optional

column type purpose
tx_fundamental positive float transmitter drive nominal repetition/fundamental (Hz)
use integer 0 or 1; defaults to 1 when omitted
ext_* any producer-specific extension columns; not required for standard interpretation

If use is present, every row must contain 0 or 1; blank values are invalid.

Empty handling

Missing measurement values are NaN, never blank or null (§3.8).

A complex datum is all-or-nothing: either both real and imag are finite (a present datum), or both are NaN (a missing datum: attempted but no usable result; consumers may skip it). One present and the other NaN is invalid. Errors are pairwise all-or-nothing too: a present datum’s err_real/err_imag are either both finite (≥ 0) — delivered uncertainties — or both NaN, meaning the producer has no uncertainty estimates for this datum (common when an acquisition contractor’s processing does not report them). One finite and one NaN is invalid. A missing datum requires NaN in err_real/err_imag; a finite or zero error on a missing datum is invalid.

NaN errors are a statement of fact (“not delivered”), not a quality judgment: producers must never write NaN errors to flag doubtful data (that is use = 0 below) and must never fabricate errors to satisfy a consumer — a consumer that requires uncertainties applies its own error model or floors and remains free to distinguish measured from assigned uncertainty.

A present datum in a row with frequency = 0 must additionally have imag = 0 and, when errors are delivered, err_imag = 0 (§3.12); a missing DC datum is all-NaN like any other.

A present datum the producer considers unreliable but must still deliver (e.g. contractual completeness) is marked use = 0, not omitted, and not given an inflated error. Its real/imag and errors remain the true measured values; use carries the quality judgment separately. The flag is advisory; consumers may override it.

Uniqueness: (tx_station_id, tx_component_id, rx_station_id, rx_component_id, frequency) must be unique. Duplicate rows are invalid.

tx_fundamental is optional provenance, not part of the key. It is not constrained to match frequency; omit it when not meaningful. The response at a given frequency is one physical quantity regardless of the exciting waveform.

The data table is the producer’s selected processed dataset for modeling. If a producer wants to deliver alternate stacks, trial processing runs, or other non-superseding realizations of the same datum, those should be shipped as separate csemx bundles and described in notes.md; they are not represented as parallel rows in one bundle.

10. File: groups.csv (optional)

One row per membership: a transmitter or receiver element’s membership in a named group. CSV (UTF-8, RFC 4180) or Parquet, per §2; the table follows the same CSV and Parquet rules as every other table. Groups may represent acquisition lines, arrays, grids, blocks, or other producer-defined collections. A bundle without useful groupings omits the file.

A group is identified by (group_kind, group_id). Group kinds provide separate namespaces, so (line, L100) and (array, L100) are distinct groups.

Required columns

column type constraint
group_kind string [A-Za-z0-9_-]{1,32}; line is reserved (below)
group_id string [A-Za-z0-9_-]{1,64}
element_kind enum tx | rx
station_id string FK to tx.csv.tx_station_id or rx.csv.rx_station_id, per element_kind

Optional

column type purpose
component_id string joint FK with station_id; blank applies the membership to every component at the station
sequence integer nonnegative ordering rank within an element kind; defined for group_kind=line
notes string free text about this membership; max 1024 characters (as tx.notes/rx.notes)
ext_* any producer-specific extension columns; not required for standard interpretation

(group_kind, group_id, element_kind, station_id, component_id) is the unique key, where every blank component_id is equivalent (blank normalization).

Membership

Relationship to data

Group membership is element metadata only. It does not assign a group to a data row and does not affect datum meaning, modeling semantics, or the data-row uniqueness tuple (§9). Consumers may use membership for application-specific selection, classification, processing, or display; this specification defines no canonical mapping from element groups to data groups. (Example consumer policies: select every datum whose receiver belongs to a receiver line; select data where both Tx and Rx belong to a chosen group; compare data connecting two lines; use groups only for plotting. These are consumer policies, not bundle semantics.) Readers may ignore the table entirely.

Kinds

Hierarchy is out of scope in v0.2: an element on line 1200N and inside array A1 has separate membership rows, and nesting stays implicit. Chainage is not a normative column; a producer-supplied designed or surveyed chainage may be carried in an ext_* column (e.g. ext_chainage_m) as provenance, which may differ from chainage derived from as-built positions.

Example

group_kind,group_id,element_kind,station_id,component_id,sequence
line,L100,tx,T01,,0
line,L100,tx,T02,,1
line,L100,rx,R01,,0
line,L100,rx,R02,,1
line,L100,rx,R03,,2
array,A1,rx,R01,Ex,
array,A1,rx,R02,Ex,

Line L100 contains both transmitter and receiver traverses under one survey line name; their sequences are independent. The array memberships apply only to the named Ex components. Neither group states anything about which data rows belong to it.

11. File: notes.md (optional)

Free-form contractor-authored markdown for bundle-level narrative. No schema. May summarize non-normative instrument provenance, processing history, contractor QC notes, and acknowledgments, but must not carry information required to interpret or calibrate the data. Readers must not interpret its contents programmatically.

12. Versioning

13. Worked Example

A representative bundle exercising every geometry flavor: a grounded-wire electric transmitter (TX01), a surface loop transmitter (TX02), a borehole point-magnetic transmitter (BH1), and one receiver station (001) with electric dipoles, point magnetic coils, and a finite magnetic loop receiver.

manifest.yaml:

format: { name: csemx, version: "0.2" }
domain: frequency
survey:
  name: "Example"
  revision: 1
  acquired_start: "2026-05-01T14:32:00Z"
  acquired_end: "2026-05-01T18:47:00Z"
  contractor: "Synthetic Producer"
  contractor_reference: "Example 0001"
coordinate_system: { epsg_horizontal: 32612 } # WGS84 / UTM 12N (onshore Arizona, land)
elevation: { epsg_vertical: 4979 }            # WGS84 ellipsoidal
sign: { time_dependence: "exp(+iwt)" }

tx.csv: point_moment_area_m2 is filled only for the borehole point source; the wire and loop leave it (and azimuth/dip) blank:

tx_station_id,tx_component_id,geometry_type,azimuth_deg,dip_deg,point_moment_area_m2
TX01,E1,wire,,,
TX02,M1,loop,,,
BH1,M1,point,0,90,0.0079

tx_vertices.csv: wire = 2 electrodes, loop = closed polygon (closure implicit), borehole point = a single vertex at depth:

tx_station_id,tx_component_id,vertex_index,easting,northing,elev
TX01,E1,0,554252.03,3626434.36,1849.10
TX01,E1,1,554648.70,3626426.20,1899.21
TX02,M1,0,556000.00,3628000.00,1805.00
TX02,M1,1,556100.00,3628000.00,1805.50
TX02,M1,2,556100.00,3628100.00,1806.00
TX02,M1,3,556000.00,3628100.00,1805.50
BH1,M1,0,556000.00,3628000.00,1000.00

rx.csv: electric channels are wires (azimuth/dip blank); magnetic point channels are point coils (axis in azimuth/dip); the magnetic loop receiver is a closed voltage receiver. No moment column appears for receivers:

rx_station_id,rx_component_id,geometry_type,azimuth_deg,dip_deg
001,Ex,wire,,
001,Ey,wire,,
001,Bx,point,0,0
001,By,point,90,0
001,Bz,point,0,90
001,Bloop,loop,,

rx_vertices.csv: dipoles have 2 vertices; the three point coils are co-located at the magnetometer point; the loop has four vertices with implicit closure:

rx_station_id,rx_component_id,vertex_index,easting,northing,elev
001,Ex,0,551100.00,3625900.00,1460.00
001,Ex,1,551200.00,3625900.00,1461.00
001,Ey,0,551150.00,3625850.00,1460.50
001,Ey,1,551150.00,3625950.00,1460.50
001,Bx,0,551150.00,3625900.00,1460.00
001,By,0,551150.00,3625900.00,1460.00
001,Bz,0,551150.00,3625900.00,1460.00
001,Bloop,0,551130.00,3625880.00,1460.00
001,Bloop,1,551170.00,3625880.00,1460.00
001,Bloop,2,551170.00,3625920.00,1460.00
001,Bloop,3,551130.00,3625920.00,1460.00

data.csv: wire and loop receiver rows are V/A; point magnetic receiver rows are T/A (per §3.6):

tx_station_id,tx_component_id,rx_station_id,rx_component_id,frequency,real,imag,err_real,err_imag
TX01,E1,001,Ex,0.125,2.14e-6,-3.10e-7,3.0e-8,2.8e-8
TX01,E1,001,Ey,0.125,8.40e-7,-1.20e-7,2.0e-8,2.1e-8
TX01,E1,001,Bz,0.125,5.30e-12,-9.10e-13,1.1e-13,1.0e-13
TX01,E1,001,Bloop,0.125,-1.10e-9,-6.70e-9,1.2e-10,1.1e-10
TX02,M1,001,Bz,0.125,7.80e-11,-1.40e-11,9.0e-13,8.5e-13
BH1,M1,001,Bz,0.125,3.20e-11,-5.50e-12,4.0e-13,3.8e-13

14. References

This spec is text-based and built on widely-supported standards, so conforming readers and writers can be assembled from existing libraries rather than written from scratch.

Underlying formats (normative). A conforming bundle is valid against these:

Coordinate / CRS handling (EPSG). Every horizontal and vertical reference system is an EPSG code; the registry and tooling are mature:

Bundle parsing: