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)
../../_images/user-guide-iot-basic-session-01.png

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.