Listening for events…

Scope freeze — Safecast radiation dexes (fixed + mobile, one source → two dexes)

Frozen 2026-07-07. Safecast is a citizen radiation network: some devices are stationary sensors, many are mobile (a Geiger counter driven around, logging a reading every few seconds). Those are two different shapes of measurement, so Safecast splits into two dexes (Mike's call 2026-07-07):

  • safecast_fixed_sensor — a Locationdex (station slots), sibling of the EPA-RadNet radiation_monitor Locationdex.
  • safecast_mobile_survey — a MovingPlatformDex (drive-track slots), sibling of aircraft and mobile_radiosonde.

Built for the paper-data campaign ([[project_paper_dex_repoint_campaign]]). Safecast is already an ingested platform source (SafecastFetcher + normalize_safecast), so this is pure organizing of data we already hold — no fetching. Each reading carries a device_id; that is the join key.

The fixed/mobile classifier (the crux)

For each device_id, take the central-95% bounding box of its reading coordinates (the 2.5th–97.5th percentile span of latitude and longitude) and its diagonal in km. The central-95% span is used deliberately: a raw bounding box is stretched by a single bad GPS fix, which would make a stationary sensor look mobile; trimming the outer 2.5% each side ignores those spikes.

  • diagonal < 0.5 km → fixed (station); >= 0.5 km → mobile (survey).

At freeze (4,665,229 geolocated readings, 411 devices):

threshold fixed devices / readings mobile devices / readings
0.2 km 272 / 2,206,485 138 / 697,968
0.5 km (chosen) 284 / 2,334,318 126 / 570,135
1.0 km 286 / 2,339,450 124 / 565,003

The split is stable from 0.5–1.0 km (only 2 devices move); ~12 devices sit in the 0.2–0.5 km fuzzy band. feedback_data_is_data_partial_coverage: the ~12 boundary devices are a documented judgment call, not hidden. (A naive raw bounding box gave 243/168 and misclassified ~40 spike-affected fixed sensors as mobile; the robust measure is why the numbers differ.)

safecast_fixed_sensor — Locationdex (fixed devices)

  • Slot: one fixed Safecast device. Slot id: device_id.
  • Fields: name, latitude/longitude (the device's median position = its home spot), n_readings, first/last UTC, radiation value min/max/mean + unit (mostly cpm), sources=["safecast"], series_embedded=false (the full reading series stays in observations, referenced by device_id).
  • Storage: data/location_storehouse/safecast_fixed_sensor/<device_id>.json, file-per-slot.

safecast_mobile_survey — MovingPlatformDex (mobile devices)

  • Slot: one mobile Safecast device. Slot id: device_id.
  • Payload: track = time-ordered [epoch, lat, lon, value] readings, plus a voyage summary (n_fixes, span, bbox, track length km, value stats). Embedded tracks are capped at 5,000 points (uniform stride); n_readings_full records the true count and track_downsampled flags it. Most devices are small (median ~617 fixes); 9 exceed 20k (max 91,198), hence the cap. The full series stays in observations.
  • Storage: data/platform_storehouse/safecast_mobile_survey/<device_id>.json, file-per-slot.

Measured reality + provenance

feedback_measured_reality_only: every reading is a Geiger counter's measured count of ionizing radiation at a real place and time. KIND_SOURCES[<both kinds>] = ["safecast"]; safecast registered in KNOWN_SOURCES (https://api.safecast.org/). Resolved read-time.

Validation

Both self-validate via the shared dex_validator.validate_kind_or_raise (file-per-slot): index vs disk, one id per file, unique non-null device_id. Fixed requires coordinates; mobile requires a non-empty track. pytest logic tests in tests/test_monitor/test_safecast_radiation_dexes.py.

Deferred → docs/dex-backfill-backlog.md

  • Non-geolocated Safecast readings (dropped here) and any device that is genuinely part-fixed, part-mobile (relocated sensor) — currently classified by its whole-history spread.
  • Threshold re-tuning if the fuzzy-band devices matter to a specific paper.
Live Feed