Skip to content

Commit ee6ec3b

Browse files
committed
Fix validation to use hybrid CV-based classifier
Updated classifyWithStatsInternal to use same CV + time-based approach as classifyWithModel. Validation now tracks rmssdHistory and consecutiveRemSignals state.
1 parent 6b4b6cf commit ee6ec3b

2 files changed

Lines changed: 46 additions & 87 deletions

File tree

.sisyphus/ralph-loop.local.md

Lines changed: 0 additions & 9 deletions
This file was deleted.

services/remOptimizedClassifier.ts

Lines changed: 46 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -908,6 +908,8 @@ async function runValidation(
908908
}
909909

910910
const sleepSessionStart = new Date(sleepStages[0].startTime);
911+
const validationRmssdHistory: number[] = [];
912+
let validationConsecutiveRemSignals = 0;
911913

912914
for (const stageRecord of sleepStages) {
913915
const stageStart = new Date(stageRecord.startTime);
@@ -928,26 +930,26 @@ async function runValidation(
928930
recentHRs.shift();
929931
}
930932

931-
const matchingHrv = hrvSamples.find((hrv) => {
932-
const t = new Date(hrv.time);
933-
return Math.abs(t.getTime() - hrTime.getTime()) < 60000;
934-
});
933+
const rmssd = computeRmssd(recentHRs);
934+
validationRmssdHistory.push(rmssd);
935+
if (validationRmssdHistory.length > MAX_RMSSD_HISTORY) {
936+
validationRmssdHistory.shift();
937+
}
935938

936-
const predictedStage = classifyWithStatsInternal(
937-
stageStats,
938-
transitionMatrix,
939-
hr.beatsPerMinute,
940-
matchingHrv?.heartRateVariabilityMillis ?? null,
941-
prevStage,
939+
const result = classifyWithStatsInternal(
942940
minutesSinceSleepStart,
943-
[...recentHRs]
941+
validationRmssdHistory,
942+
validationConsecutiveRemSignals,
943+
prevStage,
944+
recentHRs
944945
);
945946

946-
confusionMatrix[actualStage][predictedStage]++;
947-
if (actualStage === predictedStage) correct++;
947+
confusionMatrix[actualStage][result.stage]++;
948+
if (actualStage === result.stage) correct++;
948949
total++;
949950

950-
prevStage = predictedStage;
951+
prevStage = result.stage;
952+
validationConsecutiveRemSignals = result.consecutiveRemSignals;
951953
}
952954
}
953955

@@ -1020,83 +1022,49 @@ function getTimeBasedRemProbability(minutesSinceSleepStart: number): number {
10201022
}
10211023

