Visualization#
The IoT plotting layer lives in pycsamt.iot.plot and is re-exported
from pycsamt.iot. These figures are operational acquisition plots:
they show telemetry health, edge-QC decisions, power budget, and clock
synchronisation before the data are converted into impedance products or
inversion inputs.
The plotting functions accept high-level objects such as
pycsamt.iot.FieldSession, telemetry packets, energy estimates,
sync status objects, or serialised mappings. Each returned Matplotlib
figure also carries the normalised data used to draw the panels in a
fig.pycsamt_iot_* attribute. This makes report generation auditable:
the image and the plotted rows stay together.
Build A Visualisation Session#
This example is synthetic and deterministic. It creates three stations on
profile L18: one healthy station, one station with poor finite
coverage, and one station with spike and timing problems. The goal is to
exercise all visual panels, not to represent a real field deployment.
1import numpy as np
2
3from pycsamt.iot import (
4 DeviceConfig,
5 EdgeProcessingConfig,
6 EdgeProcessor,
7 EnergyConfig,
8 FieldSession,
9 MonitoringConfig,
10 PacketKind,
11 StationConfig,
12 TelemetryPacket,
13 estimate_energy_budget,
14)
15
16survey_id = "WILLY-L18-VISUAL"
17devices = [
18 DeviceConfig(
19 "l18-node-01",
20 station="L18-001",
21 channels=["ex", "ey", "hx", "hy"],
22 sample_rate_hz=256.0,
23 ),
24 DeviceConfig(
25 "l18-node-02",
26 station="L18-002",
27 channels=["ex", "ey", "hx", "hy"],
28 sample_rate_hz=256.0,
29 ),
30 DeviceConfig(
31 "l18-node-03",
32 station="L18-003",
33 channels=["ex", "ey", "hx", "hy"],
34 sample_rate_hz=256.0,
35 ),
36]
37stations = [
38 StationConfig(
39 "L18-001",
40 profile="L18",
41 position_m=0.0,
42 lat=7.501,
43 lon=-5.201,
44 channels=["ex", "ey", "hx", "hy"],
45 ),
46 StationConfig(
47 "L18-002",
48 profile="L18",
49 position_m=50.0,
50 lat=7.502,
51 lon=-5.198,
52 channels=["ex", "ey", "hx", "hy"],
53 ),
54 StationConfig(
55 "L18-003",
56 profile="L18",
57 position_m=100.0,
58 lat=7.503,
59 lon=-5.195,
60 channels=["ex", "ey", "hx", "hy"],
61 ),
62]
63
64session = FieldSession(
65 survey_id,
66 devices=devices,
67 stations=stations,
68 monitoring_config=MonitoringConfig(
69 method="amt",
70 expected_interval_s=6.0,
71 max_gap_s=18.0,
72 min_edge_acceptance_rate=0.70,
73 min_battery_v=11.2,
74 required_channels=["ex", "ey", "hx", "hy"],
75 ),
76)
77
78processor = EdgeProcessor(
79 EdgeProcessingConfig(
80 decimation=1,
81 finite_threshold=0.85,
82 warn_finite_threshold=0.95,
83 channel_names=["ex", "ey", "hx", "hy"],
84 spike_threshold=4.0,
85 max_spike_fraction=0.12,
86 )
87)
88
89rng = np.random.default_rng(42)
90for idx, device in enumerate(devices):
91 data = rng.normal(size=(256, 4))
92 if idx == 1:
93 data[20:230, 1] = np.nan
94 if idx == 2:
95 data[80:125, 0] = 15.0
96
97 edge = processor.process(data)
98 qc_packet = edge.to_packet(
99 device,
100 timestamp=1_700_000_000.0 + idx * 6.0,
101 survey_id=survey_id,
102 qos=1,
103 )
104 qc_packet.payload["station"] = device.station
105 qc_packet.payload["method"] = "amt"
106 session.add_packet(qc_packet)
107
108 health = TelemetryPacket.from_device(
109 device,
110 timestamp=1_700_000_002.0 + idx * 6.0,
111 survey_id=survey_id,
112 kind=PacketKind.HEALTH,
113 payload={
114 "station": device.station,
115 "battery_v": [12.55, 11.84, 10.92][idx],
116 "temperature_c": [30.1, 31.4, 33.2][idx],
117 },
118 )
119 session.add_packet(health)
120
121 power_config = EnergyConfig(
122 battery_wh=[180.0, 120.0, 70.0][idx],
123 active_power_w=[1.2, 1.6, 2.1][idx],
124 sleep_power_w=0.18,
125 duty_cycle=[0.25, 0.35, 0.55][idx],
126 solar_wh_per_day=[8.0, 3.0, 0.0][idx],
127 telemetry_power_w=2.0,
128 telemetry_seconds_per_day=600.0,
129 edge_power_w=0.35,
130 edge_duty_cycle=0.20,
131 min_runtime_days=5.0,
132 device_id=device.device_id,
133 )
134 power_packet = estimate_energy_budget(power_config).to_packet(
135 device,
136 timestamp=1_700_000_004.0 + idx * 6.0,
137 survey_id=survey_id,
138 )
139 power_packet.payload["station"] = device.station
140 session.add_packet(power_packet)
141
142sync_payloads = [
143 {
144 "station": "L18-001",
145 "offset_ms": 0.32,
146 "drift_ppm": 1.4,
147 "jitter_ms": 0.16,
148 "gps_lock": True,
149 "n_reference_points": 120,
150 "quality": "excellent",
151 },
152 {
153 "station": "L18-002",
154 "offset_ms": 1.84,
155 "drift_ppm": 11.2,
156 "jitter_ms": 0.72,
157 "gps_lock": True,
158 "n_reference_points": 118,
159 "quality": "fair",
160 },
161 {
162 "station": "L18-003",
163 "offset_ms": 7.62,
164 "drift_ppm": 69.5,
165 "jitter_ms": 2.15,
166 "gps_lock": False,
167 "n_reference_points": 61,
168 "quality": "poor",
169 },
170]
171for idx, payload in enumerate(sync_payloads):
172 session.add_packet(
173 TelemetryPacket.from_device(
174 devices[idx],
175 timestamp=1_700_000_006.0 + idx * 6.0,
176 survey_id=survey_id,
177 kind=PacketKind.SYNC,
178 payload=payload,
179 )
180 )
181
182print(f"survey_id: {session.survey_id}")
183print(f"n_devices: {session.n_devices}")
184print(f"n_stations: {session.n_stations}")
185print(f"n_packets: {session.n_packets}")
Output:
survey_id: WILLY-L18-VISUAL
n_devices: 3
n_stations: 3
n_packets: 12
Plot The Field Dashboard#
Use pycsamt.iot.plot_field_dashboard() for an at-a-glance field
status. The four panels show station health, edge-QC acceptance, power or
synchronisation state, and packet timing. Set station_axis="profile"
for line work and station_axis="map" when all stations have valid
coordinates.
1from pathlib import Path
2
3from pycsamt.iot import plot_field_dashboard
4
5out_dir = Path("docs/source/images/user_guide/iot")
6out_dir.mkdir(parents=True, exist_ok=True)
7
8dashboard = plot_field_dashboard(
9 session,
10 now=1_700_000_030.0,
11 station_axis="profile",
12 title="IoT field dashboard: WILLY L18",
13)
14dashboard.savefig(
15 out_dir / "user-guide-iot-visualization-01.png",
16 dpi=180,
17)
18
19dashboard_data = dashboard.pycsamt_iot_dashboard
20print(f"dashboard_stations: {len(dashboard_data['stations'])}")
21print(f"dashboard_packets: {len(dashboard_data['packets'])}")
22print(f"monitoring_level: {dashboard_data['monitoring']['level']}")
23print(f"issues: {', '.join(dashboard_data['issues']) or '-'}")
Output:
dashboard_stations: 3
dashboard_packets: 12
monitoring_level: critical
issues: battery_below_threshold, edge_acceptance_rate_below_threshold, required_channels_missing
Plot Edge-QC Detail#
Use pycsamt.iot.plot_edge_qc_summary() when the dashboard indicates
that edge quality is the problem. The function accepts a
pycsamt.iot.FieldSession, one or more QC telemetry packets, or
raw pycsamt.iot.EdgeProcessingResult objects.
1from pycsamt.iot import plot_edge_qc_summary
2
3qc_fig = plot_edge_qc_summary(
4 session,
5 title="Edge QC visual summary: WILLY L18",
6)
7qc_fig.savefig(
8 out_dir / "user-guide-iot-visualization-02.png",
9 dpi=180,
10)
11
12qc_rows = qc_fig.pycsamt_iot_edge_qc
13print(f"qc_rows: {len(qc_rows)}")
14print(f"qc_decisions: {sorted({row['decision'] for row in qc_rows})}")
15print(f"qc_channels: {sorted({row['channel'] for row in qc_rows})}")
Output:
qc_rows: 12
qc_decisions: ['accept', 'reject']
qc_channels: ['ex', 'ey', 'hx', 'hy']
Plot Power Budgets#
Use pycsamt.iot.plot_power_budget() to compare daily load, harvest,
runtime, and power states. The input can be a session containing power
packets, a list of pycsamt.iot.EnergyConfig objects, power
telemetry packets, or energy estimates.
1import numpy as np
2
3from pycsamt.iot import plot_power_budget
4
5power_fig = plot_power_budget(
6 session,
7 title="Power budget visual summary: WILLY L18",
8)
9power_fig.savefig(
10 out_dir / "user-guide-iot-visualization-03.png",
11 dpi=180,
12)
13
14power_rows = power_fig.pycsamt_iot_power_budget
15print(f"power_rows: {len(power_rows)}")
16print(f"power_states: {sorted({row['state'] for row in power_rows})}")
17print(
18 "runtime_days: "
19 + ", ".join(
20 "inf" if np.isinf(row["runtime_days"])
21 else f"{row['runtime_days']:.2f}"
22 for row in power_rows
23 )
24)
Output:
power_rows: 3
power_states: ['critical', 'ok']
runtime_days: 40.42, 7.86, 2.21
Plot Clock Synchronisation#
Use pycsamt.iot.plot_sync_quality() to inspect offset, drift, jitter,
reference support, GPS lock, and quality grades. Threshold arguments draw
reference lines but do not mutate the data.
1from pycsamt.iot import plot_sync_quality
2
3sync_fig = plot_sync_quality(
4 session,
5 title="Clock sync visual summary: WILLY L18",
6 tolerance_ms=1.0,
7 max_drift_ppm=10.0,
8 max_jitter_ms=1.0,
9)
10sync_fig.savefig(
11 out_dir / "user-guide-iot-visualization-04.png",
12 dpi=180,
13)
14
15sync_rows = sync_fig.pycsamt_iot_sync_quality
16print(f"sync_rows: {len(sync_rows)}")
17print(f"sync_quality: {sorted({row['quality'] for row in sync_rows})}")
18print(f"gps_lock_values: {[row['gps_lock'] for row in sync_rows]}")
Output:
sync_rows: 3
sync_quality: ['excellent', 'fair', 'poor']
gps_lock_values: [True, True, False]
Generated Figures#
The figures are displayed in a two-column grid so the page remains compact while still showing each diagnostic family clearly.
Choosing The Right Plot#
Function |
Best input |
Use it for |
|---|---|---|
Survey status, station health, packet timing, acceptance rates. |
||
Session, QC packets, or edge results |
Rejection reasons, finite coverage, spike fractions. |
|
Session, power packets, energy configs, or estimates |
Runtime, harvest/load balance, power states. |
|
Session, sync packets, or sync status rows |
Offset, drift, jitter, GPS lock, synchronisation grades. |
Field Interpretation#
Use the dashboard first when reviewing a live or replayed acquisition session. If the dashboard reports a telemetry issue, inspect packet tables and monitoring summaries. If it reports edge acceptance problems, move to the QC summary. If a station is losing voltage or runtime margin, use the power plot. If packet timing or GPS lock is suspicious, use the sync plot.
These figures do not replace MT/AMT/CSAMT response plots. They explain the condition of the acquisition system that produced the data, which is exactly the context needed before deciding whether a station should enter the geophysical processing workflow.