AMT/CSAMT Edge Diagnostics#
The pycsamt.iot.edge_amt module adds acquisition diagnostics that
are specific to AMT, MT, and CSAMT field telemetry. These routines are
intended for the edge side of a survey: they run on short time windows,
produce compact metrics, and help decide whether a packet should be
accepted, warned, or rejected before downstream impedance processing.
The examples below use synthetic data. No EDI or field logger file is required. The synthetic window is deliberately built with useful AMT-like energy, 50 Hz and 100 Hz powerline contamination, a clipped channel, a flat dropout interval, NaNs, and two sets of synthetic impedance windows. That makes the failure modes explicit and keeps the example reproducible.
What The Diagnostics Check#
Function |
Field question |
Typical action |
|---|---|---|
|
Are 50/60 Hz mains harmonics dominating this window? |
Warn or reject the packet, then inspect grounding and nearby power infrastructure. |
|
Is useful AMT-band energy above the noise floor? |
Prefer higher-SNR windows for transfer-function estimates. |
|
Is the ADC or logger rail clipping? |
Lower gain, check coil/electrode wiring, or discard saturated intervals. |
|
Do electric channels show high drift or high noise proxies? |
Re-seat electrodes, wet contacts, or review dipole wiring. |
|
Which target frequency bands are actually resolved? |
Mark missing bands before inversion or reporting. |
|
Are per-window impedance estimates repeatable? |
Keep stable windows and down-weight unstable windows. |
|
Did a channel go flat or contain gaps? |
Reject the affected interval and inspect sensor/logger health. |
Build A Synthetic Edge Window#
This first block creates a deterministic synthetic window. Ex contains
useful low-frequency energy plus 50 Hz and 100 Hz harmonics. Ey is
also clipped to mimic an overloaded channel. Ex then receives a short
flatline and two NaNs to mimic a sensor dropout.
1import numpy as np
2
3sample_rate = 512.0
4n_samples = 8192
5t = np.arange(n_samples) / sample_rate
6rng = np.random.default_rng(42)
7
8ex = (
9 0.80 * np.sin(2 * np.pi * 7.5 * t)
10 + 0.35 * np.sin(2 * np.pi * 23.0 * t)
11 + 0.45 * np.sin(2 * np.pi * 50.0 * t)
12 + 0.18 * np.sin(2 * np.pi * 100.0 * t)
13 + 0.08 * rng.standard_normal(n_samples)
14)
15ey = (
16 0.65 * np.sin(2 * np.pi * 9.0 * t + 0.35)
17 + 0.22 * np.sin(2 * np.pi * 31.0 * t)
18 + 0.06 * rng.standard_normal(n_samples)
19)
20hx = (
21 0.32 * np.sin(2 * np.pi * 7.5 * t + 0.9)
22 + 0.05 * rng.standard_normal(n_samples)
23)
24
25ex_bad = ex.copy()
26ex_bad[2500:2560] = ex_bad[2499]
27ex_bad[6000] = np.nan
28ex_bad[6001] = np.nan
29
30ey_clipped = np.clip(2.6 * ey, -1.0, 1.0)
Run The Edge Diagnostics#
The same window can be passed through the AMT-specific checks. The
target_bands argument says which bands the deployment cares about,
while signal_band_hz controls the SNR estimate.
1from pycsamt.iot import (
2 assess_impedance_stability,
3 check_channel_saturation,
4 check_contact_resistance,
5 detect_powerline_harmonics,
6 detect_sensor_dropout,
7 estimate_channel_snr,
8 estimate_frequency_coverage,
9)
10
11harmonics = detect_powerline_harmonics(
12 ex_bad,
13 sample_rate,
14 mains_hz=50.0,
15 n_harmonics=5,
16 threshold_ratio=0.02,
17)
18snr_ex = estimate_channel_snr(
19 ex_bad,
20 sample_rate,
21 signal_band_hz=(4.0, 40.0),
22)
23snr_hx = estimate_channel_snr(
24 hx,
25 sample_rate,
26 signal_band_hz=(4.0, 40.0),
27)
28saturation = check_channel_saturation(
29 ey_clipped,
30 limit=1.0,
31 max_clip_fraction=0.01,
32)
33contact = check_contact_resistance(
34 ex_bad,
35 ey,
36 sample_rate=sample_rate,
37 noise_rms_threshold=0.45,
38)
39coverage = estimate_frequency_coverage(
40 ex_bad,
41 sample_rate,
42 target_bands=[(4.0, 40.0), (40.0, 120.0), (120.0, 240.0)],
43 snr_floor_db=6.0,
44)
45dropout = detect_sensor_dropout(ex_bad, min_flat_run=16)
46
47z_stable = (1.0 + 0.45j) * (
48 1.0 + 0.03 * rng.standard_normal((16, 4))
49) * np.exp(1j * np.deg2rad(1.5 * rng.standard_normal((16, 4))))
50
51z_unstable = (1.0 + 0.45j) * (
52 1.0 + 0.35 * rng.standard_normal((16, 4))
53) * np.exp(1j * np.deg2rad(18.0 * rng.standard_normal((16, 4))))
54
55stable = assess_impedance_stability(z_stable)
56unstable = assess_impedance_stability(z_unstable)
57
58dominant = harmonics.dominant
59print("Synthetic AMT edge-diagnostic output")
60print(f"Powerline contaminated: {harmonics.contaminated}")
61print(f"Dominant harmonic: {dominant.frequency_hz:.1f} Hz")
62print(f"Total harmonic power ratio: {harmonics.total_ratio:.3f}")
63print(f"Ex SNR in 4-40 Hz band: {snr_ex:.2f} dB")
64print(f"Hx SNR in 4-40 Hz band: {snr_hx:.2f} dB")
65print(
66 "Ey clipped samples: "
67 f"{saturation['n_clipped']} of {saturation['n_samples']} "
68 f"({100.0 * saturation['clip_fraction']:.1f}%)"
69)
70print(f"Ey saturated: {saturation['saturated']}")
71print(f"Contact proxy ok: {contact['ok']}")
72print(f"Contact flags: {', '.join(contact['flags'])}")
73print(
74 "Frequency coverage: "
75 f"{coverage.f_low_hz:.2f}-{coverage.f_high_hz:.2f} Hz "
76 f"({coverage.n_decades:.2f} decades)"
77)
78print(f"Target-band coverage fraction: {coverage.coverage_fraction:.2f}")
79print(f"Missing target bands: {coverage.missing_bands}")
80print(
81 "Dropout: "
82 f"{dropout['dropout']}, "
83 f"NaNs={dropout['n_nan']}, "
84 f"longest flat run={dropout['longest_flat_run']} samples"
85)
86print(
87 "Stable impedance windows: "
88 f"{stable.stable} "
89 f"(CV={stable.cv_magnitude:.3f}, "
90 f"phase std={stable.phase_std_deg:.2f} deg)"
91)
92print(
93 "Unstable impedance windows: "
94 f"{unstable.stable} "
95 f"(CV={unstable.cv_magnitude:.3f}, "
96 f"phase std={unstable.phase_std_deg:.2f} deg)"
97)
Output:
Synthetic AMT edge-diagnostic output
Powerline contaminated: True
Dominant harmonic: 50.0 Hz
Total harmonic power ratio: 0.232
Ex SNR in 4-40 Hz band: 4.85 dB
Hx SNR in 4-40 Hz band: 13.30 dB
Ey clipped samples: 4601 of 8192 (56.2%)
Ey saturated: True
Contact proxy ok: False
Contact flags: ex_noise_above_threshold, ey_noise_above_threshold
Frequency coverage: 2.00-102.00 Hz (1.71 decades)
Target-band coverage fraction: 0.33
Missing target bands: [(40.0, 120.0), (120.0, 240.0)]
Dropout: True, NaNs=2, longest flat run=61 samples
Stable impedance windows: True (CV=0.026, phase std=1.60 deg)
Unstable impedance windows: False (CV=0.308, phase std=18.63 deg)
Plot The Diagnostics#
The same variables can be plotted for review or embedded into an edge report. The first figure shows the synthetic window and the live power spectrum. The second figure condenses the key QC fractions and compares stable versus unstable impedance windows.
1from pathlib import Path
2
3import matplotlib.pyplot as plt
4
5from pycsamt.iot import compute_live_spectra
6
7out_dir = Path("docs/source/images/user_guide/iot")
8out_dir.mkdir(parents=True, exist_ok=True)
9
10spectra = compute_live_spectra(ex_bad, sample_rate)
11freq = spectra["frequency_hz"]
12psd = spectra["psd"]
13
14fig, axes = plt.subplots(2, 1, figsize=(8.2, 5.8),
15 constrained_layout=True)
16axes[0].plot(t[:1600], ex_bad[:1600], lw=1.0, label="Ex synthetic")
17axes[0].plot(t[:1600], ey[:1600], lw=0.9, alpha=0.75,
18 label="Ey synthetic")
19axes[0].set_xlabel("Time (s)")
20axes[0].set_ylabel("Amplitude")
21axes[0].set_title("Synthetic AMT edge window")
22axes[0].legend(loc="upper right")
23axes[0].grid(alpha=0.25)
24
25axes[1].semilogy(freq[1:], psd[1:], color="tab:blue")
26for peak in harmonics.peaks:
27 if peak.flagged:
28 axes[1].axvline(peak.frequency_hz, color="tab:red",
29 ls="--", lw=1.0, alpha=0.75)
30axes[1].set_xlim(1, 180)
31axes[1].set_xlabel("Frequency (Hz)")
32axes[1].set_ylabel("PSD")
33axes[1].set_title("Live spectrum with flagged mains harmonics")
34axes[1].grid(alpha=0.25, which="both")
35fig.savefig(
36 out_dir / "user-guide-iot-amt-diagnostics-01.png",
37 dpi=180,
38)
39
40fig, axes = plt.subplots(1, 2, figsize=(8.2, 3.6),
41 constrained_layout=True)
42axes[0].bar(
43 ["Harmonic\nratio", "Ey clip\nfraction", "Missing\nband frac"],
44 [
45 harmonics.total_ratio,
46 saturation["clip_fraction"],
47 1.0 - coverage.coverage_fraction,
48 ],
49 color=["tab:red", "tab:orange", "tab:purple"],
50)
51axes[0].set_ylim(0, 1)
52axes[0].set_ylabel("Fraction")
53axes[0].set_title("Edge QC fractions")
54axes[0].grid(axis="y", alpha=0.25)
55
56axes[1].bar(
57 ["Stable Z", "Unstable Z"],
58 [stable.cv_magnitude, unstable.cv_magnitude],
59 color=["tab:green", "tab:red"],
60)
61axes[1].axhline(0.15, color="0.25", ls="--", lw=1.0,
62 label="max_cv")
63axes[1].set_ylabel("Coefficient of variation")
64axes[1].set_title("Impedance-window stability")
65axes[1].legend(loc="upper left")
66axes[1].grid(axis="y", alpha=0.25)
67fig.savefig(
68 out_dir / "user-guide-iot-amt-diagnostics-02.png",
69 dpi=180,
70)
Interpret The Result#
The synthetic packet should not be treated as a clean acquisition window.
The mains detector flags contamination because the 50 Hz and 100 Hz bands
carry a large fraction of the spectral power. The clipped Ey channel
is also unusable for impedance estimation because more than half of its
samples are at the ADC limit. The contact proxy is a warning rather than a
true resistance measurement; passive AMT data cannot measure contact
resistance directly, but high drift and high channel noise are useful
field-side symptoms.
The frequency-coverage result says that only one of the requested target bands is fully represented by this short window. That does not mean the survey failed; it means this packet alone should not be used to support the missing bands. The impedance stability check demonstrates the same principle at transfer-function level: low coefficient of variation and low phase scatter mark stable windows, while high magnitude and phase scatter mark windows that should be down-weighted or rejected.
Using These Metrics In Telemetry#
In an IoT acquisition workflow, these diagnostics are usually attached to
qc packets or folded into the edge-processing result. A practical
edge policy might accept packets with finite data and stable impedance,
warn on mild mains contamination, and reject packets with saturation,
dropout, or severe missing-band coverage. The exact thresholds should be
survey-specific and should be recorded in the deployment manifest so that
field decisions remain reproducible.