Clock Synchronisation#
Clock synchronisation is part of acquisition quality control. A station
can have excellent electrodes and clean spectra, but poor timing still
damages transfer-function estimates, remote-reference work, and any
comparison between stations. The pycsamt.iot.sync helpers audit
device clocks against a reference such as GPS.
The examples below use synthetic timestamps. That is intentional: synchronisation checks need paired local/reference clock samples, not EDI impedance files. The synthetic devices represent common field cases: one healthy GPS-locked node, one drifting node, one node that lost GPS lock, and one badly drifting node.
What Is Measured#
offset_msMedian local-reference timestamp offset in milliseconds.
drift_ppmLinear clock drift in parts per million, estimated from offset change through time.
jitter_msStandard deviation of drift-corrected timing residuals.
gps_lockWhether the node reports active GPS/reference lock.
qualityA compact grade:
excellent,good,fair,poor, orunknown.
Build Synthetic Reference Samples#
The reference clock is sampled every 30 seconds for 10 minutes. Local device clocks are then offset, drifted, and jittered to create realistic audit cases.
1import numpy as np
2
3rng = np.random.default_rng(24)
4reference = np.arange(0.0, 600.0, 30.0)
5
6node_good = (
7 reference
8 + 0.00035
9 + rng.normal(0.0, 0.00008, reference.size)
10)
11node_drift = (
12 reference
13 + 0.002
14 + 35e-6 * reference
15 + rng.normal(0.0, 0.00035, reference.size)
16)
17node_dropout = (
18 reference
19 + 0.0035
20 + rng.normal(0.0, 0.0012, reference.size)
21)
22node_bad = (
23 reference
24 + 0.012
25 + 110e-6 * reference
26 + rng.normal(0.0, 0.002, reference.size)
27)
Assess Individual Devices#
Use pycsamt.iot.ClockSynchronizer when you already have local
and reference timestamps for one device. The thresholds in
pycsamt.iot.SyncConfig define what counts as acceptable for this
deployment.
1from pycsamt.iot import ClockSynchronizer, SyncConfig, sync_status_table
2
3config = SyncConfig(
4 tolerance_ms=1.0,
5 reference="gps",
6 max_drift_ppm=10.0,
7 max_jitter_ms=1.0,
8)
9synchronizer = ClockSynchronizer(config)
10
11status = synchronizer.assess(
12 "l18-node-01",
13 node_good,
14 reference,
15 gps_lock=True,
16)
17
18table = sync_status_table(status)
19print(
20 table[
21 [
22 "device_id", "offset_ms", "drift_ppm", "jitter_ms",
23 "within_tolerance", "gps_lock", "quality",
24 ]
25 ].to_string(index=False)
26)
Output:
device_id offset_ms drift_ppm jitter_ms within_tolerance gps_lock quality
l18-node-01 0.344218 -0.004302 0.068069 True True excellent
Assess A Deployment#
For a deployment, keep one SyncStatus per node and
turn the list into a table. This makes the failure mode visible: the
second and fourth nodes exceed drift or offset limits, while the third
node is capped at fair because GPS lock was lost.
1statuses = [
2 synchronizer.assess(
3 "l18-node-01", node_good, reference, gps_lock=True
4 ),
5 synchronizer.assess(
6 "l18-node-02", node_drift, reference, gps_lock=True
7 ),
8 synchronizer.assess(
9 "l18-node-03", node_dropout, reference, gps_lock=False
10 ),
11 synchronizer.assess(
12 "l18-node-04", node_bad, reference, gps_lock=True
13 ),
14]
15
16table = sync_status_table(statuses)
17print(
18 table[
19 [
20 "device_id", "offset_ms", "drift_ppm", "jitter_ms",
21 "within_tolerance", "gps_lock", "quality",
22 ]
23 ].to_string(index=False)
24)
Output:
device_id offset_ms drift_ppm jitter_ms within_tolerance gps_lock quality
l18-node-01 0.344218 -0.004302 0.068069 True True excellent
l18-node-02 12.276217 35.547894 0.392135 False True poor
l18-node-03 3.162285 -1.116131 1.383608 False False fair
l18-node-04 42.897972 108.638919 1.678547 False True poor
Use The Batch Helper#
When references are already arranged by device, use
pycsamt.iot.batch_assess_sync(). Each value can be a mapping with
local, reference, and optional gps_lock fields.
1from pycsamt.iot import batch_assess_sync
2
3batch = batch_assess_sync(
4 {
5 "l18-node-01": {
6 "local": node_good,
7 "reference": reference,
8 "gps_lock": True,
9 },
10 "l18-node-02": {
11 "local": node_drift,
12 "reference": reference,
13 "gps_lock": True,
14 },
15 },
16 config=config,
17)
18print(
19 batch[
20 ["device_id", "within_tolerance", "quality"]
21 ].to_string(index=False)
22)
Output:
device_id within_tolerance quality
l18-node-01 True excellent
l18-node-02 False poor
Detect GPS Dropout#
Use pycsamt.iot.detect_gps_dropout() to summarise lock/unlock
sequences. This is different from timestamp-pair assessment: it asks
whether the device had enough reference support during the acquisition
period.
1from pycsamt.iot import detect_gps_dropout
2
3gps_lock = [True] * 8 + [False] * 3 + [True] * 6 + [False] * 2 + [True]
4dropout = detect_gps_dropout(
5 gps_lock,
6 timestamps=np.arange(len(gps_lock)) * 30.0,
7 min_lock_fraction=0.9,
8)
9
10for key in [
11 "n_samples",
12 "n_locked",
13 "lock_fraction",
14 "n_dropout_events",
15 "longest_dropout_samples",
16 "longest_dropout_s",
17 "ok",
18]:
19 value = dropout[key]
20 if isinstance(value, float):
21 print(f"{key}: {value:.3f}")
22 else:
23 print(f"{key}: {value}")
Output:
n_samples: 20
n_locked: 15
lock_fraction: 0.750
n_dropout_events: 2
longest_dropout_samples: 3
longest_dropout_s: 90.000
ok: False
Plot Synchronisation Quality#
The plotting helper summarises offset, drift, jitter, GPS lock, reference support, and quality grades. For more than one figure, IoT guide pages use grids; this page has a single combined diagnostic figure.
1from pathlib import Path
2
3from pycsamt.iot import plot_sync_quality
4
5out_dir = Path("docs/source/images/user_guide/iot")
6out_dir.mkdir(parents=True, exist_ok=True)
7
8plot_sync_quality(
9 statuses,
10 tolerance_ms=config.tolerance_ms,
11 max_drift_ppm=config.max_drift_ppm,
12 max_jitter_ms=config.max_jitter_ms,
13 figsize=(10.8, 7.2),
14 output_path=(
15 out_dir / "user-guide-iot-clock-sync-01.png"
16 ).as_posix(),
17 close=True,
18)
Field Interpretation#
The first node is suitable for timing-sensitive processing. Its offset is well below the 1 ms tolerance, drift is nearly zero, and jitter is small. The second and fourth nodes should be corrected or excluded from time-critical windows because their offsets and drift exceed the configured limits. The third node has moderate offset but no GPS lock, so its grade is limited even though the drift estimate itself is not severe.
In a field workflow, store these status rows as sync telemetry packets
or attach them to the acquisition manifest. That keeps the timing evidence
next to the QC, power, and station metadata used by later processing.