Skip to content

feat(spo2): nightly 5/MG SpO2 candidate as the mean of per-window medians (experimental) - #2825

Open
LeonardSEO wants to merge 1 commit into
ryanbr:mainfrom
LeonardSEO:feat/spo2-candidate-windowed-median
Open

LeonardSEO wants to merge 1 commit into
ryanbr:mainfrom
LeonardSEO:feat/spo2-candidate-windowed-median

Conversation

@LeonardSEO

Copy link
Copy Markdown

Experimental, like the candidate itself. The method follows one captured night; I do not expect it to be accepted as an improvement, and it is not described as one.

What this PR does

Changes how the nightly 5/MG SpO2 candidate (spo2_candidate_82) is aggregated. Display-only, behind the existing default-off toggle; it never writes spo2Pct and feeds nothing.

  • Before: the mean of every in-band (70-100) second inside the sleep sessions.
  • After: nonzero @82 seconds are grouped into measurement windows (a gap of more than 2 s ends one). A window with at least 5 in-band seconds yields its median, and the night is the mean of those medians. Status codes stay inside a window but never count as readings.
  • samples is now the in-band seconds of the windows that counted.
  • Swift and Kotlin twins changed together; the tests are rewritten with the same fixtures on both platforms.

Why: in an overnight iOS capture, byte 82 is 0 in every record except 15 windows of exactly 30 s starting exactly every 1,200 s, all during sleep. Several windows hold only 1-4 valid seconds (one a lone 78 % among status codes). A per-second mean weighs those thin windows like full ones, which may explain part of the split cross-device evidence noted at the candidate. That is a hypothesis until a captured night is compared with the official app's own SpO2.

Source: LeonardSEO/wearable5.0, FINDINGS §13 (spo2_windows).

Validation: none beyond that one night (7 windows of 93-99 %). No held-out number, so per docs/VALIDATION_PROTOCOL.md this is not claimed as an improvement.

Type of change

  • New feature

How it was tested

  • Spo2CandidateNightlyTests / Spo2CandidateNightlyTest, rewritten with the same fixtures on both platforms: window median, mean of medians, status codes and thin windows ignored, a gap splitting windows, session bounds, band edges.
  • swift test in Packages/StrandAnalytics: 2141 tests pass.
  • ./gradlew testFullDebugUnitTest: 6686 tests, all pass apart from the 2 RecoveryDriversTest half-tie cases that fail the same way on unmodified main.
  • Tools/i18n_audit.py --ci, Tools/doc_comment_lint.py, docs/protocol-examples checks and Tools/parity_ledger.py --refresh-derived (derived snapshots included): pass.
  • Tools unit tests, the parity acceptance suite and the R-R contract: pass apart from the 4 test_check_source_references cases (/var vs /private/var temp path on macOS) that fail the same way on unmodified main.
  • Apple: the macOS app (Strand) builds with this change on my development branch; StrandTests and the iOS target were not run locally for this branch. app-build.yml covers both.

Checklist

  • Swift package tests pass for any package I touched
  • Android unit tests pass if I touched android/ (apart from the 2 RecoveryDriversTest half-tie cases that fail the same way on unmodified main)
  • No new build warnings introduced (Android: none; Apple warnings not checked per branch)
  • UI changes use only StrandDesign / Palette / Metrics tokens
  • No hardcoded hex frame bytes; protocol facts live in the schema / decoders
  • Follows the conventions in docs/CONTRIBUTING.md
  • I did not commit generated output (Strand.xcodeproj/) or any secrets/keystores

Related issues

Refs #103

🤖 Generated with Claude Code

…ians

Byte 82 is measured in 30 s windows every 1,200 s during sleep (LeonardSEO/wearable5.0 FINDINGS §13). Each window with at least 5 in-band seconds yields its median; the night is the mean of those medians. Display-only and still default-off; not claimed as an improvement.

Refs ryanbr#103
@meta1971

Copy link
Copy Markdown

Independent data from an MG (fw 50.40.1.0) for the PR's premise: windows of 30 s every 1,200 s in sleep, a window needs 5 in-band seconds, in-band = 70 to 100. Labels as in #891.

OBSERVED: R18 byte 82, 24 Sep to 24 Oct, all pulls merged so each window is counted once: 402 windows on 26 nights. 401 hold exactly 30 non-zero seconds (one holds 29). Consecutive windows start 1,200 s apart (1,199 to 1,200 within every night; the larger gaps are missing windows). Every non-zero second but 3 is in the strap's sleep state (12,056 sleep, 3 up, none awake or still).

OBSERVED: with the PR's rule, 375 windows (93%) have 5 or more in-band seconds, 9 have 1 to 4 and 18 have none, so the rule drops 27 of 402 (6.7%). Window medians run from 90 to 100, and the nightly median of the window medians from 94 to 98.

OBSERVED: 1,909 non-zero seconds (16% of all non-zero seconds) lie outside the band. In order of frequency, the 14 most frequent: 8, 32, 168, 160, 2, 128, 40, 136, 16, 3, 24, 152, 144, 4. All 14 are sums of the bits 0x01, 0x02, 0x04, 0x08, 0x10, 0x20 and 0x80, and none has 0x40. INFERRED: they are status flags, not readings. No reference oximeter, so no calibration claim.

Checked: all counts from the full-corpus script output (402, 401, 375, 9, 18, 1,909, 12,056, 3) and recomputed on the 24 to 26 Oct records alone: 38 windows, every one exactly 30 non-zero seconds, 1,137 sleep seconds and 3 up, 36 windows with 5 or more in-band seconds, window medians 92 to 97, no code with 0x40. The 14 codes sum to 1,909 and the in-band rule is the PR's (70 to 100 inclusive, gap 2 s, 5 seconds).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants