Basic Session#
A pycsamt.iot.FieldSession is the operational wrapper around an
IoT-enabled survey. It stores field devices, station metadata, telemetry
packets, monitoring thresholds, and the hand-off metadata that later
processing can audit.
This example uses the repository’s real AMT demo line
data/AMT/WILLY_DATA/L18PLT to discover station identifiers from EDI
filenames. The live telemetry packets are synthetic because the EDI files
represent processed survey data, not an IoT packet stream. That split is
typical in documentation and testing: use real survey inventory where it
exists, then generate explicit synthetic telemetry for the acquisition
layer.
Discover Stations From The Demo Data#
The station IDs are taken from the first five EDI filenames in the L18 profile. In a field deployment you would usually receive the same IDs from the logger inventory or a station-occupation sheet.
1from pathlib import Path
2
3edi_dir = Path("data/AMT/WILLY_DATA/L18PLT")
4edi_files = sorted(edi_dir.glob("*.edi"))[:5]
5stations_from_data = [
6 path.stem.split("-")[-1].upper()
7 for path in edi_files
8]
9
10print("Real data path:")
11print(f" {edi_dir.as_posix()}")
12print("Station ids discovered from EDI filenames:")
13print(f" {', '.join(stations_from_data)}")
Output:
Real data path:
data/AMT/WILLY_DATA/L18PLT
Station ids discovered from EDI filenames:
001A, 002U, 003A, 004A, 005U
Create Devices And Stations#
Each station gets one recorder node. The station IDs and profile name come from the real demo line; chainage, dipole geometry, and IoT device IDs are deployment metadata supplied for the example.
1from pycsamt.iot import DeviceConfig, StationConfig
2
3devices = []
4stations = []
5
6for index, (station_id, edi_file) in enumerate(
7 zip(stations_from_data, edi_files),
8 start=1,
9):
10 device_id = f"l18-node-{index:02d}"
11
12 devices.append(
13 DeviceConfig(
14 device_id,
15 station=station_id,
16 protocol="file",
17 sample_rate_hz=512.0,
18 channels=["ex", "ey", "hx", "hy"],
19 role="recorder",
20 metadata={
21 "source_edi": edi_file.as_posix(),
22 "source": (
23 "real L18PLT station id from repository data"
24 ),
25 },
26 )
27 )
28
29 stations.append(
30 StationConfig(
31 station_id,
32 profile="L18",
33 position_m=(index - 1) * 50.0,
34 channels=["ex", "ey", "hx", "hy"],
35 dipole_length_m=50.0,
36 ex_azimuth_deg=90.0,
37 ey_azimuth_deg=0.0,
38 device_ids=[device_id],
39 notes=(
40 "Station id taken from data/AMT/WILLY_DATA/L18PLT; "
41 "IoT telemetry below is synthetic for documentation."
42 ),
43 )
44 )
Build The Field Session#
The monitoring configuration records the operational expectations for the survey: required AMT channels, expected packet interval, battery limit, clock-offset limit, and frequency band.
1from pycsamt.iot import FieldSession, MonitoringConfig
2
3session = FieldSession(
4 "WILLY-L18-IOT-DEMO",
5 devices=devices,
6 stations=stations,
7 method="amt",
8 operator="pyCSAMT documentation",
9 monitoring_config=MonitoringConfig(
10 method="amt",
11 expected_interval_s=60.0,
12 max_gap_s=90.0,
13 min_packet_success_rate=0.95,
14 min_edge_acceptance_rate=0.75,
15 min_battery_v=11.0,
16 max_clock_offset_ms=5.0,
17 required_channels=["ex", "ey", "hx", "hy"],
18 frequency_band_hz=(1.0, 1000.0),
19 ),
20 metadata={
21 "real_data_path": edi_dir.as_posix(),
22 "telemetry_source": "synthetic QC packets for documentation",
23 },
24)
25
26print("Session summary:")
27print(f" survey_id: {session.survey_id}")
28print(f" devices: {session.n_devices}")
29print(f" stations: {session.n_stations}")
30print(f" packets: {session.n_packets}")
Output:
Session summary:
survey_id: WILLY-L18-IOT-DEMO
devices: 5
stations: 5
packets: 0
Add Synthetic QC Telemetry#
The next block creates synthetic qc packets. Four stations report
accepted windows. The last station reports one rejected window and a low
battery value so that the monitoring status has something useful to flag.
1import numpy as np
2
3from pycsamt.iot import PacketKind, TelemetryPacket
4
5rng = np.random.default_rng(7)
6base_time = 1_700_000_000.0
7
8for index, (device, station) in enumerate(zip(devices, stations)):
9 for packet_index in range(3):
10 accepted = not (
11 station.station_id == stations[-1].station_id
12 and packet_index == 2
13 )
14 battery_v = 12.6 - 0.12 * index - 0.03 * packet_index
15 if (
16 station.station_id == stations[-1].station_id
17 and packet_index == 2
18 ):
19 battery_v = 10.7
20
21 payload = {
22 "method": "amt",
23 "station": station.station_id,
24 "channels": ["ex", "ey", "hx", "hy"],
25 "frequency_band_hz": [1.0, 1000.0],
26 "accepted": accepted,
27 "decision": "accept" if accepted else "reject",
28 "battery_v": battery_v,
29 "clock_offset_ms": float(rng.normal(1.2, 0.35)),
30 "latency_s": float(rng.uniform(1.5, 4.5)),
31 }
32
33 session.add_packet(
34 TelemetryPacket.from_device(
35 device,
36 timestamp=base_time + index * 180 + packet_index * 60,
37 payload=payload,
38 kind=PacketKind.QC,
39 survey_id=session.survey_id,
40 )
41 )
Inspect The Session Tables#
The session can produce station tables, packet tables, monitoring status, and a compact pipeline hand-off.
1from pycsamt.iot import monitoring_status_table, packet_table
2
3print("Session summary:")
4print(f" survey_id: {session.survey_id}")
5print(f" devices: {session.n_devices}")
6print(f" stations: {session.n_stations}")
7print(f" packets: {session.n_packets}")
8print()
9
10station_df = session.station_table()
11print("Station table:")
12print(
13 station_df[
14 ["station_id", "profile", "position_m",
15 "channels", "device_ids"]
16 ].to_string(index=False)
17)
18print()
19
20packet_df = packet_table(session.packets)
21print("Packet table:")
22print(
23 packet_df[
24 ["device_id", "kind", "timestamp", "payload_keys"]
25 ].head(5).to_string(index=False)
26)
27print()
28
29status = session.assess(now=base_time + 900.0)
30status_df = monitoring_status_table(status)
31print("Monitoring status:")
32print(
33 status_df[
34 ["level", "n_packet", "packet_success_rate",
35 "edge_acceptance_rate", "battery_min_v", "issues"]
36 ].to_string(index=False)
37)
38print()
39
40pipeline = session.to_pipeline_input()
41print("Pipeline hand-off, first two stations:")
42for row in pipeline["stations"][:2]:
43 print(
44 " "
45 f"{row['station_id']}: "
46 f"n_packets={row['n_packets']}, "
47 f"acceptance_rate={row['acceptance_rate']:.2f}, "
48 f"band={row['accepted_band_hz']}"
49 )
Output:
Session summary:
survey_id: WILLY-L18-IOT-DEMO
devices: 5
stations: 5
packets: 15
Station table:
station_id profile position_m channels device_ids
001A L18 0.0 ex;ey;hx;hy l18-node-01
002U L18 50.0 ex;ey;hx;hy l18-node-02
003A L18 100.0 ex;ey;hx;hy l18-node-03
004A L18 150.0 ex;ey;hx;hy l18-node-04
005U L18 200.0 ex;ey;hx;hy l18-node-05
Packet table:
device_id kind timestamp payload_keys
l18-node-01 qc 1700000000.0 accepted;battery_v;channels;clock_offset_ms;decision;frequency_band_hz;latency_s;method;station
l18-node-01 qc 1700000060.0 accepted;battery_v;channels;clock_offset_ms;decision;frequency_band_hz;latency_s;method;station
l18-node-01 qc 1700000120.0 accepted;battery_v;channels;clock_offset_ms;decision;frequency_band_hz;latency_s;method;station
l18-node-02 qc 1700000180.0 accepted;battery_v;channels;clock_offset_ms;decision;frequency_band_hz;latency_s;method;station
l18-node-02 qc 1700000240.0 accepted;battery_v;channels;clock_offset_ms;decision;frequency_band_hz;latency_s;method;station
Monitoring status:
level n_packet packet_success_rate edge_acceptance_rate battery_min_v issues
critical 15 1.0 0.933333 10.7 battery_below_threshold
Pipeline hand-off, first two stations:
001A: n_packets=3, acceptance_rate=1.00, band=[1.0, 1000.0]
002U: n_packets=3, acceptance_rate=1.00, band=[1.0, 1000.0]
Plot The Basic Session#
The field dashboard gives a quick operational view of station health, edge-QC acceptance, battery/synchronisation state, and packet timing.
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
8plot_field_dashboard(
9 session,
10 now=base_time + 900.0,
11 station_axis="profile",
12 figsize=(10.8, 7.8),
13 output_path=(
14 out_dir / "user-guide-iot-basic-session-01.png"
15 ).as_posix(),
16 close=True,
17)
What To Carry Forward#
The station inventory is grounded in the real L18 demo dataset, while the
telemetry is synthetic and explicitly marked as such. That distinction is
important: EDI files are downstream geophysical products, but an IoT
FieldSession records the operational evidence around acquisition. The
pipeline hand-off keeps those layers connected by carrying station IDs,
channels, frequency-band coverage, packet counts, and acceptance rates
forward into later processing or reporting.