Skip to content

Commit 1a927eb

Browse files
committed
feat: Enhanced sleep classifier with REM-focused optimization
- Add enhancedSleepClassifier.ts with 30-day history, HRV estimation, temporal smoothing - Integrate enhanced model into hybridClassifier for vitals-based classification - Use log-likelihoods + softmax for numerical stability - Optimize for REM detection (score = remAccuracy*2 + awakeAccuracy + balancedAccuracy) - Add training UI in Settings with real-time progress messages - Rename settings menu items for better UX (remove 'debug' terminology) - Hide Test Sleep Detection on Android/iOS (web only)
1 parent 00aec63 commit 1a927eb

5 files changed

Lines changed: 1296 additions & 21 deletions

File tree

app/(tabs)/settings.tsx

Lines changed: 182 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ import {
2828
runHealthConnectDebugReport,
2929
formatHealthConnectDebugReport,
3030
} from '@/services/healthConnect';
31+
import {
32+
trainAndValidate,
33+
loadEnhancedModel,
34+
clearEnhancedModel,
35+
type EnhancedModel,
36+
type ValidationResult,
37+
} from '@/services/enhancedSleepClassifier';
3138

3239
export default function SettingsScreen() {
3340
const [showSleepDebug, setShowSleepDebug] = useState(false);
@@ -40,6 +47,9 @@ export default function SettingsScreen() {
4047
const [showHCDebug, setShowHCDebug] = useState(false);
4148
const [hcDebugReport, setHcDebugReport] = useState<string | null>(null);
4249
const [isLoadingHCDebug, setIsLoadingHCDebug] = useState(false);
50+
const [showEnhancedTraining, setShowEnhancedTraining] = useState(false);
51+
const [enhancedModelStatus, setEnhancedModelStatus] = useState<string | null>(null);
52+
const [isTrainingEnhanced, setIsTrainingEnhanced] = useState(false);
4353

4454
const health = useHealth();
4555

@@ -85,6 +95,82 @@ export default function SettingsScreen() {
8595
}
8696
};
8797

98+
const formatEnhancedModelStatus = (
99+
model: EnhancedModel | null,
100+
validation?: ValidationResult
101+
) => {
102+
if (!model) return 'No enhanced model trained yet.';
103+
104+
let status = `ENHANCED MODEL STATUS\n`;
105+
status += `━━━━━━━━━━━━━━━━━━━━━\n`;
106+
status += `Nights analyzed: ${model.nightsAnalyzed}\n`;
107+
status += `Last updated: ${new Date(model.lastUpdated).toLocaleString()}\n`;
108+
status += `Temporal smoothing: ${model.temporalSmoothingStrength.toFixed(2)}\n\n`;
109+
110+
status += `FEATURE WEIGHTS\n`;
111+
status += `HR: ${model.featureWeights.hr.toFixed(2)}, HRV: ${model.featureWeights.hrv.toFixed(2)}\n`;
112+
status += `HRV Est: ${model.featureWeights.hrvEst.toFixed(2)}, RR: ${model.featureWeights.rr.toFixed(2)}\n\n`;
113+
114+
if (model.validationAccuracy !== null) {
115+
status += `VALIDATION ACCURACY\n`;
116+
status += `━━━━━━━━━━━━━━━━━━━━━\n`;
117+
status += `Overall: ${(model.validationAccuracy * 100).toFixed(1)}%\n\n`;
118+
status += `Per-Stage:\n`;
119+
status += ` Awake: ${(model.perStageAccuracy.awake * 100).toFixed(1)}%\n`;
120+
status += ` Light: ${(model.perStageAccuracy.light * 100).toFixed(1)}%\n`;
121+
status += ` Deep: ${(model.perStageAccuracy.deep * 100).toFixed(1)}%\n`;
122+
status += ` REM: ${(model.perStageAccuracy.rem * 100).toFixed(1)}%\n`;
123+
}
124+
125+
if (validation) {
126+
status += `\nCONFUSION MATRIX (rows=actual, cols=predicted)\n`;
127+
status += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
128+
const stages: Array<'awake' | 'light' | 'deep' | 'rem'> = ['awake', 'light', 'deep', 'rem'];
129+
status += ` awake light deep rem\n`;
130+
for (const actual of stages) {
131+
status += `${actual.padEnd(6)}`;
132+
for (const predicted of stages) {
133+
status += `${validation.confusionMatrix[actual][predicted].toString().padStart(6)}`;
134+
}
135+
status += '\n';
136+
}
137+
status += `\nTotal samples: ${validation.totalSamples}\n`;
138+
}
139+
140+
return status;
141+
};
142+
143+
const handleLoadEnhancedModel = async () => {
144+
const model = await loadEnhancedModel();
145+
setEnhancedModelStatus(formatEnhancedModelStatus(model));
146+
};
147+
148+
const handleTrainEnhanced = async () => {
149+
setIsTrainingEnhanced(true);
150+
setEnhancedModelStatus('Starting training...');
151+
try {
152+
const { model, validation, bestParams } = await trainAndValidate((message) => {
153+
setEnhancedModelStatus(message);
154+
});
155+
let status = formatEnhancedModelStatus(model, validation);
156+
status += `\nBEST PARAMETERS FOUND\n`;
157+
status += `━━━━━━━━━━━━━━━━━━━━━\n`;
158+
status += `Temporal smoothing: ${bestParams.temporalSmoothing}\n`;
159+
status += `HR weight: ${bestParams.hrWeight}\n`;
160+
status += `HRV weight: ${bestParams.hrvWeight}\n`;
161+
setEnhancedModelStatus(status);
162+
} catch (error) {
163+
setEnhancedModelStatus(`Training failed: ${error}`);
164+
} finally {
165+
setIsTrainingEnhanced(false);
166+
}
167+
};
168+
169+
const handleClearEnhanced = async () => {
170+
await clearEnhancedModel();
171+
setEnhancedModelStatus('Enhanced model cleared.');
172+
};
173+
88174
useEffect(() => {
89175
if (showWearable && health.status?.permissionsGranted) {
90176
health.startVitalsPolling();
@@ -282,16 +368,20 @@ export default function SettingsScreen() {
282368
)}
283369

284370
<View style={styles.debugSection}>
285-
<MenuRow
286-
icon="bug-outline"
287-
label="Sleep Detection Debug"
288-
onPress={() => setShowSleepDebug(!showSleepDebug)}
289-
/>
290-
{showSleepDebug && <SleepDebugPanel />}
371+
{Platform.OS === 'web' && (
372+
<>
373+
<MenuRow
374+
icon="pulse-outline"
375+
label="Test Sleep Detection"
376+
onPress={() => setShowSleepDebug(!showSleepDebug)}
377+
/>
378+
{showSleepDebug && <SleepDebugPanel />}
379+
</>
380+
)}
291381

292382
<MenuRow
293383
icon="analytics-outline"
294-
label="Sleep Model Debug (Fitbit/Wearable)"
384+
label="Wearable Sleep Insights"
295385
onPress={() => setShowModelDebug(!showModelDebug)}
296386
/>
297387
{showModelDebug && (
@@ -305,10 +395,10 @@ export default function SettingsScreen() {
305395
{isLoadingModelDebug ? (
306396
<ActivityIndicator size="small" color={colors.primary[500]} />
307397
) : (
308-
<Ionicons name="play" size={16} color={colors.primary[500]} />
398+
<Ionicons name="eye-outline" size={16} color={colors.primary[500]} />
309399
)}
310400
<Text variant="caption" color="primary">
311-
Run Debug Report
401+
View Analysis
312402
</Text>
313403
</Pressable>
314404
<Pressable
@@ -318,13 +408,13 @@ export default function SettingsScreen() {
318408
>
319409
<Ionicons name="refresh" size={16} color={colors.accent.cyan} />
320410
<Text variant="caption" color="primary">
321-
Retrain Model
411+
Refresh
322412
</Text>
323413
</Pressable>
324414
<Pressable style={styles.debugButton} onPress={handleClearModel}>
325415
<Ionicons name="trash-outline" size={16} color={colors.error} />
326416
<Text variant="caption" color="primary">
327-
Clear
417+
Reset
328418
</Text>
329419
</Pressable>
330420
</View>
@@ -350,12 +440,86 @@ export default function SettingsScreen() {
350440
{(Platform.OS === 'android' || Platform.OS === 'ios') && (
351441
<>
352442
<MenuRow
353-
icon="medkit-outline"
354-
label="Health Connect Data Debug"
443+
icon="fitness-outline"
444+
label="Sleep Stage Classifier Training"
445+
onPress={() => {
446+
setShowEnhancedTraining(!showEnhancedTraining);
447+
if (!showEnhancedTraining && !enhancedModelStatus) {
448+
handleLoadEnhancedModel();
449+
}
450+
}}
451+
/>
452+
{showEnhancedTraining && (
453+
<View style={styles.expandedSection}>
454+
<Text variant="caption" color="muted" style={{ marginBottom: spacing.sm }}>
455+
Learn your personal sleep patterns from your wearable data to improve REM
456+
detection accuracy.
457+
</Text>
458+
<View style={styles.debugButtonRow}>
459+
<Pressable
460+
style={styles.debugButton}
461+
onPress={handleLoadEnhancedModel}
462+
disabled={isTrainingEnhanced}
463+
>
464+
<Ionicons name="information-circle" size={16} color={colors.primary[500]} />
465+
<Text variant="caption" color="primary">
466+
Check Status
467+
</Text>
468+
</Pressable>
469+
<Pressable
470+
style={[styles.debugButton, styles.trainButton]}
471+
onPress={handleTrainEnhanced}
472+
disabled={isTrainingEnhanced}
473+
>
474+
{isTrainingEnhanced ? (
475+
<ActivityIndicator size="small" color={colors.gray[950]} />
476+
) : (
477+
<Ionicons name="flash" size={16} color={colors.gray[950]} />
478+
)}
479+
<Text variant="caption" style={{ color: colors.gray[950] }}>
480+
Train Model
481+
</Text>
482+
</Pressable>
483+
<Pressable
484+
style={styles.debugButton}
485+
onPress={handleClearEnhanced}
486+
disabled={isTrainingEnhanced}
487+
>
488+
<Ionicons name="trash-outline" size={16} color={colors.error} />
489+
<Text variant="caption" color="primary">
490+
Clear
491+
</Text>
492+
</Pressable>
493+
</View>
494+
{enhancedModelStatus && (
495+
<ScrollView
496+
style={styles.debugOutput}
497+
horizontal={false}
498+
nestedScrollEnabled={true}
499+
>
500+
<Text
501+
variant="caption"
502+
color="muted"
503+
style={styles.debugOutputText}
504+
selectable={true}
505+
>
506+
{enhancedModelStatus}
507+
</Text>
508+
</ScrollView>
509+
)}
510+
</View>
511+
)}
512+
513+
<MenuRow
514+
icon="heart-outline"
515+
label="View Health Data"
355516
onPress={() => setShowHCDebug(!showHCDebug)}
356517
/>
357518
{showHCDebug && (
358519
<View style={styles.expandedSection}>
520+
<Text variant="caption" color="muted" style={{ marginBottom: spacing.sm }}>
521+
Recent health data from your wearable (last 24 hours)
522+
</Text>
359523
<View style={styles.debugButtonRow}>
360524
<Pressable
361525
style={styles.debugButton}
@@ -365,16 +529,13 @@ export default function SettingsScreen() {
365529
{isLoadingHCDebug ? (
366530
<ActivityIndicator size="small" color={colors.primary[500]} />
367531
) : (
368-
<Ionicons name="play" size={16} color={colors.primary[500]} />
532+
<Ionicons name="refresh-outline" size={16} color={colors.primary[500]} />
369533
)}
370534
<Text variant="caption" color="primary">
371-
Run Debug Report
535+
Load Data
372536
</Text>
373537
</Pressable>
374538
</View>
375-
<Text variant="caption" color="muted" style={{ marginBottom: spacing.sm }}>
376-
Shows all Health Connect data available from your wearable (last 24h)
377-
</Text>
378539
{hcDebugReport && (
379540
<ScrollView
380541
style={styles.debugOutput}
@@ -634,6 +795,9 @@ const styles = StyleSheet.create({
634795
paddingVertical: spacing.xs,
635796
borderRadius: 6,
636797
},
798+
trainButton: {
799+
backgroundColor: colors.primary[500],
800+
},
637801
debugOutput: {
638802
maxHeight: 400,
639803
backgroundColor: colors.gray[900],
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Session: Enhanced Sleep Classifier (2026-01-13)
2+
3+
## Summary
4+
5+
Continued development of the enhanced sleep stage classifier for the dream-stream app. Fixed TypeScript errors, integrated into hybrid classifier, added training UI, and verified on Android emulator.
6+
7+
## Completed Tasks
8+
9+
1. **Fixed TypeScript errors** in `services/enhancedSleepClassifier.ts`
10+
- Lines 408, 661: Removed unnecessary `!== 'any'` comparisons
11+
- The `STAGES` array only contains `['awake', 'light', 'deep', 'rem']`
12+
- The `prevStage` variable is already filtered by `fitbitStage` null/any check
13+
14+
2. **Integrated enhanced classifier into hybridClassifier.ts**
15+
- Added imports for enhanced classifier functions
16+
- Made `startHybridSession()` async to load enhanced model
17+
- Reset temporal state on session start/stop
18+
- Updated `classifyFromVitals()` to use enhanced model when available
19+
- Falls back to basic model if enhanced model not trained
20+
21+
3. **Added training UI to Settings screen**
22+
- New "Enhanced Classifier Training" expandable section
23+
- Three buttons: Check Status, Train Model, Clear
24+
- Displays model status, validation accuracy, per-stage accuracy
25+
- Shows confusion matrix after training
26+
- Styled Train Model button with primary green color
27+
28+
4. **Tested on Android emulator**
29+
- Built release APK successfully
30+
- Installed on Medium_Phone_API_35 emulator
31+
- Verified Settings screen shows new training UI
32+
- Confirmed error handling works (shows "0 nights found" on emulator)
33+
34+
## Files Modified
35+
36+
- `services/enhancedSleepClassifier.ts` - Fixed TS errors (lines 408, 661)
37+
- `services/hybridClassifier.ts` - Integrated enhanced model
38+
- `services/sleep.ts` - Added await for async startHybridSession
39+
- `app/(tabs)/settings.tsx` - Added Enhanced Classifier Training UI
40+
41+
## Files Created
42+
43+
- `services/enhancedSleepClassifier.ts` (from previous session - 959 lines)
44+
45+
## Remaining Work
46+
47+
1. **Run cross-validation with real data** - Need device with Fitbit/Health Connect sleep data
48+
2. **Tune parameters** - Target >70% per-stage accuracy
49+
3. **Add real-time visualization** - Show predictions vs ground truth during sleep
50+
51+
## Technical Notes
52+
53+
### Enhanced Classifier Features
54+
55+
- 30-day history fetching from Health Connect
56+
- HRV estimation from raw HR using RMSSD proxy
57+
- Temporal smoothing with Bayesian prior
58+
- Leave-one-out cross-validation
59+
- Per-user calibration against Fitbit ground truth
60+
61+
### Parameter Grid (Cross-Validation)
62+
63+
- Temporal smoothing: [0.1, 0.2, 0.3, 0.4, 0.5, 0.6]
64+
- HR weights: [0.3, 0.4, 0.5]
65+
- HRV weights: [0.2, 0.3, 0.4]
66+
67+
### Integration Pattern
68+
69+
```
70+
startHybridSession()
71+
-> loadEnhancedModel()
72+
-> resetTemporalState()
73+
74+
classifyFromVitals(vitals)
75+
-> if enhancedModel exists:
76+
classifyWithTemporal(model, vitals)
77+
else:
78+
fallback to basic model
79+
```
80+
81+
## Screenshots
82+
83+
- `notes/screenshots/app_launch.png` - Home screen
84+
- `notes/screenshots/settings_screen.png` - Settings overview
85+
- `notes/screenshots/enhanced_expanded.png` - Training UI expanded
86+
87+
## Next Steps
88+
89+
1. Test with real Fitbit data on physical device
90+
2. Run full cross-validation suite
91+
3. Document optimal parameters for different users
92+
4. Consider adding training progress indicator (% complete)

0 commit comments

Comments
 (0)