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-RadNetradiation_monitorLocationdex.safecast_mobile_survey— a MovingPlatformDex (drive-track slots), sibling ofaircraftandmobile_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 inobservations, 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 avoyagesummary (n_fixes, span, bbox, track length km, value stats). Embedded tracks are capped at 5,000 points (uniform stride);n_readings_fullrecords the true count andtrack_downsampledflags it. Most devices are small (median ~617 fixes); 9 exceed 20k (max 91,198), hence the cap. The full series stays inobservations. - 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.