10221024
function classifyWithStatsInternal(
1023-
stageStats: Record<SleepStage3, Stage3Statistics | null>,
1024-
transitionMatrix: Record<SleepStage3, Record<SleepStage3, number>>,
1025-
heartRate: number,
1026-
hrv: number | null,
1027-
prevStage: SleepStage3,
10281025
minutesSinceSleepStart: number,
1026+
rmssdHistoryInput: number[],
1027+
prevConsecutiveRemSignals: number,
1028+
prevStage: SleepStage3,
10291029
recentHRs: number[]
1030-
): SleepStage3 {
1031-
const STAGES: SleepStage3[] = ['awake', 'nrem', 'rem'];
1032-
1033-
const localRmssd = computeRmssd(recentHRs);
1034-
const localHRMean = recentHRs.length >= 3 ? mean(recentHRs) : heartRate;
1035-
1036-
const remRmssd = stageStats.rem?.pseudoRMSSD ?? 3.0;
1037-
const nremRmssd = stageStats.nrem?.pseudoRMSSD ?? 4.3;
1038-
const awakeRmssd = stageStats.awake?.pseudoRMSSD ?? 8.6;
1030+
): { stage: SleepStage3; consecutiveRemSignals: number } {
1031+
const cv = computeCV(rmssdHistoryInput);
1032+
const timeRemProb = getTimeBasedRemProbability(minutesSinceSleepStart);
10391033

1040-
const remNremThreshold = (remRmssd + nremRmssd) / 2;
1041-
const nremAwakeThreshold = (nremRmssd + awakeRmssd) / 2;
1034+
const cvRemSignal = cv < CV_THRESHOLD ? 1.0 : 0.0;
1035+
const strongCvSignal = cv < CV_THRESHOLD * 0.7;
10421036

1043-
let scores: number[] = [0, 0, 0];
1037+
const remScore = 0.5 * timeRemProb + 0.5 * cvRemSignal * 0.5 + (strongCvSignal ? 0.15 : 0);
10441038

1045-
for (let i = 0; i < STAGES.length; i++) {
1046-
const stage = STAGES[i];
1047-
let score = 0;
1039+
let stage: SleepStage3;
1040+
let newConsecutiveRemSignals = prevConsecutiveRemSignals;
10481041

1049-
if (stage === 'rem') {
1050-
if (minutesSinceSleepStart < FIRST_REM_LATENCY_MINUTES) {
1051-
score = 0.05;
1052-
} else if (localRmssd < remNremThreshold) {
1053-
const rmssdScore = 1 - localRmssd / remNremThreshold;
1054-
score = 0.4 + rmssdScore * 0.4;
1055-
if (prevStage === 'nrem') {
1056-
score += 0.1;
1057-
}
1058-
} else {
1059-
score = 0.15;
1060-
}
1061-
} else if (stage === 'nrem') {
1062-
if (minutesSinceSleepStart < FIRST_REM_LATENCY_MINUTES) {
1063-
score = 0.65;
1064-
} else if (localRmssd >= remNremThreshold && localRmssd < nremAwakeThreshold) {
1065-
const rmssdScore =
1066-
(localRmssd - remNremThreshold) / (nremAwakeThreshold - remNremThreshold);
1067-
score = 0.4 + rmssdScore * 0.3;
1068-
} else if (localRmssd >= nremAwakeThreshold) {
1069-
score = 0.25;
1070-
} else {
1071-
score = 0.3;
1072-
}
1042+
if (minutesSinceSleepStart < 70) {
1043+
stage = 'nrem';
1044+
newConsecutiveRemSignals = 0;
1045+
} else if (remScore > 0.25) {
1046+
newConsecutiveRemSignals++;
1047+
if (newConsecutiveRemSignals >= REM_CONSECUTIVE_REQUIRED) {
1048+
stage = 'rem';
10731049
} else {
1074-
if (localRmssd >= nremAwakeThreshold) {
1075-
const rmssdScore = Math.min(1, (localRmssd - nremAwakeThreshold) / nremAwakeThreshold);
1076-
score = 0.4 + rmssdScore * 0.3;
1077-
} else if (heartRate > localHRMean + 10) {
1078-
score = 0.5;
1079-
} else if (minutesSinceSleepStart < 20) {
1080-
score = 0.3;
1081-
} else {
1082-
score = 0.1;
1083-
}
1050+
stage = 'nrem';
10841051
}
1052+
} else {
1053+
newConsecutiveRemSignals = 0;
1054+
const localHRStd = recentHRs.length >= 3 ? std(recentHRs) : 0;
10851055

1086-
const transitionProb = transitionMatrix[prevStage][stage];
1087-
score += transitionProb * 0.15;
1088-
1089-
scores[i] = score;
1056+
if (cv > 0.5 && localHRStd > 5) {
1057+
stage = 'awake';
1058+
} else {
1059+
stage = 'nrem';
1060+
}
10901061
}
10911062

1092-
let bestIdx = 0;
1093-
for (let i = 1; i < scores.length; i++) {
1094-
if (scores[i] > scores[bestIdx]) {
1095-
bestIdx = i;
1096-
}
1063+
if (prevStage === 'rem' && stage !== 'rem' && remScore > 0.15) {
1064+
stage = 'rem';
10971065
}
10981066

1099-
return STAGES[bestIdx];
1067+
return { stage, consecutiveRemSignals: newConsecutiveRemSignals };
11001068
}
11011069

11021070
/**

0 commit comments

Comments
 (0)