Controlled-Source Edge QC (CSAMT / CSEM)#

Natural-source AMT/MT diagnostics (AMT/CSAMT Edge Diagnostics) assume a plane-wave field with no operator-controlled transmitter. Controlled-source methods – CSAMT and CSEM – add a grounded dipole transmitter, and with it a distinct set of field questions: is the receiver in the far field where the plane-wave assumption holds, is there energy at every transmitted frequency, and is the source current steady? The pycsamt.iot.edge_csamt and pycsamt.iot.edge_csem modules answer these, numpy-only, on short edge windows.

A transmitter device role and a source telemetry packet let a transmitter node report its state alongside the receivers.

Field zones from skin depth#

The controlling quantity is the transmitter-receiver offset expressed in skin depths, \(r/\delta\). A larger ratio means the receiver is electrically farther from the source and the plane-wave assumption is safer. classify_field_zones() labels each frequency near, transition, or far:

import numpy as np
from pycsamt.iot import classify_field_zones, skin_depth_m

freqs = np.array([4096.0, 1024.0, 256.0, 64.0, 16.0, 4.0, 1.0])
cov = classify_field_zones(freqs, resistivity=100.0, offset_m=5000.0)

cov.all_far_field            # False -- some low frequencies roll off
cov.correction_recommended   # True  -- a near-field correction is due
cov.first_far_field_hz()     # lowest frequency safe for plane-wave MT

High frequencies (shallow skin depth) reach the far field first; the lowest frequencies fall into the transition or near field, where CSAMT apparent resistivities roll off and require a near-field correction.

Transmitter frequency comb#

CSAMT transmits a discrete set of frequencies. QC therefore checks that resolvable energy is actually present at each expected line, using a robust median noise floor so closely spaced lines never mask one another:

from pycsamt.iot import detect_transmitter_frequencies

comb = detect_transmitter_frequencies(
    window, sample_rate=2048.0,
    tx_frequencies=[8.0, 32.0, 128.0, 512.0],
)
comb.n_detected, comb.n_expected   # e.g. (3, 4)
comb.missing()                     # frequencies with no resolvable energy

Source-signal stability#

The transmitter current sets the signal level of every sounding, so its steadiness bounds data quality. assess_source_stability() reports the on-state current coefficient of variation, drift, and the on/off keying fraction:

from pycsamt.iot import assess_source_stability

status = assess_source_stability(tx_current, tx_voltage=tx_voltage)
status.stable, status.current_cv, status.on_fraction

CSEM: magnitude/phase versus offset#

CSEM records a dipole source with a receiver array, and its signature data product is the response as a function of source-receiver offset at each frequency. field_vs_offset() builds the magnitude-versus-offset (MVO) and phase-versus-offset (PVO) curve, finds where the signal crosses the noise floor, and checks that amplitude decays monotonically – a bump usually means a bad receiver, a geometry error, or genuine 3-D structure worth a second look:

from pycsamt.iot import field_vs_offset

resp = field_vs_offset(
    offsets_m=[1000, 2000, 4000, 6000, 8000, 10000],
    amplitudes=amplitudes, phases_deg=phases,
    noise_floor=1e-13, frequency_hz=1.0,
)
resp.max_detectable_offset_m   # detectability limit
resp.monotonic_decay           # False flags a suspect reading
resp.dynamic_range_db

Transmitter telemetry#

A transmitter node reports its state as a source packet, parsed by the SourcePayload schema with the same tolerant alias folding and range validation as the other payloads:

from pycsamt.iot import DeviceConfig, FieldSession, parse_payload

tx = DeviceConfig("tx-1", role="transmitter")
payload = parse_payload("source", {
    "tx_id": "TX1", "current": 9.8, "tx_voltage": 250.0,
    "frequency": 32.0, "ab_m": 100.0, "tx_rx_offset": 5000.0,
})
payload.tx_current_a, payload.tx_frequency_hz, payload.offset_m

session = FieldSession("CS1", method="csamt", devices=[tx])
session.add_packet({"device_id": "tx-1", "timestamp": 10.0,
                    "topic": tx.topic("source"), "kind": "source",
                    "payload": payload.as_dict()})

Static shift and transmitter timing lock#

Two further controlled-source concerns. A galvanic static shift multiplies apparent resistivity by a frequency-independent factor while leaving phase unchanged; estimate_static_shift() detects it as a persistent, phase-neutral split between the xy and yx resistivity modes and separates it from anisotropy:

from pycsamt.iot import estimate_static_shift

ss = estimate_static_shift(res_xy, res_yx, phase_xy=phi_xy, phase_yx=phi_yx)
ss.static_shift, ss.shift_factor   # e.g. (True, 3.0)

And a CSAMT/CSEM receiver can report its transmitter timing lock alongside the clock sync, using the tx_locked, tx_sync_offset_ms, and tx_id fields of the sync payload:

from pycsamt.iot import parse_payload

sync = parse_payload("sync", {"offset_ms": 0.4, "transmitter_locked": True,
                              "tx_offset_ms": 0.2, "tx_id": "TX1"})
sync.tx_locked, sync.tx_sync_offset_ms

Aggregation#

csamt_edge_report() and csem_edge_report() collate the per-channel diagnostics, and csamt_edge_table() / csem_edge_table() flatten them into pyCSAMT tables for reporting.