From 028571ba31954729eb53640c393f64a4b01225e1 Mon Sep 17 00:00:00 2001 From: Claudia Onorato Date: Sun, 8 Nov 2020 17:00:21 -0500 Subject: [PATCH 01/29] added board type & completed metadata response --- backend/app.py | 4 +++- backend/classification/config/constants.py | 5 +++++ backend/classification/request.py | 3 ++- backend/classification/response.py | 7 ++++--- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/backend/app.py b/backend/app.py index 226311b3..e95ab14a 100644 --- a/backend/app.py +++ b/backend/app.py @@ -5,7 +5,7 @@ from classification.file_loading import get_raw_array from classification.exceptions import ClassificationError -from classification.config.constants import Sex, ALLOWED_FILE_EXTENSIONS +from classification.config.constants import Sex, AcquisitionBoard, ALLOWED_FILE_EXTENSIONS from classification.model import SleepStagesClassifier from classification.request import ClassificationRequest from classification.response import ClassificationResponse @@ -51,6 +51,7 @@ def analyze_sleep(): form_data = request.form.to_dict() raw_array = get_raw_array(file) + print(AcquisitionBoard[form_data['device']]) try: classification_request = ClassificationRequest( @@ -59,6 +60,7 @@ def analyze_sleep(): stream_start=int(form_data['stream_start']), bedtime=int(form_data['bedtime']), wakeup=int(form_data['wakeup']), + board=AcquisitionBoard[form_data['device']], raw_eeg=raw_array, ) except (KeyError, ValueError, ClassificationError): diff --git a/backend/classification/config/constants.py b/backend/classification/config/constants.py index cc0bb101..c9619bc5 100644 --- a/backend/classification/config/constants.py +++ b/backend/classification/config/constants.py @@ -8,6 +8,11 @@ class Sex(Enum): M = 2 +class AcquisitionBoard(Enum): + OPENBCI_CYTON = 1 + OPENBCI_GANGLION = 2 + + class SleepStage(Enum): W = 0 N1 = 1 diff --git a/backend/classification/request.py b/backend/classification/request.py index 38b1e2ee..f0e51fae 100644 --- a/backend/classification/request.py +++ b/backend/classification/request.py @@ -12,12 +12,13 @@ class ClassificationRequest(): - def __init__(self, sex, age, stream_start, bedtime, wakeup, raw_eeg): + def __init__(self, sex, age, stream_start, bedtime, wakeup, board, raw_eeg): self.sex = sex self.age = age self.stream_start = stream_start self.bedtime = bedtime self.wakeup = wakeup + self.board = board self.stream_duration = raw_eeg.times[-1] self.raw_eeg = raw_eeg diff --git a/backend/classification/response.py b/backend/classification/response.py index dce5a2b3..b0214820 100644 --- a/backend/classification/response.py +++ b/backend/classification/response.py @@ -11,6 +11,7 @@ def __init__(self, request, predictions, spectrogram): self.stream_duration = request.stream_duration self.bedtime = request.bedtime self.wakeup = request.wakeup + self.board = request.board self.n_epochs = request.n_epochs self.spectrogram = spectrogram @@ -33,8 +34,8 @@ def metadata(self): "sessionEndTime": self.stream_duration + self.stream_start, "totalSessionTime": self.stream_duration, "bedTime": self.bedtime, - "wakeUpTime": None, - "totalBedTime": None, + "wakeUpTime": self.wakeup, + "totalBedTime": self.wakeup - self.bedtime, } @property @@ -51,6 +52,6 @@ def response(self): 'report': None, 'metadata': self.metadata, 'subject': self.subject, - 'board': None, + 'board': self.board.name, 'spectrograms': self.spectrogram, } From 2a404537f9de9947ea7951413585b53b48ba399e Mon Sep 17 00:00:00 2001 From: Claudia Onorato Date: Sun, 8 Nov 2020 17:37:53 -0500 Subject: [PATCH 02/29] added base work for tests --- backend/assets/readme.md | 26 ++++++++------- backend/classification/response.py | 34 +++++++++++++++++++- backend/requirements-dev.txt | 1 + backend/tests/test_response.py | 51 ++++++++++++++++++++++++++++++ 4 files changed, 100 insertions(+), 12 deletions(-) create mode 100644 backend/tests/test_response.py diff --git a/backend/assets/readme.md b/backend/assets/readme.md index 09e50e65..38c97f4f 100644 --- a/backend/assets/readme.md +++ b/backend/assets/readme.md @@ -18,21 +18,25 @@ "report": { "sleepOnset": 1602211380, // Time at which the subject fell asleep (time of the first non-wake epoch) "sleepOffset": 1602242425, // Time at which the subject woke up (time of the epoch after the last non-wake epoch) - "wakeAfterSleepOffset": 500, // [seconds] (wakeUpTime - sleepOffset) - "totalSleepTime": 31045, // Total amount of time sleeping including nocturnal awakenings (sleepOffset - sleepOnset) - "efficientSleepTime": 27113, // Total amount of seconds passed in non-wake stages - "sleepEffeciency": 0.8733, // Overall sense of how well the patient slept (totalSleepTime/bedTime) - "totalWASO": 3932, // Total amount of time passed in nocturnal awakenings. It is the total time passed in non-wake stage from sleep Onset to sleep offset (totalSleepTime - efficientSleepTime) - "sleepLatency": 1000, // Time to fall asleep [seconds] (sleepOnset - bedTime) "remOnset": 1602214232, // First REM epoch + + "sleepLatency": 1000, // Time to fall asleep [seconds] (sleepOnset - bedTime) "remLatency": 3852, // [seconds] (remOnset- bedTime) + + "sleepEfficiency": 0.8733, // Overall sense of how well the patient slept (totalSleepTime/bedTime) "awakenings": 7, // number of times the subject woke up between sleep onset & offset "stageShifts": 89, // number of times the subject transitionned from one stage to another between sleep onset & offset - "totalWTime": 3932, // [seconds] time passed in this stage between bedTime to wakeUpTime - "totalREMTime": 2370, - "totalN1Time": 3402, - "totalN2Time": 16032, - "totalN3Time": 5309 + + + "wakeAfterSleepOffset": 500, // [seconds] (wakeUpTime - sleepOffset) + "efficientSleepTime": 27113, // Total amount of seconds passed in non-wake stages + "WASO": 3932, // Total amount of time passed in nocturnal awakenings. It is the total time passed in non-wake stage from sleep Onset to sleep offset (totalSleepTime - efficientSleepTime) + "WTime": 3932, // [seconds] time passed in this stage between bedTime to wakeUpTime + "SleepTime": 31045, // Total amount of time sleeping including nocturnal awakenings (sleepOffset - sleepOnset) + "REMTime": 2370, + "N1Time": 3402, + "N2Time": 16032, + "N3Time": 5309 }, "epochs": { "timestamps": [ diff --git a/backend/classification/response.py b/backend/classification/response.py index b0214820..6217ad26 100644 --- a/backend/classification/response.py +++ b/backend/classification/response.py @@ -45,11 +45,43 @@ def subject(self): 'sex': self.sex.name, } + @property + def report(self): + return { + "sleepOnset": 1602211380, # Time at which the subject fell asleep(time of the first non - wake epoch) + # Time at which the subject woke up(time of the epoch after the last non - wake epoch) + "sleepOffset": 1602242425, + "remOnset": 1602214232, # First REM epoch + + "sleepLatency": 1000, # Time to fall asleep[seconds](sleepOnset - bedTime) + "remLatency": 3852, # [seconds](remOnset - bedTime) + + "sleepEfficiency": 0.8733, # Overall sense of how well the patient slept(totalSleepTime / bedTime) + "awakenings": 7, # number of times the subject woke up between sleep onset & offset + # number of times the subject transitionned from one stage to another between sleep onset & offset + "stageShifts": 89, + + + "wakeAfterSleepOffset": 500, # [seconds](wakeUpTime - sleepOffset) + "efficientSleepTime": 27113, # Total amount of seconds passed in non - wake stages + # Total amount of time passed in nocturnal awakenings. It is the total + # time passed in non - wake stage from sleep Onset to sleep + # offset(totalSleepTime - efficientSleepTime) + "WASO": 3932, + "WTime": 3932, # [seconds] time passed in this stage between bedTime to wakeUpTime + # Total amount of time sleeping including nocturnal awakenings(sleepOffset - sleepOnset) + "SleepTime": 31045, + "REMTime": 2370, + "N1Time": 3402, + "N2Time": 16032, + "N3Time": 5309 + } + @property def response(self): return { 'epochs': self.epochs, - 'report': None, + 'report': self.report, 'metadata': self.metadata, 'subject': self.subject, 'board': self.board.name, diff --git a/backend/requirements-dev.txt b/backend/requirements-dev.txt index 3e77229d..cf3afd74 100644 --- a/backend/requirements-dev.txt +++ b/backend/requirements-dev.txt @@ -1,2 +1,3 @@ hupper==1.10.2 pyinstaller==4.0 +pytest==6.1.2 diff --git a/backend/tests/test_response.py b/backend/tests/test_response.py new file mode 100644 index 00000000..7d9e13a5 --- /dev/null +++ b/backend/tests/test_response.py @@ -0,0 +1,51 @@ +class TestReportTimePassedInStage(): + """ + "wakeAfterSleepOffset": 500, // [seconds] (wakeUpTime - sleepOffset) + "efficientSleepTime": 27113, // Total amount of seconds passed in non-wake stages + "WASO": 3932, // Total amount of time passed in nocturnal awakenings. It is the total time passed in non-wake stage from sleep Onset to sleep offset (totalSleepTime - efficientSleepTime) + "WTime": 3932, // [seconds] time passed in this stage between bedTime to wakeUpTime + "SleepTime": 31045, // Total amount of time sleeping including nocturnal awakenings (sleepOffset - sleepOnset) + "REMTime": 2370, + "N1Time": 3402, + "N2Time": 16032, + "N3Time": 5309 + """ + + def test_null_time_passed_in_stage(self): + pass + + def test_complete_time_passed_in_stage(self): + pass + + def test_partial_time_passed_in_stage(self): + pass + + +class TestReportLatency(): + """ + "sleepLatency": 1000, // Time to fall asleep [seconds] (sleepOnset - bedTime) + "remLatency": 3852, // [seconds] (remOnset- bedTime) + """ + + def test_bla(self): + pass + + +class TestReportTimestamps(): + """ + "sleepOnset": 1602211380, // Time at which the subject fell asleep (time of the first non-wake epoch) + "sleepOffset": 1602242425, // Time at which the subject woke up (time of the epoch after the last non-wake epoch) + "remOnset": 1602214232, // First REM epoch + """ + + def test_bla(self): + pass + + +class TestReportMetrics(): + """ + "sleepEfficiency": 0.8733, // Overall sense of how well the patient slept (totalSleepTime/bedTime) + "awakenings": 7, // number of times the subject woke up between sleep onset & offset + "stageShifts": 89, // number of times the subject transitionned from one stage to another between sleep onset & offset + """ + pass From a63d07d95670938e9a868b06e658082ef7f46a79 Mon Sep 17 00:00:00 2001 From: Claudia Onorato Date: Mon, 9 Nov 2020 14:10:08 -0500 Subject: [PATCH 03/29] setup tests --- backend/classification/response.py | 4 +-- backend/tests/setup.py | 8 ++++++ backend/tests/test_response.py | 39 +++++++++++++++++++++++++++--- 3 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 backend/tests/setup.py diff --git a/backend/classification/response.py b/backend/classification/response.py index 6217ad26..762a37d7 100644 --- a/backend/classification/response.py +++ b/backend/classification/response.py @@ -46,7 +46,7 @@ def subject(self): } @property - def report(self): + def _report(self): return { "sleepOnset": 1602211380, # Time at which the subject fell asleep(time of the first non - wake epoch) # Time at which the subject woke up(time of the epoch after the last non - wake epoch) @@ -81,7 +81,7 @@ def report(self): def response(self): return { 'epochs': self.epochs, - 'report': self.report, + 'report': self._report, 'metadata': self.metadata, 'subject': self.subject, 'board': self.board.name, diff --git a/backend/tests/setup.py b/backend/tests/setup.py new file mode 100644 index 00000000..20044e5a --- /dev/null +++ b/backend/tests/setup.py @@ -0,0 +1,8 @@ + +def pytest_generate_tests(metafunc): + # called once per each test function + funcarglist = metafunc.cls.params[metafunc.function.__name__] + argnames = sorted(funcarglist[0]) + metafunc.parametrize( + argnames, [[funcargs[name] for name in argnames] for funcargs in funcarglist] + ) diff --git a/backend/tests/test_response.py b/backend/tests/test_response.py index 7d9e13a5..cb518b51 100644 --- a/backend/tests/test_response.py +++ b/backend/tests/test_response.py @@ -1,17 +1,43 @@ +from tests.setup import pytest_generate_tests # noqa: F401 + +from classification.config.constants import EPOCH_DURATION +from classification.response import ClassificationResponse +from classification.request import ClassificationRequest + +MOCK_REQUEST = ClassificationRequest + + class TestReportTimePassedInStage(): """ "wakeAfterSleepOffset": 500, // [seconds] (wakeUpTime - sleepOffset) "efficientSleepTime": 27113, // Total amount of seconds passed in non-wake stages "WASO": 3932, // Total amount of time passed in nocturnal awakenings. It is the total time passed in non-wake stage from sleep Onset to sleep offset (totalSleepTime - efficientSleepTime) - "WTime": 3932, // [seconds] time passed in this stage between bedTime to wakeUpTime "SleepTime": 31045, // Total amount of time sleeping including nocturnal awakenings (sleepOffset - sleepOnset) + + "WTime": 3932, // [seconds] time passed in this stage between bedTime to wakeUpTime "REMTime": 2370, "N1Time": 3402, "N2Time": 16032, "N3Time": 5309 """ + params = { + "test_null_time_passed_in_stage": [dict( + sequence=['W', 'W', 'W'], w_time=3 * EPOCH_DURATION, rem_time=0, n1_time=0, n2_time=0, n3_time=0, + ), dict( + sequence=['REM', 'REM', 'REM'], rem_time=3 * EPOCH_DURATION, w_time=0, n1_time=0, n2_time=0, n3_time=0, + ), dict( + sequence=['N1', 'N1', 'N1'], n1_time=3 * EPOCH_DURATION, w_time=0, rem_time=0, n2_time=0, n3_time=0, + ), dict( + sequence=['N2', 'N2', 'N2'], n2_time=3 * EPOCH_DURATION, w_time=0, rem_time=0, n1_time=0, n3_time=0, + ), dict( + sequence=['N3', 'N3', 'N3'], n3_time=3 * EPOCH_DURATION, w_time=0, rem_time=0, n1_time=0, n2_time=0, + )], + "test_complete_time_passed_in_stage": [dict()], + "test_partial_time_passed_in_stage": [dict()], + } - def test_null_time_passed_in_stage(self): + def test_null_time_passed_in_stage(self, sequence, w_time, rem_time, n1_time, n2_time, n3_time): + # ClassificationResponse(sequence, ) pass def test_complete_time_passed_in_stage(self): @@ -26,6 +52,9 @@ class TestReportLatency(): "sleepLatency": 1000, // Time to fall asleep [seconds] (sleepOnset - bedTime) "remLatency": 3852, // [seconds] (remOnset- bedTime) """ + params = { + "test_bla": [dict()] + } def test_bla(self): pass @@ -37,6 +66,9 @@ class TestReportTimestamps(): "sleepOffset": 1602242425, // Time at which the subject woke up (time of the epoch after the last non-wake epoch) "remOnset": 1602214232, // First REM epoch """ + params = { + "test_bla": [dict()] + } def test_bla(self): pass @@ -48,4 +80,5 @@ class TestReportMetrics(): "awakenings": 7, // number of times the subject woke up between sleep onset & offset "stageShifts": 89, // number of times the subject transitionned from one stage to another between sleep onset & offset """ - pass + params = { + } From 6fab7a9fdc6231e745b39fb955fd0044e1e0653b Mon Sep 17 00:00:00 2001 From: Claudia Onorato Date: Mon, 9 Nov 2020 16:54:47 -0500 Subject: [PATCH 04/29] moved server files not related to classification to backend package --- backend/app.py | 8 ++++---- backend/{classification => backend}/file_loading.py | 0 backend/{classification => backend}/request.py | 0 backend/{classification => backend}/response.py | 0 .../{classification => backend}/spectrogram_generator.py | 0 5 files changed, 4 insertions(+), 4 deletions(-) rename backend/{classification => backend}/file_loading.py (100%) rename backend/{classification => backend}/request.py (100%) rename backend/{classification => backend}/response.py (100%) rename backend/{classification => backend}/spectrogram_generator.py (100%) diff --git a/backend/app.py b/backend/app.py index e95ab14a..41ab9d22 100644 --- a/backend/app.py +++ b/backend/app.py @@ -3,14 +3,14 @@ from waitress import serve from http import HTTPStatus -from classification.file_loading import get_raw_array +from backend.file_loading import get_raw_array +from backend.request import ClassificationRequest +from backend.response import ClassificationResponse +from backend.spectrogram_generator import SpectrogramGenerator from classification.exceptions import ClassificationError from classification.config.constants import Sex, AcquisitionBoard, ALLOWED_FILE_EXTENSIONS from classification.model import SleepStagesClassifier -from classification.request import ClassificationRequest -from classification.response import ClassificationResponse from classification.features.preprocessing import preprocess -from classification.spectrogram_generator import SpectrogramGenerator app = Flask(__name__) sleep_stage_classifier = SleepStagesClassifier() diff --git a/backend/classification/file_loading.py b/backend/backend/file_loading.py similarity index 100% rename from backend/classification/file_loading.py rename to backend/backend/file_loading.py diff --git a/backend/classification/request.py b/backend/backend/request.py similarity index 100% rename from backend/classification/request.py rename to backend/backend/request.py diff --git a/backend/classification/response.py b/backend/backend/response.py similarity index 100% rename from backend/classification/response.py rename to backend/backend/response.py diff --git a/backend/classification/spectrogram_generator.py b/backend/backend/spectrogram_generator.py similarity index 100% rename from backend/classification/spectrogram_generator.py rename to backend/backend/spectrogram_generator.py From f52b5c1d5b9ce9a686185b03a3c8ad19f2f41d40 Mon Sep 17 00:00:00 2001 From: Claudia Onorato Date: Mon, 9 Nov 2020 21:10:49 -0500 Subject: [PATCH 05/29] added time passed in stage metrics --- backend/backend/request.py | 6 +- backend/backend/response.py | 4 +- backend/tests/test_response.py | 196 +++++++++++++++++++++++++++------ 3 files changed, 167 insertions(+), 39 deletions(-) diff --git a/backend/backend/request.py b/backend/backend/request.py index f0e51fae..ce253b71 100644 --- a/backend/backend/request.py +++ b/backend/backend/request.py @@ -12,16 +12,16 @@ class ClassificationRequest(): - def __init__(self, sex, age, stream_start, bedtime, wakeup, board, raw_eeg): + def __init__(self, sex, age, stream_start, bedtime, wakeup, board, raw_eeg, stream_duration=None): self.sex = sex self.age = age self.stream_start = stream_start self.bedtime = bedtime self.wakeup = wakeup self.board = board - - self.stream_duration = raw_eeg.times[-1] self.raw_eeg = raw_eeg + self.stream_duration = stream_duration if stream_duration else self.raw_eeg.times[-1] + ( + 1 / self.raw_eeg.info['sfreq']) self._validate() diff --git a/backend/backend/response.py b/backend/backend/response.py index 762a37d7..6217ad26 100644 --- a/backend/backend/response.py +++ b/backend/backend/response.py @@ -46,7 +46,7 @@ def subject(self): } @property - def _report(self): + def report(self): return { "sleepOnset": 1602211380, # Time at which the subject fell asleep(time of the first non - wake epoch) # Time at which the subject woke up(time of the epoch after the last non - wake epoch) @@ -81,7 +81,7 @@ def _report(self): def response(self): return { 'epochs': self.epochs, - 'report': self._report, + 'report': self.report, 'metadata': self.metadata, 'subject': self.subject, 'board': self.board.name, diff --git a/backend/tests/test_response.py b/backend/tests/test_response.py index cb518b51..3955bd23 100644 --- a/backend/tests/test_response.py +++ b/backend/tests/test_response.py @@ -1,19 +1,16 @@ -from tests.setup import pytest_generate_tests # noqa: F401 +from unittest.mock import patch -from classification.config.constants import EPOCH_DURATION -from classification.response import ClassificationResponse -from classification.request import ClassificationRequest +from tests.setup import pytest_generate_tests # noqa: F401 +from backend.response import ClassificationResponse +from backend.request import ClassificationRequest +from classification.config.constants import EPOCH_DURATION, SleepStage, Sex, AcquisitionBoard -MOCK_REQUEST = ClassificationRequest +SLEEP_STAGE_NAMES = [e.name for e in SleepStage] class TestReportTimePassedInStage(): - """ - "wakeAfterSleepOffset": 500, // [seconds] (wakeUpTime - sleepOffset) - "efficientSleepTime": 27113, // Total amount of seconds passed in non-wake stages - "WASO": 3932, // Total amount of time passed in nocturnal awakenings. It is the total time passed in non-wake stage from sleep Onset to sleep offset (totalSleepTime - efficientSleepTime) - "SleepTime": 31045, // Total amount of time sleeping including nocturnal awakenings (sleepOffset - sleepOnset) - + """Tests the time passed in each stage metrics in the response metrics + The evaluated metrics are: "WTime": 3932, // [seconds] time passed in this stage between bedTime to wakeUpTime "REMTime": 2370, "N1Time": 3402, @@ -21,30 +18,160 @@ class TestReportTimePassedInStage(): "N3Time": 5309 """ params = { - "test_null_time_passed_in_stage": [dict( - sequence=['W', 'W', 'W'], w_time=3 * EPOCH_DURATION, rem_time=0, n1_time=0, n2_time=0, n3_time=0, - ), dict( - sequence=['REM', 'REM', 'REM'], rem_time=3 * EPOCH_DURATION, w_time=0, n1_time=0, n2_time=0, n3_time=0, - ), dict( - sequence=['N1', 'N1', 'N1'], n1_time=3 * EPOCH_DURATION, w_time=0, rem_time=0, n2_time=0, n3_time=0, - ), dict( - sequence=['N2', 'N2', 'N2'], n2_time=3 * EPOCH_DURATION, w_time=0, rem_time=0, n1_time=0, n3_time=0, - ), dict( - sequence=['N3', 'N3', 'N3'], n3_time=3 * EPOCH_DURATION, w_time=0, rem_time=0, n1_time=0, n2_time=0, - )], - "test_complete_time_passed_in_stage": [dict()], - "test_partial_time_passed_in_stage": [dict()], + "test_null_time_passed_in_stage": [ + dict( + sequence=[ + 'W', + 'W', + 'W'], + WTime=3 * EPOCH_DURATION, + REMTime=0, + N1Time=0, + N2Time=0, + N3Time=0), + dict( + sequence=[ + 'REM', + 'REM', + 'REM'], + REMTime=3 * EPOCH_DURATION, + WTime=0, + N1Time=0, + N2Time=0, + N3Time=0), + dict( + sequence=[ + 'N1', + 'N1', + 'N1'], + N1Time=3 * EPOCH_DURATION, + WTime=0, + REMTime=0, + N2Time=0, + N3Time=0), + dict( + sequence=[ + 'N2', + 'N2', + 'N2'], + N2Time=3 * EPOCH_DURATION, + WTime=0, + REMTime=0, + N1Time=0, + N3Time=0), + dict( + sequence=[ + 'N3', + 'N3', + 'N3'], + N3Time=3 * EPOCH_DURATION, + WTime=0, + REMTime=0, + N1Time=0, + N2Time=0)], + "test_partial_time_passed_in_stage": [ + dict( + sequence=[ + 'W', + 'N1', + 'N2', + 'W'], + WTime=2 * EPOCH_DURATION, + REMTime=0, + N1Time=EPOCH_DURATION, + N2Time=EPOCH_DURATION, + N3Time=0), + dict( + sequence=[ + 'N1', + 'W', + 'W'], + WTime=2 * EPOCH_DURATION, + REMTime=0, + N1Time=EPOCH_DURATION, + N2Time=0, + N3Time=0), + dict( + sequence=[ + 'W', + 'N1', + 'N2', + 'N3', + 'REM', + 'W'], + WTime=2 * EPOCH_DURATION, + REMTime=EPOCH_DURATION, + N1Time=EPOCH_DURATION, + N2Time=EPOCH_DURATION, + N3Time=EPOCH_DURATION), + dict( + sequence=[ + 'N1', + 'N2', + 'N2', + 'REM'], + WTime=0, + REMTime=EPOCH_DURATION, + N1Time=EPOCH_DURATION, + N2Time=2 * EPOCH_DURATION, + N3Time=0), + ] } - def test_null_time_passed_in_stage(self, sequence, w_time, rem_time, n1_time, n2_time, n3_time): - # ClassificationResponse(sequence, ) - pass - - def test_complete_time_passed_in_stage(self): - pass - - def test_partial_time_passed_in_stage(self): - pass + @classmethod + def setup_class(cls): + """ setup any state specific to the execution of the given class (which + usually contains tests). + """ + with patch.object(ClassificationRequest, '_validate', lambda *x, **y: None): + cls.MOCK_REQUEST = ClassificationRequest( + sex=Sex.M, + age=22, + stream_start=1582418280, + bedtime=1582423980, + wakeup=1582452240, + board=AcquisitionBoard.OPENBCI_CYTON, + raw_eeg=None, + stream_duration=35760, + ) + + def test_null_time_passed_in_stage(self, sequence, WTime, REMTime, N1Time, N2Time, N3Time): + response = ClassificationResponse(self.MOCK_REQUEST, sequence, None) + report = response.report + + assert report[sequence[0].upper() + 'Time'] == len(sequence) * EPOCH_DURATION + self.assert_times(sequence, report, WTime, REMTime, N1Time, N2Time, N3Time) + + def test_partial_time_passed_in_stage(self, sequence, WTime, REMTime, N1Time, N2Time, N3Time): + response = ClassificationResponse(self.MOCK_REQUEST, sequence, None) + report = response.report + self.assert_times(sequence, report, WTime, REMTime, N1Time, N2Time, N3Time) + + def assert_times(self, sequence, report, WTime, REMTime, N1Time, N2Time, N3Time): + assert ( + report['WTime'] + + report['REMTime'] + + report['N1Time'] + + report['N2Time'] + + report['N3Time'] + ) == len(sequence) * EPOCH_DURATION + + assert report['WTime'] == WTime + assert report['REMTime'] == REMTime + assert report['N1Time'] == N1Time + assert report['N2Time'] == N2Time + assert report['N3Time'] == N3Time + + +class TestReportDurations(): + """ + "wakeAfterSleepOffset": 500, // [seconds] (wakeUpTime - sleepOffset) + "efficientSleepTime": 27113, // Total amount of seconds passed in non-wake stages + "WASO": 3932, // Total amount of time passed in nocturnal awakenings. It is the total time passed in non-wake stage + // from sleep Onset to sleep offset (totalSleepTime - efficientSleepTime) + "SleepTime": 31045, // Total amount of time sleeping including nocturnal awakenings (sleepOffset - sleepOnset) + """ + pass class TestReportLatency(): @@ -78,7 +205,8 @@ class TestReportMetrics(): """ "sleepEfficiency": 0.8733, // Overall sense of how well the patient slept (totalSleepTime/bedTime) "awakenings": 7, // number of times the subject woke up between sleep onset & offset - "stageShifts": 89, // number of times the subject transitionned from one stage to another between sleep onset & offset + "stageShifts": 89, // number of times the subject transitionned + // from one stage to another between sleep onset & offset """ params = { } From 070e99af5b2958543523018c1ecf2fd907705647 Mon Sep 17 00:00:00 2001 From: Claudia Onorato Date: Mon, 9 Nov 2020 21:15:38 -0500 Subject: [PATCH 06/29] added time passed in each sleep stages tests --- backend/tests/setup.py | 21 +++++++++++++++++++++ backend/tests/test_response.py | 29 +++++++++-------------------- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/backend/tests/setup.py b/backend/tests/setup.py index 20044e5a..26caabcc 100644 --- a/backend/tests/setup.py +++ b/backend/tests/setup.py @@ -1,3 +1,8 @@ +from unittest.mock import patch + +from backend.request import ClassificationRequest +from classification.config.constants import Sex, AcquisitionBoard + def pytest_generate_tests(metafunc): # called once per each test function @@ -6,3 +11,19 @@ def pytest_generate_tests(metafunc): metafunc.parametrize( argnames, [[funcargs[name] for name in argnames] for funcargs in funcarglist] ) + + +def get_mock_request(): + with patch.object(ClassificationRequest, '_validate', lambda *x, **y: None): + mock_request = ClassificationRequest( + sex=Sex.M, + age=22, + stream_start=1582418280, + bedtime=1582423980, + wakeup=1582452240, + board=AcquisitionBoard.OPENBCI_CYTON, + raw_eeg=None, + stream_duration=35760, + ) + + return mock_request diff --git a/backend/tests/test_response.py b/backend/tests/test_response.py index 3955bd23..351a36d4 100644 --- a/backend/tests/test_response.py +++ b/backend/tests/test_response.py @@ -1,9 +1,8 @@ -from unittest.mock import patch from tests.setup import pytest_generate_tests # noqa: F401 +from tests.setup import get_mock_request from backend.response import ClassificationResponse -from backend.request import ClassificationRequest -from classification.config.constants import EPOCH_DURATION, SleepStage, Sex, AcquisitionBoard +from classification.config.constants import EPOCH_DURATION, SleepStage SLEEP_STAGE_NAMES = [e.name for e in SleepStage] @@ -34,8 +33,8 @@ class TestReportTimePassedInStage(): 'REM', 'REM', 'REM'], - REMTime=3 * EPOCH_DURATION, WTime=0, + REMTime=3 * EPOCH_DURATION, N1Time=0, N2Time=0, N3Time=0), @@ -44,9 +43,9 @@ class TestReportTimePassedInStage(): 'N1', 'N1', 'N1'], - N1Time=3 * EPOCH_DURATION, WTime=0, REMTime=0, + N1Time=3 * EPOCH_DURATION, N2Time=0, N3Time=0), dict( @@ -54,22 +53,22 @@ class TestReportTimePassedInStage(): 'N2', 'N2', 'N2'], - N2Time=3 * EPOCH_DURATION, WTime=0, REMTime=0, N1Time=0, + N2Time=3 * EPOCH_DURATION, N3Time=0), dict( sequence=[ 'N3', 'N3', 'N3'], - N3Time=3 * EPOCH_DURATION, WTime=0, REMTime=0, N1Time=0, - N2Time=0)], - "test_partial_time_passed_in_stage": [ + N2Time=0, + N3Time=3 * EPOCH_DURATION), + ], "test_partial_time_passed_in_stage": [ dict( sequence=[ 'W', @@ -123,17 +122,7 @@ def setup_class(cls): """ setup any state specific to the execution of the given class (which usually contains tests). """ - with patch.object(ClassificationRequest, '_validate', lambda *x, **y: None): - cls.MOCK_REQUEST = ClassificationRequest( - sex=Sex.M, - age=22, - stream_start=1582418280, - bedtime=1582423980, - wakeup=1582452240, - board=AcquisitionBoard.OPENBCI_CYTON, - raw_eeg=None, - stream_duration=35760, - ) + cls.MOCK_REQUEST = get_mock_request() def test_null_time_passed_in_stage(self, sequence, WTime, REMTime, N1Time, N2Time, N3Time): response = ClassificationResponse(self.MOCK_REQUEST, sequence, None) From 3babe483f158ddfedaf3daa9576ac0368cd9abca Mon Sep 17 00:00:00 2001 From: Claudia Onorato Date: Mon, 9 Nov 2020 22:29:50 -0500 Subject: [PATCH 07/29] tests pass for time passed in stage --- backend/backend/response.py | 11 +-- backend/classification/config/constants.py | 4 + backend/metric/time_passed_in_stage.py | 18 ++++ backend/tests/test_response.py | 97 +++++++++++----------- 4 files changed, 75 insertions(+), 55 deletions(-) create mode 100644 backend/metric/time_passed_in_stage.py diff --git a/backend/backend/response.py b/backend/backend/response.py index 6217ad26..57e78fe8 100644 --- a/backend/backend/response.py +++ b/backend/backend/response.py @@ -2,6 +2,8 @@ from classification.config.constants import EPOCH_DURATION, SleepStage +from metric.time_passed_in_stage import get_time_passed_in_stage + class ClassificationResponse(): def __init__(self, request, predictions, spectrogram): @@ -19,7 +21,7 @@ def __init__(self, request, predictions, spectrogram): @property def sleep_stages(self): - ordered_sleep_stage_names = np.array([SleepStage(stage_index).name for stage_index in range(len(SleepStage))]) + ordered_sleep_stage_names = np.array(SleepStage.tolist()) return ordered_sleep_stage_names[self.predictions] @property @@ -68,13 +70,8 @@ def report(self): # time passed in non - wake stage from sleep Onset to sleep # offset(totalSleepTime - efficientSleepTime) "WASO": 3932, - "WTime": 3932, # [seconds] time passed in this stage between bedTime to wakeUpTime - # Total amount of time sleeping including nocturnal awakenings(sleepOffset - sleepOnset) "SleepTime": 31045, - "REMTime": 2370, - "N1Time": 3402, - "N2Time": 16032, - "N3Time": 5309 + **get_time_passed_in_stage(self.sleep_stages), } @property diff --git a/backend/classification/config/constants.py b/backend/classification/config/constants.py index c9619bc5..bee515b1 100644 --- a/backend/classification/config/constants.py +++ b/backend/classification/config/constants.py @@ -20,6 +20,10 @@ class SleepStage(Enum): N3 = 3 REM = 4 + @staticmethod + def tolist(): + return [e.name for e in SleepStage] + class HiddenMarkovModelProbability(Enum): emission = auto() diff --git a/backend/metric/time_passed_in_stage.py b/backend/metric/time_passed_in_stage.py new file mode 100644 index 00000000..282c126a --- /dev/null +++ b/backend/metric/time_passed_in_stage.py @@ -0,0 +1,18 @@ +from collections import Counter +from classification.config.constants import SleepStage, EPOCH_DURATION + + +def get_time_passed_in_stage(sequence): + """Calculates time passed in each stage for all of the sequence + Input: + - sequence: list or np.array of the SleepStage labels + """ + nb_epoch_passed_by_stage = Counter(sequence) + + def get_time_passed(stage): + return EPOCH_DURATION * nb_epoch_passed_by_stage[stage] if stage in nb_epoch_passed_by_stage else 0 + + return { + f"{stage.upper()}Time": get_time_passed(stage) + for stage in SleepStage.tolist() + } diff --git a/backend/tests/test_response.py b/backend/tests/test_response.py index 351a36d4..51124340 100644 --- a/backend/tests/test_response.py +++ b/backend/tests/test_response.py @@ -1,11 +1,10 @@ +import numpy as np from tests.setup import pytest_generate_tests # noqa: F401 from tests.setup import get_mock_request from backend.response import ClassificationResponse from classification.config.constants import EPOCH_DURATION, SleepStage -SLEEP_STAGE_NAMES = [e.name for e in SleepStage] - class TestReportTimePassedInStage(): """Tests the time passed in each stage metrics in the response metrics @@ -19,50 +18,35 @@ class TestReportTimePassedInStage(): params = { "test_null_time_passed_in_stage": [ dict( - sequence=[ - 'W', - 'W', - 'W'], + sequence=['W', 'W', 'W'], WTime=3 * EPOCH_DURATION, REMTime=0, N1Time=0, N2Time=0, N3Time=0), dict( - sequence=[ - 'REM', - 'REM', - 'REM'], + sequence=['REM', 'REM', 'REM'], WTime=0, REMTime=3 * EPOCH_DURATION, N1Time=0, N2Time=0, N3Time=0), dict( - sequence=[ - 'N1', - 'N1', - 'N1'], + sequence=['N1', 'N1', 'N1'], WTime=0, REMTime=0, N1Time=3 * EPOCH_DURATION, N2Time=0, N3Time=0), dict( - sequence=[ - 'N2', - 'N2', - 'N2'], + sequence=['N2', 'N2', 'N2'], WTime=0, REMTime=0, N1Time=0, N2Time=3 * EPOCH_DURATION, N3Time=0), dict( - sequence=[ - 'N3', - 'N3', - 'N3'], + sequence=['N3', 'N3', 'N3'], WTime=0, REMTime=0, N1Time=0, @@ -70,45 +54,28 @@ class TestReportTimePassedInStage(): N3Time=3 * EPOCH_DURATION), ], "test_partial_time_passed_in_stage": [ dict( - sequence=[ - 'W', - 'N1', - 'N2', - 'W'], + sequence=['W', 'N1', 'N2', 'W'], WTime=2 * EPOCH_DURATION, REMTime=0, N1Time=EPOCH_DURATION, N2Time=EPOCH_DURATION, N3Time=0), dict( - sequence=[ - 'N1', - 'W', - 'W'], + sequence=['N1', 'W', 'W'], WTime=2 * EPOCH_DURATION, REMTime=0, N1Time=EPOCH_DURATION, N2Time=0, N3Time=0), dict( - sequence=[ - 'W', - 'N1', - 'N2', - 'N3', - 'REM', - 'W'], + sequence=['W', 'N1', 'N2', 'N3', 'REM', 'W'], WTime=2 * EPOCH_DURATION, REMTime=EPOCH_DURATION, N1Time=EPOCH_DURATION, N2Time=EPOCH_DURATION, N3Time=EPOCH_DURATION), dict( - sequence=[ - 'N1', - 'N2', - 'N2', - 'REM'], + sequence=['N1', 'N2', 'N2', 'REM'], WTime=0, REMTime=EPOCH_DURATION, N1Time=EPOCH_DURATION, @@ -119,23 +86,25 @@ class TestReportTimePassedInStage(): @classmethod def setup_class(cls): - """ setup any state specific to the execution of the given class (which - usually contains tests). - """ cls.MOCK_REQUEST = get_mock_request() def test_null_time_passed_in_stage(self, sequence, WTime, REMTime, N1Time, N2Time, N3Time): - response = ClassificationResponse(self.MOCK_REQUEST, sequence, None) + value_sequence = self.convert_sleep_stage_name_to_values(sequence) + response = ClassificationResponse(self.MOCK_REQUEST, value_sequence, None) report = response.report assert report[sequence[0].upper() + 'Time'] == len(sequence) * EPOCH_DURATION self.assert_times(sequence, report, WTime, REMTime, N1Time, N2Time, N3Time) def test_partial_time_passed_in_stage(self, sequence, WTime, REMTime, N1Time, N2Time, N3Time): + sequence = self.convert_sleep_stage_name_to_values(sequence) response = ClassificationResponse(self.MOCK_REQUEST, sequence, None) report = response.report self.assert_times(sequence, report, WTime, REMTime, N1Time, N2Time, N3Time) + def convert_sleep_stage_name_to_values(self, sequence): + return np.array([SleepStage[stage].value for stage in sequence]) + def assert_times(self, sequence, report, WTime, REMTime, N1Time, N2Time, N3Time): assert ( report['WTime'] @@ -160,7 +129,39 @@ class TestReportDurations(): // from sleep Onset to sleep offset (totalSleepTime - efficientSleepTime) "SleepTime": 31045, // Total amount of time sleeping including nocturnal awakenings (sleepOffset - sleepOnset) """ - pass + params = { + "test_wake_": [ + dict( + sequence=['W', 'W', 'W'], + wakeAfterSleepOffset=0, + efficientSleepTime=0, + WASO=0, + SleepTime=0, + ), dict( + sequence=['REM', 'REM', 'REM'], + wakeAfterSleepOffset=0, + efficientSleepTime=0, + WASO=0, + SleepTime=0, + ), dict( + sequence=['REM', 'REM', 'REM'], + wakeAfterSleepOffset=0, + efficientSleepTime=0, + WASO=0, + SleepTime=0, + ), dict( + sequence=['REM', 'REM', 'REM'], + wakeAfterSleepOffset=0, + efficientSleepTime=0, + WASO=0, + SleepTime=0, + ), + ], + } + + @ classmethod + def setup_class(cls): + cls.MOCK_REQUEST = get_mock_request() class TestReportLatency(): From d4a7808db5e2f43608e42f76472191b12be92bc0 Mon Sep 17 00:00:00 2001 From: Claudia Onorato Date: Mon, 9 Nov 2020 23:26:02 -0500 Subject: [PATCH 08/29] added latency tests --- backend/tests/test_response.py | 109 ++++++++++++++++++++++++++++++--- 1 file changed, 100 insertions(+), 9 deletions(-) diff --git a/backend/tests/test_response.py b/backend/tests/test_response.py index 51124340..3a29d075 100644 --- a/backend/tests/test_response.py +++ b/backend/tests/test_response.py @@ -6,6 +6,10 @@ from classification.config.constants import EPOCH_DURATION, SleepStage +def convert_sleep_stage_name_to_values(sequence): + return np.array([SleepStage[stage].value for stage in sequence]) + + class TestReportTimePassedInStage(): """Tests the time passed in each stage metrics in the response metrics The evaluated metrics are: @@ -89,7 +93,7 @@ def setup_class(cls): cls.MOCK_REQUEST = get_mock_request() def test_null_time_passed_in_stage(self, sequence, WTime, REMTime, N1Time, N2Time, N3Time): - value_sequence = self.convert_sleep_stage_name_to_values(sequence) + value_sequence = convert_sleep_stage_name_to_values(sequence) response = ClassificationResponse(self.MOCK_REQUEST, value_sequence, None) report = response.report @@ -97,14 +101,11 @@ def test_null_time_passed_in_stage(self, sequence, WTime, REMTime, N1Time, N2Tim self.assert_times(sequence, report, WTime, REMTime, N1Time, N2Time, N3Time) def test_partial_time_passed_in_stage(self, sequence, WTime, REMTime, N1Time, N2Time, N3Time): - sequence = self.convert_sleep_stage_name_to_values(sequence) + sequence = convert_sleep_stage_name_to_values(sequence) response = ClassificationResponse(self.MOCK_REQUEST, sequence, None) report = response.report self.assert_times(sequence, report, WTime, REMTime, N1Time, N2Time, N3Time) - def convert_sleep_stage_name_to_values(self, sequence): - return np.array([SleepStage[stage].value for stage in sequence]) - def assert_times(self, sequence, report, WTime, REMTime, N1Time, N2Time, N3Time): assert ( report['WTime'] @@ -165,16 +166,106 @@ def setup_class(cls): class TestReportLatency(): - """ + """Tests the time it took to enter a specific stage + The evaluated metrics are: "sleepLatency": 1000, // Time to fall asleep [seconds] (sleepOnset - bedTime) "remLatency": 3852, // [seconds] (remOnset- bedTime) """ params = { - "test_bla": [dict()] + "test_sequence_starts_with_stage": [ + dict( + sequence=['REM', 'REM', 'W', 'W'], + test_rem=True, + ), dict( + sequence=['REM', 'W', 'N1', 'W'], + test_rem=True, + ), dict( + sequence=['REM', 'W', 'N1', 'N2', 'N3', 'REM', 'W'], + test_rem=False, + ), dict( + sequence=['N1', 'W', 'N1', 'N2', 'N3', 'REM', 'W'], + test_rem=False, + ), dict( + sequence=['N2', 'W', 'N1', 'N2', 'N3', 'REM', 'W'], + test_rem=False, + ), dict( + sequence=['N3', 'W', 'N1', 'N2', 'N3', 'REM', 'W'], + test_rem=False, + ), + ], "test_sequence_has_no_stage": [ + dict( + sequence=['W', 'N1', 'N2', 'N3', 'W'], + test_rem=True, + ), dict( + sequence=['W', 'W', 'W', 'W', 'W'], + test_rem=False, + ), dict( + sequence=['W', ], + test_rem=False, + ), + ], "test_sequence_ends_with_stage": [ + dict( + sequence=['W', 'W', 'REM'], + test_rem=True, + ), dict( + sequence=['W', 'W', 'N1'], + test_rem=False, + ), dict( + sequence=['W', 'W', 'N2'], + test_rem=False, + ), dict( + sequence=['W', 'W', 'N3'], + test_rem=False, + ), + ], "test_sequence_with_stage_at_middle": [ + dict( + sequence=['W', 'N1', 'N2', 'N1', 'REM', 'W'], + test_rem=True, + latency=4 * EPOCH_DURATION, + ), dict( + sequence=['W', 'W', 'N1', 'W', 'N1', 'W'], + test_rem=False, + latency=2 * EPOCH_DURATION, + ), dict( + sequence=['W', 'W', 'N2', 'W', 'N2', 'W'], + test_rem=False, + latency=2 * EPOCH_DURATION, + ), dict( + sequence=['W', 'W', 'N3', 'W', 'N3', 'W'], + test_rem=False, + latency=2 * EPOCH_DURATION, + ), + ], } - def test_bla(self): - pass + @classmethod + def setup_class(cls): + cls.MOCK_REQUEST = get_mock_request() + + def get_report_key(self, test_rem): + return 'remLatency' if test_rem else 'sleepLatency' + + def test_sequence_starts_with_stage(self, sequence, test_rem): + expected_latency = 0 + self.assert_latency_equals_expected(expected_latency, sequence, test_rem) + + def test_sequence_has_no_stage(self, sequence, test_rem): + expected_latency = -1 + self.assert_latency_equals_expected(expected_latency, sequence, test_rem) + + def test_sequence_ends_with_stage(self, sequence, test_rem): + expected_latency = EPOCH_DURATION * (len(sequence) - 1) + self.assert_latency_equals_expected(expected_latency, sequence, test_rem) + + def test_sequence_with_stage_at_middle(self, sequence, test_rem, latency): + self.assert_latency_equals_expected(latency, sequence, test_rem) + + def assert_latency_equals_expected(self, expected, sequence, test_rem): + sequence = convert_sleep_stage_name_to_values(sequence) + response = ClassificationResponse(self.MOCK_REQUEST, sequence, None) + report = response.report + + assert report[self.get_report_key(test_rem)] == expected class TestReportTimestamps(): From 7db5f061fdecb1a82a0eec95fe83c58e36a1e315 Mon Sep 17 00:00:00 2001 From: Claudia Onorato Date: Mon, 9 Nov 2020 23:47:35 -0500 Subject: [PATCH 09/29] latency tests passes --- backend/backend/response.py | 4 ++-- backend/metric/latency.py | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 backend/metric/latency.py diff --git a/backend/backend/response.py b/backend/backend/response.py index 57e78fe8..f974a83c 100644 --- a/backend/backend/response.py +++ b/backend/backend/response.py @@ -3,6 +3,7 @@ from classification.config.constants import EPOCH_DURATION, SleepStage from metric.time_passed_in_stage import get_time_passed_in_stage +from metric.latency import get_latencies class ClassificationResponse(): @@ -55,8 +56,7 @@ def report(self): "sleepOffset": 1602242425, "remOnset": 1602214232, # First REM epoch - "sleepLatency": 1000, # Time to fall asleep[seconds](sleepOnset - bedTime) - "remLatency": 3852, # [seconds](remOnset - bedTime) + **get_latencies(self.sleep_stages), "sleepEfficiency": 0.8733, # Overall sense of how well the patient slept(totalSleepTime / bedTime) "awakenings": 7, # number of times the subject woke up between sleep onset & offset diff --git a/backend/metric/latency.py b/backend/metric/latency.py new file mode 100644 index 00000000..eb5aa249 --- /dev/null +++ b/backend/metric/latency.py @@ -0,0 +1,19 @@ +import numpy as np + +from classification.config.constants import SleepStage, EPOCH_DURATION + + +def get_latencies(sequence): + """Tests the time it took to enter a specific stage + Input: + - sequence: np.array of the SleepStage labels + """ + return { + "sleepLatency": _get_latency_of_stage(sequence != SleepStage.W.name), + "remLatency": _get_latency_of_stage(sequence == SleepStage.REM.name), + } + + +def _get_latency_of_stage(sequence_is_stage): + epochs_of_stage_of_interest = np.where(sequence_is_stage)[0] + return -1 if epochs_of_stage_of_interest.shape[0] == 0 else epochs_of_stage_of_interest[0] * EPOCH_DURATION From 2402eb0b8d61ed4019415aebb7b3c77063abd370 Mon Sep 17 00:00:00 2001 From: Claudia Onorato Date: Mon, 9 Nov 2020 23:47:46 -0500 Subject: [PATCH 10/29] added tests to backend CI --- .github/workflows/backend.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index d0a77126..d7bed79d 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -26,6 +26,7 @@ jobs: with: python-version: 3.8 - run: python -m pip install -r requirements.txt -r requirements-dev.txt + - run: python -m pytest - run: python -m PyInstaller --onefile app.py - uses: actions/upload-artifact@v2 with: From e40a02ba44e6a3b18fb811516e49aae494ae42d5 Mon Sep 17 00:00:00 2001 From: Claudia Onorato Date: Tue, 10 Nov 2020 00:02:49 -0500 Subject: [PATCH 11/29] added onset tests --- backend/metric/latency.py | 2 +- backend/tests/test_response.py | 31 ++++++++++++++----------------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/backend/metric/latency.py b/backend/metric/latency.py index eb5aa249..86cdb7dd 100644 --- a/backend/metric/latency.py +++ b/backend/metric/latency.py @@ -16,4 +16,4 @@ def get_latencies(sequence): def _get_latency_of_stage(sequence_is_stage): epochs_of_stage_of_interest = np.where(sequence_is_stage)[0] - return -1 if epochs_of_stage_of_interest.shape[0] == 0 else epochs_of_stage_of_interest[0] * EPOCH_DURATION + return int(-1 if epochs_of_stage_of_interest.shape[0] == 0 else epochs_of_stage_of_interest[0] * EPOCH_DURATION) diff --git a/backend/tests/test_response.py b/backend/tests/test_response.py index 3a29d075..fc60390f 100644 --- a/backend/tests/test_response.py +++ b/backend/tests/test_response.py @@ -165,12 +165,19 @@ def setup_class(cls): cls.MOCK_REQUEST = get_mock_request() -class TestReportLatency(): - """Tests the time it took to enter a specific stage +class TestReportLatenciesOnset(): + """Tests the event-related latencies and onsets The evaluated metrics are: "sleepLatency": 1000, // Time to fall asleep [seconds] (sleepOnset - bedTime) "remLatency": 3852, // [seconds] (remOnset- bedTime) + + "sleepOnset": 1602211380, // Time at which the subject fell asleep (time of the first non-wake epoch) + "remOnset": 1602214232, // First REM epoch + + NOT TESTED: + "sleepOffset": 1602242425, // Time at which the subject woke up (time of the epoch after the last non-wake epoch) """ + params = { "test_sequence_starts_with_stage": [ dict( @@ -265,21 +272,11 @@ def assert_latency_equals_expected(self, expected, sequence, test_rem): response = ClassificationResponse(self.MOCK_REQUEST, sequence, None) report = response.report - assert report[self.get_report_key(test_rem)] == expected - - -class TestReportTimestamps(): - """ - "sleepOnset": 1602211380, // Time at which the subject fell asleep (time of the first non-wake epoch) - "sleepOffset": 1602242425, // Time at which the subject woke up (time of the epoch after the last non-wake epoch) - "remOnset": 1602214232, // First REM epoch - """ - params = { - "test_bla": [dict()] - } - - def test_bla(self): - pass + assert report[self.get_report_key( + test_rem)] == expected, f"Latency of {'rem' if test_rem else 'sleep'} is not as expected" + assert report[self.get_report_key(test_rem)] == expected + self.MOCK_REQUEST.bedtime, ( + f"Onset of {'rem' if test_rem else 'sleep'} is not as expected" + ) class TestReportMetrics(): From 2f6363b10574dd203b871619a66819e16e53fe8c Mon Sep 17 00:00:00 2001 From: Claudia Onorato Date: Tue, 10 Nov 2020 00:24:32 -0500 Subject: [PATCH 12/29] completed onsets --- backend/backend/response.py | 21 +++++++++++++++------ backend/tests/test_response.py | 28 ++++++++++++++++++---------- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/backend/backend/response.py b/backend/backend/response.py index f974a83c..c5a79869 100644 --- a/backend/backend/response.py +++ b/backend/backend/response.py @@ -50,20 +50,30 @@ def subject(self): @property def report(self): + latencies = get_latencies(self.sleep_stages) + time_passed_in_stage = get_time_passed_in_stage(self.sleep_stages) + onsets = { + "sleepOnset": ( + latencies['sleepLatency'] if latencies['sleepLatency'] >= 0 else 0 + ) + self.bedtime, + "remOnset": ( + latencies['remLatency'] if latencies['remLatency'] >= 0 else 0 + ) + self.bedtime + } + return { - "sleepOnset": 1602211380, # Time at which the subject fell asleep(time of the first non - wake epoch) + **latencies, + **time_passed_in_stage, + **onsets, + # Time at which the subject woke up(time of the epoch after the last non - wake epoch) "sleepOffset": 1602242425, - "remOnset": 1602214232, # First REM epoch - - **get_latencies(self.sleep_stages), "sleepEfficiency": 0.8733, # Overall sense of how well the patient slept(totalSleepTime / bedTime) "awakenings": 7, # number of times the subject woke up between sleep onset & offset # number of times the subject transitionned from one stage to another between sleep onset & offset "stageShifts": 89, - "wakeAfterSleepOffset": 500, # [seconds](wakeUpTime - sleepOffset) "efficientSleepTime": 27113, # Total amount of seconds passed in non - wake stages # Total amount of time passed in nocturnal awakenings. It is the total @@ -71,7 +81,6 @@ def report(self): # offset(totalSleepTime - efficientSleepTime) "WASO": 3932, "SleepTime": 31045, - **get_time_passed_in_stage(self.sleep_stages), } @property diff --git a/backend/tests/test_response.py b/backend/tests/test_response.py index fc60390f..11aa30c0 100644 --- a/backend/tests/test_response.py +++ b/backend/tests/test_response.py @@ -249,32 +249,40 @@ class TestReportLatenciesOnset(): def setup_class(cls): cls.MOCK_REQUEST = get_mock_request() - def get_report_key(self, test_rem): + def get_latency_report_key(self, test_rem): return 'remLatency' if test_rem else 'sleepLatency' + def get_onset_report_key(self, test_rem): + return 'remOnset' if test_rem else 'sleepOnset' + def test_sequence_starts_with_stage(self, sequence, test_rem): expected_latency = 0 - self.assert_latency_equals_expected(expected_latency, sequence, test_rem) + expected_onset = self.MOCK_REQUEST.bedtime + self.assert_latency_equals_expected(expected_latency, expected_onset, sequence, test_rem) def test_sequence_has_no_stage(self, sequence, test_rem): expected_latency = -1 - self.assert_latency_equals_expected(expected_latency, sequence, test_rem) + expected_onset = self.MOCK_REQUEST.bedtime + self.assert_latency_equals_expected(expected_latency, expected_onset, sequence, test_rem) def test_sequence_ends_with_stage(self, sequence, test_rem): expected_latency = EPOCH_DURATION * (len(sequence) - 1) - self.assert_latency_equals_expected(expected_latency, sequence, test_rem) + expected_onset = expected_latency + self.MOCK_REQUEST.bedtime + self.assert_latency_equals_expected(expected_latency, expected_onset, sequence, test_rem) def test_sequence_with_stage_at_middle(self, sequence, test_rem, latency): - self.assert_latency_equals_expected(latency, sequence, test_rem) + expected_onset = latency + self.MOCK_REQUEST.bedtime + self.assert_latency_equals_expected(latency, expected_onset, sequence, test_rem) - def assert_latency_equals_expected(self, expected, sequence, test_rem): + def assert_latency_equals_expected(self, expected_latency, expected_onset, sequence, test_rem): sequence = convert_sleep_stage_name_to_values(sequence) - response = ClassificationResponse(self.MOCK_REQUEST, sequence, None) + response = ClassificationResponse(TestReportLatenciesOnset.MOCK_REQUEST, sequence, None) report = response.report - assert report[self.get_report_key( - test_rem)] == expected, f"Latency of {'rem' if test_rem else 'sleep'} is not as expected" - assert report[self.get_report_key(test_rem)] == expected + self.MOCK_REQUEST.bedtime, ( + assert report[self.get_latency_report_key(test_rem)] == expected_latency, ( + f"Latency of {'rem' if test_rem else 'sleep'} is not as expected" + ) + assert report[self.get_onset_report_key(test_rem)] == expected_onset, ( f"Onset of {'rem' if test_rem else 'sleep'} is not as expected" ) From c7811fa9e569433d63987110144d46c2571fc75f Mon Sep 17 00:00:00 2001 From: Claudia Onorato Date: Tue, 10 Nov 2020 02:15:54 -0500 Subject: [PATCH 13/29] added sleep offset --- backend/backend/response.py | 5 +- backend/metric/offset.py | 11 +++ backend/tests/test_response.py | 130 +++++++++++++++++++-------------- 3 files changed, 90 insertions(+), 56 deletions(-) create mode 100644 backend/metric/offset.py diff --git a/backend/backend/response.py b/backend/backend/response.py index c5a79869..ebadbd2e 100644 --- a/backend/backend/response.py +++ b/backend/backend/response.py @@ -4,6 +4,7 @@ from metric.time_passed_in_stage import get_time_passed_in_stage from metric.latency import get_latencies +from metric.offset import get_sleep_offset class ClassificationResponse(): @@ -65,9 +66,7 @@ def report(self): **latencies, **time_passed_in_stage, **onsets, - - # Time at which the subject woke up(time of the epoch after the last non - wake epoch) - "sleepOffset": 1602242425, + **get_sleep_offset(self.sleep_stages, self.bedtime), "sleepEfficiency": 0.8733, # Overall sense of how well the patient slept(totalSleepTime / bedTime) "awakenings": 7, # number of times the subject woke up between sleep onset & offset diff --git a/backend/metric/offset.py b/backend/metric/offset.py new file mode 100644 index 00000000..519078dc --- /dev/null +++ b/backend/metric/offset.py @@ -0,0 +1,11 @@ +import numpy as np + +from classification.config.constants import SleepStage, EPOCH_DURATION + + +def get_sleep_offset(sequence, bedtime): + sleep_indexes = np.where(sequence != SleepStage.W.name)[0] + sleep_nb_epochs = (sleep_indexes[-1] + 1) if len(sleep_indexes) else len(sequence) + + # Time at which the subject woke up(time of the epoch after the last non - wake epoch) + return {'sleepOffset': sleep_nb_epochs * EPOCH_DURATION + bedtime} diff --git a/backend/tests/test_response.py b/backend/tests/test_response.py index 11aa30c0..c266c087 100644 --- a/backend/tests/test_response.py +++ b/backend/tests/test_response.py @@ -1,3 +1,10 @@ +""" +Not tested as they seemed obvious: +- "SleepTime": 31045, // Total amount of time sleeping including nocturnal awakenings (sleepOffset - sleepOnset) +- "WASO": 3932, // Total amount of time passed in nocturnal awakenings. It is the total time passed in non-wake stage + // from sleep Onset to sleep offset (totalSleepTime - efficientSleepTime) +""" + import numpy as np from tests.setup import pytest_generate_tests # noqa: F401 @@ -122,49 +129,6 @@ def assert_times(self, sequence, report, WTime, REMTime, N1Time, N2Time, N3Time) assert report['N3Time'] == N3Time -class TestReportDurations(): - """ - "wakeAfterSleepOffset": 500, // [seconds] (wakeUpTime - sleepOffset) - "efficientSleepTime": 27113, // Total amount of seconds passed in non-wake stages - "WASO": 3932, // Total amount of time passed in nocturnal awakenings. It is the total time passed in non-wake stage - // from sleep Onset to sleep offset (totalSleepTime - efficientSleepTime) - "SleepTime": 31045, // Total amount of time sleeping including nocturnal awakenings (sleepOffset - sleepOnset) - """ - params = { - "test_wake_": [ - dict( - sequence=['W', 'W', 'W'], - wakeAfterSleepOffset=0, - efficientSleepTime=0, - WASO=0, - SleepTime=0, - ), dict( - sequence=['REM', 'REM', 'REM'], - wakeAfterSleepOffset=0, - efficientSleepTime=0, - WASO=0, - SleepTime=0, - ), dict( - sequence=['REM', 'REM', 'REM'], - wakeAfterSleepOffset=0, - efficientSleepTime=0, - WASO=0, - SleepTime=0, - ), dict( - sequence=['REM', 'REM', 'REM'], - wakeAfterSleepOffset=0, - efficientSleepTime=0, - WASO=0, - SleepTime=0, - ), - ], - } - - @ classmethod - def setup_class(cls): - cls.MOCK_REQUEST = get_mock_request() - - class TestReportLatenciesOnset(): """Tests the event-related latencies and onsets The evaluated metrics are: @@ -173,9 +137,6 @@ class TestReportLatenciesOnset(): "sleepOnset": 1602211380, // Time at which the subject fell asleep (time of the first non-wake epoch) "remOnset": 1602214232, // First REM epoch - - NOT TESTED: - "sleepOffset": 1602242425, // Time at which the subject woke up (time of the epoch after the last non-wake epoch) """ params = { @@ -249,12 +210,6 @@ class TestReportLatenciesOnset(): def setup_class(cls): cls.MOCK_REQUEST = get_mock_request() - def get_latency_report_key(self, test_rem): - return 'remLatency' if test_rem else 'sleepLatency' - - def get_onset_report_key(self, test_rem): - return 'remOnset' if test_rem else 'sleepOnset' - def test_sequence_starts_with_stage(self, sequence, test_rem): expected_latency = 0 expected_onset = self.MOCK_REQUEST.bedtime @@ -274,9 +229,15 @@ def test_sequence_with_stage_at_middle(self, sequence, test_rem, latency): expected_onset = latency + self.MOCK_REQUEST.bedtime self.assert_latency_equals_expected(latency, expected_onset, sequence, test_rem) + def get_latency_report_key(self, test_rem): + return 'remLatency' if test_rem else 'sleepLatency' + + def get_onset_report_key(self, test_rem): + return 'remOnset' if test_rem else 'sleepOnset' + def assert_latency_equals_expected(self, expected_latency, expected_onset, sequence, test_rem): sequence = convert_sleep_stage_name_to_values(sequence) - response = ClassificationResponse(TestReportLatenciesOnset.MOCK_REQUEST, sequence, None) + response = ClassificationResponse(self.MOCK_REQUEST, sequence, None) report = response.report assert report[self.get_latency_report_key(test_rem)] == expected_latency, ( @@ -287,12 +248,75 @@ def assert_latency_equals_expected(self, expected_latency, expected_onset, seque ) +class TestReportSleepOffset(): + """Tests timestamp at which user woke up + "sleepOffset": 1602242425, // Time at which the subject woke up (time of the epoch after the last non-wake epoch) + """ + params = { + 'test_wake_up_end': [dict( + sequence=['W', 'N1', 'N2', 'N3', 'REM', 'N1', 'W'], + )], 'test_wake_up_middle': [dict( + sequence=['W', 'N1', 'N2', 'W', 'W', 'W', 'W'], + awake_index=3, + )], 'test_awakes_and_goes_back_to_sleep_and_wakes': [dict( + sequence=['W', 'N1', 'N2', 'W', 'N1', 'N2', 'W'], + awake_index=6, + )], 'test_awakes_and_goes_back_to_sleep_and_doesnt_awake': [dict( + sequence=['W', 'N1', 'N2', 'W', 'N1', 'N2', 'N2'], + )], 'test_always_awake': [ + dict(sequence=['W', 'W', 'W']), + dict(sequence=['W']), + ], 'test_doesnt_awaken': [ + dict(sequence=['W', 'N1', 'N2']), + dict(sequence=['N1', 'N1', 'N2']), + ], + } + + @classmethod + def setup_class(cls): + cls.MOCK_REQUEST = get_mock_request() + + def test_wake_up_end(self, sequence): + expected_sleep_offset = self.MOCK_REQUEST.bedtime + EPOCH_DURATION * (len(sequence) - 1) + self.assert_sleep_offset(sequence, expected_sleep_offset) + + def test_wake_up_middle(self, sequence, awake_index): + expected_sleep_offset = self.MOCK_REQUEST.bedtime + EPOCH_DURATION * awake_index + self.assert_sleep_offset(sequence, expected_sleep_offset) + + def test_awakes_and_goes_back_to_sleep_and_wakes(self, sequence, awake_index): + expected_sleep_offset = self.MOCK_REQUEST.bedtime + EPOCH_DURATION * awake_index + self.assert_sleep_offset(sequence, expected_sleep_offset) + + def test_awakes_and_goes_back_to_sleep_and_doesnt_awake(self, sequence): + expected_sleep_offset = self.MOCK_REQUEST.bedtime + EPOCH_DURATION * len(sequence) + self.assert_sleep_offset(sequence, expected_sleep_offset) + + def test_always_awake(self, sequence): + expected_sleep_offset = self.MOCK_REQUEST.bedtime + EPOCH_DURATION * len(sequence) + self.assert_sleep_offset(sequence, expected_sleep_offset) + + def test_doesnt_awaken(self, sequence): + expected_sleep_offset = self.MOCK_REQUEST.bedtime + EPOCH_DURATION * len(sequence) + self.assert_sleep_offset(sequence, expected_sleep_offset) + + def assert_sleep_offset(self, sequence, expected_sleep_offset): + sequence = convert_sleep_stage_name_to_values(sequence) + response = ClassificationResponse(self.MOCK_REQUEST, sequence, None) + report = response.report + + assert report['sleepOffset'] == expected_sleep_offset + + class TestReportMetrics(): """ "sleepEfficiency": 0.8733, // Overall sense of how well the patient slept (totalSleepTime/bedTime) "awakenings": 7, // number of times the subject woke up between sleep onset & offset "stageShifts": 89, // number of times the subject transitionned // from one stage to another between sleep onset & offset + "wakeAfterSleepOffset": 500, // [seconds] (wakeUpTime - sleepOffset) + "efficientSleepTime": 27113, // Total amount of seconds passed in non-wake stages + """ params = { } From 9e9f704568149e94a70a2a9132f474d8e7be6eb2 Mon Sep 17 00:00:00 2001 From: Claudia Onorato Date: Tue, 10 Nov 2020 02:16:13 -0500 Subject: [PATCH 14/29] added wake after sleep offset --- backend/backend/response.py | 5 ++--- backend/metric/offset.py | 13 ++++++++++--- backend/tests/test_response.py | 25 ++++++++++++++++--------- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/backend/backend/response.py b/backend/backend/response.py index ebadbd2e..a6d20393 100644 --- a/backend/backend/response.py +++ b/backend/backend/response.py @@ -4,7 +4,7 @@ from metric.time_passed_in_stage import get_time_passed_in_stage from metric.latency import get_latencies -from metric.offset import get_sleep_offset +from metric.offset import get_sleep_offset_with_wake class ClassificationResponse(): @@ -66,14 +66,13 @@ def report(self): **latencies, **time_passed_in_stage, **onsets, - **get_sleep_offset(self.sleep_stages, self.bedtime), + **get_sleep_offset_with_wake(self.sleep_stages, self.bedtime), "sleepEfficiency": 0.8733, # Overall sense of how well the patient slept(totalSleepTime / bedTime) "awakenings": 7, # number of times the subject woke up between sleep onset & offset # number of times the subject transitionned from one stage to another between sleep onset & offset "stageShifts": 89, - "wakeAfterSleepOffset": 500, # [seconds](wakeUpTime - sleepOffset) "efficientSleepTime": 27113, # Total amount of seconds passed in non - wake stages # Total amount of time passed in nocturnal awakenings. It is the total # time passed in non - wake stage from sleep Onset to sleep diff --git a/backend/metric/offset.py b/backend/metric/offset.py index 519078dc..7a5822df 100644 --- a/backend/metric/offset.py +++ b/backend/metric/offset.py @@ -3,9 +3,16 @@ from classification.config.constants import SleepStage, EPOCH_DURATION -def get_sleep_offset(sequence, bedtime): +def get_sleep_offset_with_wake(sequence, bedtime): sleep_indexes = np.where(sequence != SleepStage.W.name)[0] sleep_nb_epochs = (sleep_indexes[-1] + 1) if len(sleep_indexes) else len(sequence) + wake_after_sleep_offset_nb_epochs = ( + len(sequence) - sleep_indexes[-1] - 1 + ) if len(sleep_indexes) and sequence[-1] == SleepStage.W.name else 0 - # Time at which the subject woke up(time of the epoch after the last non - wake epoch) - return {'sleepOffset': sleep_nb_epochs * EPOCH_DURATION + bedtime} + return { + # Time at which the subject woke up(time of the epoch after the last non - wake epoch) + 'sleepOffset': sleep_nb_epochs * EPOCH_DURATION + bedtime, + # [seconds](wakeUpTime - sleepOffset) + 'wakeAfterSleepOffset': wake_after_sleep_offset_nb_epochs * EPOCH_DURATION, + } diff --git a/backend/tests/test_response.py b/backend/tests/test_response.py index c266c087..13f1a5fb 100644 --- a/backend/tests/test_response.py +++ b/backend/tests/test_response.py @@ -258,9 +258,11 @@ class TestReportSleepOffset(): )], 'test_wake_up_middle': [dict( sequence=['W', 'N1', 'N2', 'W', 'W', 'W', 'W'], awake_index=3, + expected_wake_after_sleep_offset=4 * EPOCH_DURATION, )], 'test_awakes_and_goes_back_to_sleep_and_wakes': [dict( sequence=['W', 'N1', 'N2', 'W', 'N1', 'N2', 'W'], awake_index=6, + expected_wake_after_sleep_offset=EPOCH_DURATION, )], 'test_awakes_and_goes_back_to_sleep_and_doesnt_awake': [dict( sequence=['W', 'N1', 'N2', 'W', 'N1', 'N2', 'N2'], )], 'test_always_awake': [ @@ -278,34 +280,39 @@ def setup_class(cls): def test_wake_up_end(self, sequence): expected_sleep_offset = self.MOCK_REQUEST.bedtime + EPOCH_DURATION * (len(sequence) - 1) - self.assert_sleep_offset(sequence, expected_sleep_offset) + expected_wake_after_sleep_offset = EPOCH_DURATION + self.assert_sleep_offset_with_wake(sequence, expected_sleep_offset, expected_wake_after_sleep_offset) - def test_wake_up_middle(self, sequence, awake_index): + def test_wake_up_middle(self, sequence, awake_index, expected_wake_after_sleep_offset): expected_sleep_offset = self.MOCK_REQUEST.bedtime + EPOCH_DURATION * awake_index - self.assert_sleep_offset(sequence, expected_sleep_offset) + self.assert_sleep_offset_with_wake(sequence, expected_sleep_offset, expected_wake_after_sleep_offset) - def test_awakes_and_goes_back_to_sleep_and_wakes(self, sequence, awake_index): + def test_awakes_and_goes_back_to_sleep_and_wakes(self, sequence, awake_index, expected_wake_after_sleep_offset): expected_sleep_offset = self.MOCK_REQUEST.bedtime + EPOCH_DURATION * awake_index - self.assert_sleep_offset(sequence, expected_sleep_offset) + self.assert_sleep_offset_with_wake(sequence, expected_sleep_offset, expected_wake_after_sleep_offset) def test_awakes_and_goes_back_to_sleep_and_doesnt_awake(self, sequence): expected_sleep_offset = self.MOCK_REQUEST.bedtime + EPOCH_DURATION * len(sequence) - self.assert_sleep_offset(sequence, expected_sleep_offset) + expected_wake_after_sleep_offset = 0 + self.assert_sleep_offset_with_wake(sequence, expected_sleep_offset, expected_wake_after_sleep_offset) def test_always_awake(self, sequence): expected_sleep_offset = self.MOCK_REQUEST.bedtime + EPOCH_DURATION * len(sequence) - self.assert_sleep_offset(sequence, expected_sleep_offset) + expected_wake_after_sleep_offset = 0 + self.assert_sleep_offset_with_wake(sequence, expected_sleep_offset, expected_wake_after_sleep_offset) def test_doesnt_awaken(self, sequence): expected_sleep_offset = self.MOCK_REQUEST.bedtime + EPOCH_DURATION * len(sequence) - self.assert_sleep_offset(sequence, expected_sleep_offset) + expected_wake_after_sleep_offset = 0 + self.assert_sleep_offset_with_wake(sequence, expected_sleep_offset, expected_wake_after_sleep_offset) - def assert_sleep_offset(self, sequence, expected_sleep_offset): + def assert_sleep_offset_with_wake(self, sequence, expected_sleep_offset, expected_wake_after_sleep_offset): sequence = convert_sleep_stage_name_to_values(sequence) response = ClassificationResponse(self.MOCK_REQUEST, sequence, None) report = response.report assert report['sleepOffset'] == expected_sleep_offset + assert report['wakeAfterSleepOffset'] == expected_wake_after_sleep_offset class TestReportMetrics(): From 36076f80a8f1c2a4858cd91f853a0a0ddde5a199 Mon Sep 17 00:00:00 2001 From: Claudia Onorato Date: Tue, 10 Nov 2020 02:26:17 -0500 Subject: [PATCH 15/29] added efficient tests --- backend/tests/test_response.py | 56 ++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/backend/tests/test_response.py b/backend/tests/test_response.py index 13f1a5fb..b5a9a8a2 100644 --- a/backend/tests/test_response.py +++ b/backend/tests/test_response.py @@ -315,14 +315,64 @@ def assert_sleep_offset_with_wake(self, sequence, expected_sleep_offset, expecte assert report['wakeAfterSleepOffset'] == expected_wake_after_sleep_offset -class TestReportMetrics(): +class TestReportSleepEfficiency(): """ "sleepEfficiency": 0.8733, // Overall sense of how well the patient slept (totalSleepTime/bedTime) + "efficientSleepTime": 27113, // Total amount of seconds passed in non-wake stages + """ + params = { + 'test_sleep_time_null': [ + dict(sequence=['W', 'W', 'W']), + dict(sequence=['W']), + ], 'test_sleep_time_not_null': [ + dict( + sequence=['W', 'W', 'N1', 'W'], + expected_efficiency=(1 / 4), + expected_efficient_sleep_time=EPOCH_DURATION + ), dict( + sequence=['W', 'W', 'N2', 'W'], + expected_efficiency=(1 / 4), + expected_efficient_sleep_time=EPOCH_DURATION + ), dict( + sequence=['W', 'W', 'N3', 'W'], + expected_efficiency=(1 / 4), + expected_efficient_sleep_time=EPOCH_DURATION + ), dict( + sequence=['W', 'W', 'REM', 'W'], + expected_efficiency=(1 / 4), + expected_efficient_sleep_time=EPOCH_DURATION + ), dict( + sequence=['W', 'W', 'N1', 'N2', 'N3', 'REM', 'N1', 'W'], + expected_efficiency=(5 / 8), + expected_efficient_sleep_time=5 * EPOCH_DURATION + ), + ] + } + + @classmethod + def setup_class(cls): + cls.MOCK_REQUEST = get_mock_request() + + def test_sleep_time_null(self, sequence): + self.assert_sleep_efficiency(sequence, expected_efficiency=0, expected_efficient_sleep_time=0) + + def test_sleep_time_not_null(self, sequence, expected_efficiency, expected_efficient_sleep_time): + self.assert_sleep_efficiency(sequence, expected_efficiency, expected_efficient_sleep_time) + + def assert_sleep_efficiency(self, sequence, expected_efficiency, expected_efficient_sleep_time): + sequence = convert_sleep_stage_name_to_values(sequence) + response = ClassificationResponse(self.MOCK_REQUEST, sequence, None) + report = response.report + + assert report['sleepEfficiency'] == expected_efficiency + assert report['efficientSleepTime'] == expected_efficient_sleep_time + + +class TestReportMetrics(): + """ "awakenings": 7, // number of times the subject woke up between sleep onset & offset "stageShifts": 89, // number of times the subject transitionned // from one stage to another between sleep onset & offset - "wakeAfterSleepOffset": 500, // [seconds] (wakeUpTime - sleepOffset) - "efficientSleepTime": 27113, // Total amount of seconds passed in non-wake stages """ params = { From c0ec7c2d661d09447d024a2fbb1a35d45c01b100 Mon Sep 17 00:00:00 2001 From: Claudia Onorato Date: Tue, 10 Nov 2020 02:41:41 -0500 Subject: [PATCH 16/29] added efficiency --- backend/backend/response.py | 21 +++++++++++---------- backend/metric/efficiency.py | 16 ++++++++++++++++ backend/metric/offset.py | 2 -- 3 files changed, 27 insertions(+), 12 deletions(-) create mode 100644 backend/metric/efficiency.py diff --git a/backend/backend/response.py b/backend/backend/response.py index a6d20393..3224aae6 100644 --- a/backend/backend/response.py +++ b/backend/backend/response.py @@ -5,6 +5,7 @@ from metric.time_passed_in_stage import get_time_passed_in_stage from metric.latency import get_latencies from metric.offset import get_sleep_offset_with_wake +from metric.efficiency import get_efficiency class ClassificationResponse(): @@ -53,6 +54,8 @@ def subject(self): def report(self): latencies = get_latencies(self.sleep_stages) time_passed_in_stage = get_time_passed_in_stage(self.sleep_stages) + efficiencies = get_efficiency(self.sleep_stages) + sleep_offset_with_wake = get_sleep_offset_with_wake(self.sleep_stages, self.bedtime) onsets = { "sleepOnset": ( latencies['sleepLatency'] if latencies['sleepLatency'] >= 0 else 0 @@ -61,24 +64,22 @@ def report(self): latencies['remLatency'] if latencies['remLatency'] >= 0 else 0 ) + self.bedtime } + sleep_time = sleep_offset_with_wake['sleepOffset'] - onsets['sleepOnset'] + waso = sleep_time - efficiencies['efficientSleepTime'] return { **latencies, **time_passed_in_stage, **onsets, - **get_sleep_offset_with_wake(self.sleep_stages, self.bedtime), + **efficiencies, + **sleep_offset_with_wake, - "sleepEfficiency": 0.8733, # Overall sense of how well the patient slept(totalSleepTime / bedTime) - "awakenings": 7, # number of times the subject woke up between sleep onset & offset - # number of times the subject transitionned from one stage to another between sleep onset & offset + "awakenings": 7, "stageShifts": 89, - "efficientSleepTime": 27113, # Total amount of seconds passed in non - wake stages - # Total amount of time passed in nocturnal awakenings. It is the total - # time passed in non - wake stage from sleep Onset to sleep - # offset(totalSleepTime - efficientSleepTime) - "WASO": 3932, - "SleepTime": 31045, + # not tested + "WASO": waso, + "SleepTime": sleep_time, } @property diff --git a/backend/metric/efficiency.py b/backend/metric/efficiency.py new file mode 100644 index 00000000..8761da69 --- /dev/null +++ b/backend/metric/efficiency.py @@ -0,0 +1,16 @@ +import numpy as np + +from classification.config.constants import SleepStage, EPOCH_DURATION + + +def get_efficiency(sequence): + """ + "sleepEfficiency": 0.8733, # Overall sense of how well the patient slept(totalSleepTime / bedTime) + "efficientSleepTime": 27113, # Total amount of seconds passed in non - wake stages + """ + sleep_indexes = np.where(sequence != SleepStage.W.name)[0] + + return { + "sleepEfficiency": sleep_indexes.shape[0] / sequence.shape[0], + "efficientSleepTime": sleep_indexes.shape[0] * EPOCH_DURATION, + } diff --git a/backend/metric/offset.py b/backend/metric/offset.py index 7a5822df..e12dfd99 100644 --- a/backend/metric/offset.py +++ b/backend/metric/offset.py @@ -11,8 +11,6 @@ def get_sleep_offset_with_wake(sequence, bedtime): ) if len(sleep_indexes) and sequence[-1] == SleepStage.W.name else 0 return { - # Time at which the subject woke up(time of the epoch after the last non - wake epoch) 'sleepOffset': sleep_nb_epochs * EPOCH_DURATION + bedtime, - # [seconds](wakeUpTime - sleepOffset) 'wakeAfterSleepOffset': wake_after_sleep_offset_nb_epochs * EPOCH_DURATION, } From 8af967b2e913334b92f005fc8cc9f47cc4117a9d Mon Sep 17 00:00:00 2001 From: Claudia Onorato Date: Tue, 10 Nov 2020 21:50:40 -0500 Subject: [PATCH 17/29] fixed warning --- backend/classification/parser/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/classification/parser/__init__.py b/backend/classification/parser/__init__.py index 99a014ad..79cc4d37 100644 --- a/backend/classification/parser/__init__.py +++ b/backend/classification/parser/__init__.py @@ -16,7 +16,7 @@ from classification.config.constants import OPENBCI_CYTON_SAMPLE_RATE, EEG_CHANNELS from classification.parser.constants import SCALE_V_PER_COUNT -from classification.parser.file_type import FileType, detect_file_type +from classification.parser.file_type import detect_file_type def get_raw_array(file): From e9ca36e8deb19b682afabe8e01ab258df0a22e1e Mon Sep 17 00:00:00 2001 From: Claudia Onorato Date: Tue, 10 Nov 2020 23:35:26 -0500 Subject: [PATCH 18/29] added tests --- backend/tests/test_response.py | 95 ++++++++++++++++++++++++++++++++-- 1 file changed, 91 insertions(+), 4 deletions(-) diff --git a/backend/tests/test_response.py b/backend/tests/test_response.py index b5a9a8a2..4c4d60a4 100644 --- a/backend/tests/test_response.py +++ b/backend/tests/test_response.py @@ -316,7 +316,8 @@ def assert_sleep_offset_with_wake(self, sequence, expected_sleep_offset, expecte class TestReportSleepEfficiency(): - """ + """Tests sleep efficiency related metrics + The evaluated metrics are: "sleepEfficiency": 0.8733, // Overall sense of how well the patient slept (totalSleepTime/bedTime) "efficientSleepTime": 27113, // Total amount of seconds passed in non-wake stages """ @@ -368,12 +369,98 @@ def assert_sleep_efficiency(self, sequence, expected_efficiency, expected_effici assert report['efficientSleepTime'] == expected_efficient_sleep_time -class TestReportMetrics(): - """ +class TestReportAwakenings(): + """Tests number of awakenings per night "awakenings": 7, // number of times the subject woke up between sleep onset & offset + """ + params = { + 'test_sleep_time_null': [ + dict(sequence=['W', 'W', 'W']), + dict(sequence=['W']), + ], 'test_one_awakening': [ + dict(sequence=['W', 'N1', 'W']), + dict(sequence=['W', 'N1', 'N2', 'N3', 'W', 'W']), + ], 'test_doesnt_awaken': [ + dict(sequence=['W', 'N1', 'N2', 'N3', 'REM']), + dict(sequence=['W', 'N1']), + ], 'test_many_awakening': [ + dict(sequence=['W', 'N1', 'W', 'N1', 'W'], nb_awakenings=2), + dict(sequence=['W', 'N1', 'N2', 'W', 'N1', 'W', 'W', 'N1', 'W'], nb_awakenings=3), + ], + } + + @classmethod + def setup_class(cls): + cls.MOCK_REQUEST = get_mock_request() + + def test_sleep_time_null(self, sequence): + self.assert_sleep_efficiency(sequence, expected_nb_awakenings=0) + + def test_one_awakening(self, sequence): + self.assert_sleep_efficiency(sequence, expected_nb_awakenings=1) + + def test_doesnt_awaken(self, sequence): + self.assert_sleep_efficiency(sequence, expected_nb_awakenings=1) + + def test_many_awakening(self, sequence, nb_awakenings): + self.assert_sleep_efficiency(sequence, expected_nb_awakenings=nb_awakenings) + + def assert_sleep_efficiency(self, sequence, expected_nb_awakenings): + sequence = convert_sleep_stage_name_to_values(sequence) + response = ClassificationResponse(self.MOCK_REQUEST, sequence, None) + report = response.report + + assert report['awakenings'] == expected_nb_awakenings + + +class TestReportStageShifts(): + """Test number of stage shifts per night "stageShifts": 89, // number of times the subject transitionned // from one stage to another between sleep onset & offset - """ params = { + 'test_sleep_time_null': [ + dict(sequence=['W', 'W', 'W']), + dict(sequence=['W']), + ], 'test_one_sleep_stage': [ + dict(sequence=['W', 'W', 'N1', 'N1', 'N1', 'W']), + dict(sequence=['W', 'N1', 'W']), + ], 'test_sleep_with_awakenings': [ + dict(sequence=['W', 'N1', 'W', 'N1', 'N1', 'W'], sleep_shifts=4), + dict(sequence=['W', 'N1', 'W', 'N2', 'W', 'N3', 'W'], sleep_shifts=6), + ], 'test_does_not_awaken': [ + dict(sequence=['W', 'N1', 'N1'], sleep_shifts=2), + dict(sequence=['W', 'N1', 'N2'], sleep_shifts=2), + dict(sequence=['W', 'N1', 'N3'], sleep_shifts=2), + dict(sequence=['W', 'N1', 'REM'], sleep_shifts=2), + ], 'test_many_sleep_stages': [ + dict(sequence=['W', 'N1', 'N2', 'N2', 'W'], sleep_shifts=3), + dict(sequence=['W', 'N1', 'N1', 'N3', 'N3', 'REM', 'REM', 'N1', 'W'], sleep_shifts=5), + ], } + + @classmethod + def setup_class(cls): + cls.MOCK_REQUEST = get_mock_request() + + def test_sleep_time_null(self, sequence): + self.assert_sleep_efficiency(sequence, expected_sleep_shifts=0) + + def test_one_sleep_stage(self, sequence): + self.assert_sleep_efficiency(sequence, expected_sleep_shifts=2) + + def test_sleep_with_awakenings(self, sequence, sleep_shifts): + self.assert_sleep_efficiency(sequence, expected_sleep_shifts=sleep_shifts) + + def test_does_not_awaken(self, sequence, sleep_shifts): + self.assert_sleep_efficiency(sequence, expected_sleep_shifts=sleep_shifts) + + def test_many_sleep_stages(self, sequence, sleep_shifts): + self.assert_sleep_efficiency(sequence, expected_sleep_shifts=sleep_shifts) + + def assert_sleep_efficiency(self, sequence, expected_sleep_shifts): + sequence = convert_sleep_stage_name_to_values(sequence) + response = ClassificationResponse(self.MOCK_REQUEST, sequence, None) + report = response.report + + assert report['stageShifts'] == expected_sleep_shifts From f06294b7dd6ac0fee1ee6af39d80df78a9163209 Mon Sep 17 00:00:00 2001 From: Claudia Onorato Date: Tue, 10 Nov 2020 23:47:24 -0500 Subject: [PATCH 19/29] moved all metrics to single module --- backend/backend/metric.py | 94 ++++++++++++++++++++++++++ backend/backend/response.py | 36 +--------- backend/metric/efficiency.py | 16 ----- backend/metric/latency.py | 19 ------ backend/metric/offset.py | 16 ----- backend/metric/time_passed_in_stage.py | 18 ----- 6 files changed, 96 insertions(+), 103 deletions(-) create mode 100644 backend/backend/metric.py delete mode 100644 backend/metric/efficiency.py delete mode 100644 backend/metric/latency.py delete mode 100644 backend/metric/offset.py delete mode 100644 backend/metric/time_passed_in_stage.py diff --git a/backend/backend/metric.py b/backend/backend/metric.py new file mode 100644 index 00000000..9c93afb2 --- /dev/null +++ b/backend/backend/metric.py @@ -0,0 +1,94 @@ +from collections import Counter +import numpy as np + +from classification.config.constants import SleepStage, EPOCH_DURATION + + +def get_metrics(sleep_stages, bedtime): + latencies = _get_latencies(sleep_stages) + time_passed_in_stage = _get_time_passed_in_stage(sleep_stages) + efficiencies = _get_efficiency(sleep_stages) + sleep_offset_with_wake = _get_sleep_offset_with_wake(sleep_stages, bedtime) + onsets = { + "sleepOnset": ( + latencies['sleepLatency'] if latencies['sleepLatency'] >= 0 else 0 + ) + bedtime, + "remOnset": ( + latencies['remLatency'] if latencies['remLatency'] >= 0 else 0 + ) + bedtime + } + + sleep_time = sleep_offset_with_wake['sleepOffset'] - onsets['sleepOnset'] + waso = sleep_time - efficiencies['efficientSleepTime'] + + return { + **latencies, + **time_passed_in_stage, + **onsets, + **efficiencies, + **sleep_offset_with_wake, + + "awakenings": 7, + "stageShifts": 89, + + # not tested + "WASO": waso, + "SleepTime": sleep_time, + } + + +def _get_efficiency(sequence): + """ + "sleepEfficiency": 0.8733, # Overall sense of how well the patient slept(totalSleepTime / bedTime) + "efficientSleepTime": 27113, # Total amount of seconds passed in non - wake stages + """ + sleep_indexes = np.where(sequence != SleepStage.W.name)[0] + + return { + "sleepEfficiency": sleep_indexes.shape[0] / sequence.shape[0], + "efficientSleepTime": sleep_indexes.shape[0] * EPOCH_DURATION, + } + + +def _get_latencies(sequence): + """Tests the time it took to enter a specific stage + Input: + - sequence: np.array of the SleepStage labels + """ + def get_latency_of_stage(sequence_is_stage): + epochs_of_stage_of_interest = np.where(sequence_is_stage)[0] + return int(-1 if epochs_of_stage_of_interest.shape[0] == 0 else epochs_of_stage_of_interest[0] * EPOCH_DURATION) + + return { + "sleepLatency": get_latency_of_stage(sequence != SleepStage.W.name), + "remLatency": get_latency_of_stage(sequence == SleepStage.REM.name), + } + + +def _get_sleep_offset_with_wake(sequence, bedtime): + sleep_indexes = np.where(sequence != SleepStage.W.name)[0] + sleep_nb_epochs = (sleep_indexes[-1] + 1) if len(sleep_indexes) else len(sequence) + wake_after_sleep_offset_nb_epochs = ( + len(sequence) - sleep_indexes[-1] - 1 + ) if len(sleep_indexes) and sequence[-1] == SleepStage.W.name else 0 + + return { + 'sleepOffset': sleep_nb_epochs * EPOCH_DURATION + bedtime, + 'wakeAfterSleepOffset': wake_after_sleep_offset_nb_epochs * EPOCH_DURATION, + } + + +def _get_time_passed_in_stage(sequence): + """Calculates time passed in each stage for all of the sequence + Input: + - sequence: list or np.array of the SleepStage labels + """ + nb_epoch_passed_by_stage = Counter(sequence) + + def get_time_passed(stage): + return EPOCH_DURATION * nb_epoch_passed_by_stage[stage] if stage in nb_epoch_passed_by_stage else 0 + + return { + f"{stage.upper()}Time": get_time_passed(stage) + for stage in SleepStage.tolist() + } diff --git a/backend/backend/response.py b/backend/backend/response.py index 3224aae6..24f418d6 100644 --- a/backend/backend/response.py +++ b/backend/backend/response.py @@ -1,12 +1,8 @@ import numpy as np +from backend.metric import get_metrics from classification.config.constants import EPOCH_DURATION, SleepStage -from metric.time_passed_in_stage import get_time_passed_in_stage -from metric.latency import get_latencies -from metric.offset import get_sleep_offset_with_wake -from metric.efficiency import get_efficiency - class ClassificationResponse(): def __init__(self, request, predictions, spectrogram): @@ -52,35 +48,7 @@ def subject(self): @property def report(self): - latencies = get_latencies(self.sleep_stages) - time_passed_in_stage = get_time_passed_in_stage(self.sleep_stages) - efficiencies = get_efficiency(self.sleep_stages) - sleep_offset_with_wake = get_sleep_offset_with_wake(self.sleep_stages, self.bedtime) - onsets = { - "sleepOnset": ( - latencies['sleepLatency'] if latencies['sleepLatency'] >= 0 else 0 - ) + self.bedtime, - "remOnset": ( - latencies['remLatency'] if latencies['remLatency'] >= 0 else 0 - ) + self.bedtime - } - sleep_time = sleep_offset_with_wake['sleepOffset'] - onsets['sleepOnset'] - waso = sleep_time - efficiencies['efficientSleepTime'] - - return { - **latencies, - **time_passed_in_stage, - **onsets, - **efficiencies, - **sleep_offset_with_wake, - - "awakenings": 7, - "stageShifts": 89, - - # not tested - "WASO": waso, - "SleepTime": sleep_time, - } + return get_metrics(self.sleep_stages, self.bedtime) @property def response(self): diff --git a/backend/metric/efficiency.py b/backend/metric/efficiency.py deleted file mode 100644 index 8761da69..00000000 --- a/backend/metric/efficiency.py +++ /dev/null @@ -1,16 +0,0 @@ -import numpy as np - -from classification.config.constants import SleepStage, EPOCH_DURATION - - -def get_efficiency(sequence): - """ - "sleepEfficiency": 0.8733, # Overall sense of how well the patient slept(totalSleepTime / bedTime) - "efficientSleepTime": 27113, # Total amount of seconds passed in non - wake stages - """ - sleep_indexes = np.where(sequence != SleepStage.W.name)[0] - - return { - "sleepEfficiency": sleep_indexes.shape[0] / sequence.shape[0], - "efficientSleepTime": sleep_indexes.shape[0] * EPOCH_DURATION, - } diff --git a/backend/metric/latency.py b/backend/metric/latency.py deleted file mode 100644 index 86cdb7dd..00000000 --- a/backend/metric/latency.py +++ /dev/null @@ -1,19 +0,0 @@ -import numpy as np - -from classification.config.constants import SleepStage, EPOCH_DURATION - - -def get_latencies(sequence): - """Tests the time it took to enter a specific stage - Input: - - sequence: np.array of the SleepStage labels - """ - return { - "sleepLatency": _get_latency_of_stage(sequence != SleepStage.W.name), - "remLatency": _get_latency_of_stage(sequence == SleepStage.REM.name), - } - - -def _get_latency_of_stage(sequence_is_stage): - epochs_of_stage_of_interest = np.where(sequence_is_stage)[0] - return int(-1 if epochs_of_stage_of_interest.shape[0] == 0 else epochs_of_stage_of_interest[0] * EPOCH_DURATION) diff --git a/backend/metric/offset.py b/backend/metric/offset.py deleted file mode 100644 index e12dfd99..00000000 --- a/backend/metric/offset.py +++ /dev/null @@ -1,16 +0,0 @@ -import numpy as np - -from classification.config.constants import SleepStage, EPOCH_DURATION - - -def get_sleep_offset_with_wake(sequence, bedtime): - sleep_indexes = np.where(sequence != SleepStage.W.name)[0] - sleep_nb_epochs = (sleep_indexes[-1] + 1) if len(sleep_indexes) else len(sequence) - wake_after_sleep_offset_nb_epochs = ( - len(sequence) - sleep_indexes[-1] - 1 - ) if len(sleep_indexes) and sequence[-1] == SleepStage.W.name else 0 - - return { - 'sleepOffset': sleep_nb_epochs * EPOCH_DURATION + bedtime, - 'wakeAfterSleepOffset': wake_after_sleep_offset_nb_epochs * EPOCH_DURATION, - } diff --git a/backend/metric/time_passed_in_stage.py b/backend/metric/time_passed_in_stage.py deleted file mode 100644 index 282c126a..00000000 --- a/backend/metric/time_passed_in_stage.py +++ /dev/null @@ -1,18 +0,0 @@ -from collections import Counter -from classification.config.constants import SleepStage, EPOCH_DURATION - - -def get_time_passed_in_stage(sequence): - """Calculates time passed in each stage for all of the sequence - Input: - - sequence: list or np.array of the SleepStage labels - """ - nb_epoch_passed_by_stage = Counter(sequence) - - def get_time_passed(stage): - return EPOCH_DURATION * nb_epoch_passed_by_stage[stage] if stage in nb_epoch_passed_by_stage else 0 - - return { - f"{stage.upper()}Time": get_time_passed(stage) - for stage in SleepStage.tolist() - } From 0aa377eefbb7e483c3cefebab5d2c02e921aa37f Mon Sep 17 00:00:00 2001 From: Claudia Onorato Date: Wed, 11 Nov 2020 00:27:05 -0500 Subject: [PATCH 20/29] refactored and added stage shifts --- backend/backend/metric.py | 103 +++++++++++++++++++-------------- backend/tests/test_response.py | 90 ++++++++++++++-------------- 2 files changed, 106 insertions(+), 87 deletions(-) diff --git a/backend/backend/metric.py b/backend/backend/metric.py index 9c93afb2..4618e9cc 100644 --- a/backend/backend/metric.py +++ b/backend/backend/metric.py @@ -5,68 +5,44 @@ def get_metrics(sleep_stages, bedtime): - latencies = _get_latencies(sleep_stages) - time_passed_in_stage = _get_time_passed_in_stage(sleep_stages) - efficiencies = _get_efficiency(sleep_stages) - sleep_offset_with_wake = _get_sleep_offset_with_wake(sleep_stages, bedtime) - onsets = { - "sleepOnset": ( - latencies['sleepLatency'] if latencies['sleepLatency'] >= 0 else 0 - ) + bedtime, - "remOnset": ( - latencies['remLatency'] if latencies['remLatency'] >= 0 else 0 - ) + bedtime + independent_metrics = { + **_get_rem_latency(sleep_stages), + **_get_time_passed_in_stage(sleep_stages), + **_get_sleep_vs_wake_metrics(sleep_stages, bedtime), + **_get_stage_shifts(sleep_stages) } - - sleep_time = sleep_offset_with_wake['sleepOffset'] - onsets['sleepOnset'] - waso = sleep_time - efficiencies['efficientSleepTime'] + onsets = _get_onsets(independent_metrics, bedtime) + sleep_time = independent_metrics['sleepOffset'] - onsets['sleepOnset'] + waso = dict(WASO=sleep_time - independent_metrics['efficientSleepTime']) return { - **latencies, - **time_passed_in_stage, + **independent_metrics, **onsets, - **efficiencies, - **sleep_offset_with_wake, - - "awakenings": 7, - "stageShifts": 89, - # not tested - "WASO": waso, + **waso, "SleepTime": sleep_time, + + "awakenings": 7, } -def _get_efficiency(sequence): +def _get_sleep_vs_wake_metrics(sequence, bedtime): """ "sleepEfficiency": 0.8733, # Overall sense of how well the patient slept(totalSleepTime / bedTime) "efficientSleepTime": 27113, # Total amount of seconds passed in non - wake stages """ - sleep_indexes = np.where(sequence != SleepStage.W.name)[0] + sleep_condition = sequence != SleepStage.W.name + sleep_indexes = np.where(sleep_condition)[0] return { "sleepEfficiency": sleep_indexes.shape[0] / sequence.shape[0], "efficientSleepTime": sleep_indexes.shape[0] * EPOCH_DURATION, + "sleepLatency": _get_latency_of_stage(sleep_condition), + **_get_sleep_offset_with_wake(sleep_indexes, sequence, bedtime) } -def _get_latencies(sequence): - """Tests the time it took to enter a specific stage - Input: - - sequence: np.array of the SleepStage labels - """ - def get_latency_of_stage(sequence_is_stage): - epochs_of_stage_of_interest = np.where(sequence_is_stage)[0] - return int(-1 if epochs_of_stage_of_interest.shape[0] == 0 else epochs_of_stage_of_interest[0] * EPOCH_DURATION) - - return { - "sleepLatency": get_latency_of_stage(sequence != SleepStage.W.name), - "remLatency": get_latency_of_stage(sequence == SleepStage.REM.name), - } - - -def _get_sleep_offset_with_wake(sequence, bedtime): - sleep_indexes = np.where(sequence != SleepStage.W.name)[0] +def _get_sleep_offset_with_wake(sleep_indexes, sequence, bedtime): sleep_nb_epochs = (sleep_indexes[-1] + 1) if len(sleep_indexes) else len(sequence) wake_after_sleep_offset_nb_epochs = ( len(sequence) - sleep_indexes[-1] - 1 @@ -78,6 +54,17 @@ def _get_sleep_offset_with_wake(sequence, bedtime): } +def _get_rem_latency(sequence): + """Tests the time it took to enter a specific stage + Input: + - sequence: np.array of the SleepStage labels + """ + + return { + "remLatency": _get_latency_of_stage(sequence == SleepStage.REM.name), + } + + def _get_time_passed_in_stage(sequence): """Calculates time passed in each stage for all of the sequence Input: @@ -92,3 +79,35 @@ def get_time_passed(stage): f"{stage.upper()}Time": get_time_passed(stage) for stage in SleepStage.tolist() } + + +def _get_stage_shifts(sequence): + consecutive_stages_occurences = Counter(zip(sequence[1:], sequence[:-1])) + transition_occurences = [ + consecutive_stages_occurences[consecutive_stages] + for consecutive_stages in consecutive_stages_occurences if consecutive_stages[0] != consecutive_stages[1] + ] + nb_stage_shifts = sum(transition_occurences) + + is_last_stage_sleep = sequence[-1] != SleepStage.W.name + has_slept = len(np.unique(sequence)) != 1 or np.unique(sequence)[0] != SleepStage.W.name + if is_last_stage_sleep and has_slept: + nb_stage_shifts += 1 + + return dict(stageShifts=nb_stage_shifts) + + +def _get_onsets(independent_metrics, bedtime): + return { + "sleepOnset": ( + independent_metrics['sleepLatency'] if independent_metrics['sleepLatency'] >= 0 else 0 + ) + bedtime, + "remOnset": ( + independent_metrics['remLatency'] if independent_metrics['remLatency'] >= 0 else 0 + ) + bedtime + } + + +def _get_latency_of_stage(sequence_is_stage): + epochs_of_stage_of_interest = np.where(sequence_is_stage)[0] + return int(-1 if epochs_of_stage_of_interest.shape[0] == 0 else epochs_of_stage_of_interest[0] * EPOCH_DURATION) diff --git a/backend/tests/test_response.py b/backend/tests/test_response.py index 4c4d60a4..e2fb40ed 100644 --- a/backend/tests/test_response.py +++ b/backend/tests/test_response.py @@ -369,48 +369,48 @@ def assert_sleep_efficiency(self, sequence, expected_efficiency, expected_effici assert report['efficientSleepTime'] == expected_efficient_sleep_time -class TestReportAwakenings(): - """Tests number of awakenings per night - "awakenings": 7, // number of times the subject woke up between sleep onset & offset - """ - params = { - 'test_sleep_time_null': [ - dict(sequence=['W', 'W', 'W']), - dict(sequence=['W']), - ], 'test_one_awakening': [ - dict(sequence=['W', 'N1', 'W']), - dict(sequence=['W', 'N1', 'N2', 'N3', 'W', 'W']), - ], 'test_doesnt_awaken': [ - dict(sequence=['W', 'N1', 'N2', 'N3', 'REM']), - dict(sequence=['W', 'N1']), - ], 'test_many_awakening': [ - dict(sequence=['W', 'N1', 'W', 'N1', 'W'], nb_awakenings=2), - dict(sequence=['W', 'N1', 'N2', 'W', 'N1', 'W', 'W', 'N1', 'W'], nb_awakenings=3), - ], - } - - @classmethod - def setup_class(cls): - cls.MOCK_REQUEST = get_mock_request() - - def test_sleep_time_null(self, sequence): - self.assert_sleep_efficiency(sequence, expected_nb_awakenings=0) - - def test_one_awakening(self, sequence): - self.assert_sleep_efficiency(sequence, expected_nb_awakenings=1) - - def test_doesnt_awaken(self, sequence): - self.assert_sleep_efficiency(sequence, expected_nb_awakenings=1) - - def test_many_awakening(self, sequence, nb_awakenings): - self.assert_sleep_efficiency(sequence, expected_nb_awakenings=nb_awakenings) - - def assert_sleep_efficiency(self, sequence, expected_nb_awakenings): - sequence = convert_sleep_stage_name_to_values(sequence) - response = ClassificationResponse(self.MOCK_REQUEST, sequence, None) - report = response.report - - assert report['awakenings'] == expected_nb_awakenings +# class TestReportAwakenings(): +# """Tests number of awakenings per night +# "awakenings": 7, // number of times the subject woke up between sleep onset & offset +# """ +# params = { +# 'test_sleep_time_null': [ +# dict(sequence=['W', 'W', 'W']), +# dict(sequence=['W']), +# ], 'test_one_awakening': [ +# dict(sequence=['W', 'N1', 'W']), +# dict(sequence=['W', 'N1', 'N2', 'N3', 'W', 'W']), +# ], 'test_doesnt_awaken': [ +# dict(sequence=['W', 'N1', 'N2', 'N3', 'REM']), +# dict(sequence=['W', 'N1']), +# ], 'test_many_awakening': [ +# dict(sequence=['W', 'N1', 'W', 'N1', 'W'], nb_awakenings=2), +# dict(sequence=['W', 'N1', 'N2', 'W', 'N1', 'W', 'W', 'N1', 'W'], nb_awakenings=3), +# ], +# } + +# @classmethod +# def setup_class(cls): +# cls.MOCK_REQUEST = get_mock_request() + +# def test_sleep_time_null(self, sequence): +# self.assert_sleep_efficiency(sequence, expected_nb_awakenings=0) + +# def test_one_awakening(self, sequence): +# self.assert_sleep_efficiency(sequence, expected_nb_awakenings=1) + +# def test_doesnt_awaken(self, sequence): +# self.assert_sleep_efficiency(sequence, expected_nb_awakenings=1) + +# def test_many_awakening(self, sequence, nb_awakenings): +# self.assert_sleep_efficiency(sequence, expected_nb_awakenings=nb_awakenings) + +# def assert_sleep_efficiency(self, sequence, expected_nb_awakenings): +# sequence = convert_sleep_stage_name_to_values(sequence) +# response = ClassificationResponse(self.MOCK_REQUEST, sequence, None) +# report = response.report + +# assert report['awakenings'] == expected_nb_awakenings class TestReportStageShifts(): @@ -430,9 +430,9 @@ class TestReportStageShifts(): dict(sequence=['W', 'N1', 'W', 'N2', 'W', 'N3', 'W'], sleep_shifts=6), ], 'test_does_not_awaken': [ dict(sequence=['W', 'N1', 'N1'], sleep_shifts=2), - dict(sequence=['W', 'N1', 'N2'], sleep_shifts=2), - dict(sequence=['W', 'N1', 'N3'], sleep_shifts=2), - dict(sequence=['W', 'N1', 'REM'], sleep_shifts=2), + dict(sequence=['W', 'N1', 'N2'], sleep_shifts=3), + dict(sequence=['W', 'N1', 'N3'], sleep_shifts=3), + dict(sequence=['W', 'N1', 'REM'], sleep_shifts=3), ], 'test_many_sleep_stages': [ dict(sequence=['W', 'N1', 'N2', 'N2', 'W'], sleep_shifts=3), dict(sequence=['W', 'N1', 'N1', 'N3', 'N3', 'REM', 'REM', 'N1', 'W'], sleep_shifts=5), From 67ebf3e67dde131b79d0f6198c7e275ca613f524 Mon Sep 17 00:00:00 2001 From: Claudia Onorato Date: Wed, 11 Nov 2020 10:10:42 -0500 Subject: [PATCH 21/29] fixed awakenings tests --- backend/backend/metric.py | 23 ++++++---- backend/tests/test_response.py | 84 +++++++++++++++++----------------- 2 files changed, 57 insertions(+), 50 deletions(-) diff --git a/backend/backend/metric.py b/backend/backend/metric.py index 4618e9cc..8a661f46 100644 --- a/backend/backend/metric.py +++ b/backend/backend/metric.py @@ -9,7 +9,7 @@ def get_metrics(sleep_stages, bedtime): **_get_rem_latency(sleep_stages), **_get_time_passed_in_stage(sleep_stages), **_get_sleep_vs_wake_metrics(sleep_stages, bedtime), - **_get_stage_shifts(sleep_stages) + **_get_transition_based_metrics(sleep_stages) } onsets = _get_onsets(independent_metrics, bedtime) sleep_time = independent_metrics['sleepOffset'] - onsets['sleepOnset'] @@ -21,8 +21,6 @@ def get_metrics(sleep_stages, bedtime): # not tested **waso, "SleepTime": sleep_time, - - "awakenings": 7, } @@ -81,20 +79,29 @@ def get_time_passed(stage): } -def _get_stage_shifts(sequence): - consecutive_stages_occurences = Counter(zip(sequence[1:], sequence[:-1])) - transition_occurences = [ - consecutive_stages_occurences[consecutive_stages] +def _get_transition_based_metrics(sequence): + consecutive_stages_occurences = Counter(zip(sequence[:-1], sequence[1:])) + occurences_by_transition = { + consecutive_stages: consecutive_stages_occurences[consecutive_stages] for consecutive_stages in consecutive_stages_occurences if consecutive_stages[0] != consecutive_stages[1] + } + transition_occurences = list(occurences_by_transition.values()) + awakenings_occurences = [ + occurences_by_transition[transition_stages] + for transition_stages in occurences_by_transition + if transition_stages[0] != SleepStage.W.name + and transition_stages[1] == SleepStage.W.name ] nb_stage_shifts = sum(transition_occurences) + nb_awakenings = sum(awakenings_occurences) is_last_stage_sleep = sequence[-1] != SleepStage.W.name has_slept = len(np.unique(sequence)) != 1 or np.unique(sequence)[0] != SleepStage.W.name if is_last_stage_sleep and has_slept: nb_stage_shifts += 1 + nb_awakenings += 1 - return dict(stageShifts=nb_stage_shifts) + return dict(stageShifts=nb_stage_shifts, awakenings=nb_awakenings) def _get_onsets(independent_metrics, bedtime): diff --git a/backend/tests/test_response.py b/backend/tests/test_response.py index e2fb40ed..26c6e315 100644 --- a/backend/tests/test_response.py +++ b/backend/tests/test_response.py @@ -369,48 +369,48 @@ def assert_sleep_efficiency(self, sequence, expected_efficiency, expected_effici assert report['efficientSleepTime'] == expected_efficient_sleep_time -# class TestReportAwakenings(): -# """Tests number of awakenings per night -# "awakenings": 7, // number of times the subject woke up between sleep onset & offset -# """ -# params = { -# 'test_sleep_time_null': [ -# dict(sequence=['W', 'W', 'W']), -# dict(sequence=['W']), -# ], 'test_one_awakening': [ -# dict(sequence=['W', 'N1', 'W']), -# dict(sequence=['W', 'N1', 'N2', 'N3', 'W', 'W']), -# ], 'test_doesnt_awaken': [ -# dict(sequence=['W', 'N1', 'N2', 'N3', 'REM']), -# dict(sequence=['W', 'N1']), -# ], 'test_many_awakening': [ -# dict(sequence=['W', 'N1', 'W', 'N1', 'W'], nb_awakenings=2), -# dict(sequence=['W', 'N1', 'N2', 'W', 'N1', 'W', 'W', 'N1', 'W'], nb_awakenings=3), -# ], -# } - -# @classmethod -# def setup_class(cls): -# cls.MOCK_REQUEST = get_mock_request() - -# def test_sleep_time_null(self, sequence): -# self.assert_sleep_efficiency(sequence, expected_nb_awakenings=0) - -# def test_one_awakening(self, sequence): -# self.assert_sleep_efficiency(sequence, expected_nb_awakenings=1) - -# def test_doesnt_awaken(self, sequence): -# self.assert_sleep_efficiency(sequence, expected_nb_awakenings=1) - -# def test_many_awakening(self, sequence, nb_awakenings): -# self.assert_sleep_efficiency(sequence, expected_nb_awakenings=nb_awakenings) - -# def assert_sleep_efficiency(self, sequence, expected_nb_awakenings): -# sequence = convert_sleep_stage_name_to_values(sequence) -# response = ClassificationResponse(self.MOCK_REQUEST, sequence, None) -# report = response.report - -# assert report['awakenings'] == expected_nb_awakenings +class TestReportAwakenings(): + """Tests number of awakenings per night + "awakenings": 7, // number of times the subject woke up between sleep onset & offset + """ + params = { + 'test_sleep_time_null': [ + dict(sequence=['W', 'W', 'W']), + dict(sequence=['W']), + ], 'test_one_awakening': [ + dict(sequence=['W', 'N1', 'W']), + dict(sequence=['W', 'N1', 'N2', 'N3', 'W', 'W']), + ], 'test_doesnt_awaken': [ + dict(sequence=['W', 'N1', 'N2', 'N3', 'REM']), + dict(sequence=['W', 'N1']), + ], 'test_many_awakening': [ + dict(sequence=['W', 'N1', 'W', 'N1', 'W'], nb_awakenings=2), + dict(sequence=['W', 'N1', 'N2', 'W', 'N1', 'W', 'W', 'N1', 'W'], nb_awakenings=3), + ], + } + + @classmethod + def setup_class(cls): + cls.MOCK_REQUEST = get_mock_request() + + def test_sleep_time_null(self, sequence): + self.assert_sleep_efficiency(sequence, expected_nb_awakenings=0) + + def test_one_awakening(self, sequence): + self.assert_sleep_efficiency(sequence, expected_nb_awakenings=1) + + def test_doesnt_awaken(self, sequence): + self.assert_sleep_efficiency(sequence, expected_nb_awakenings=1) + + def test_many_awakening(self, sequence, nb_awakenings): + self.assert_sleep_efficiency(sequence, expected_nb_awakenings=nb_awakenings) + + def assert_sleep_efficiency(self, sequence, expected_nb_awakenings): + sequence = convert_sleep_stage_name_to_values(sequence) + response = ClassificationResponse(self.MOCK_REQUEST, sequence, None) + report = response.report + + assert report['awakenings'] == expected_nb_awakenings class TestReportStageShifts(): From e8b4767c6efd5a85a3f24cbe4acb6af9b437f691 Mon Sep 17 00:00:00 2001 From: Claudia Onorato Date: Wed, 11 Nov 2020 18:33:39 -0500 Subject: [PATCH 22/29] return -1 timestamps in case user doesnt sleep --- backend/backend/metric.py | 31 ++++++++++------ backend/tests/test_response.py | 65 +++++++++------------------------- 2 files changed, 38 insertions(+), 58 deletions(-) diff --git a/backend/backend/metric.py b/backend/backend/metric.py index 8a661f46..d06cc63f 100644 --- a/backend/backend/metric.py +++ b/backend/backend/metric.py @@ -3,6 +3,8 @@ from classification.config.constants import SleepStage, EPOCH_DURATION +INVALID_TIMESTAMP = -1 + def get_metrics(sleep_stages, bedtime): independent_metrics = { @@ -12,14 +14,15 @@ def get_metrics(sleep_stages, bedtime): **_get_transition_based_metrics(sleep_stages) } onsets = _get_onsets(independent_metrics, bedtime) - sleep_time = independent_metrics['sleepOffset'] - onsets['sleepOnset'] - waso = dict(WASO=sleep_time - independent_metrics['efficientSleepTime']) + sleep_time = independent_metrics['sleepOffset'] - \ + onsets['sleepOnset'] if INVALID_TIMESTAMP == onsets['sleepOnset'] else 0 + waso = sleep_time - independent_metrics['efficientSleepTime'] return { **independent_metrics, **onsets, # not tested - **waso, + "WASO": waso, "SleepTime": sleep_time, } @@ -41,6 +44,12 @@ def _get_sleep_vs_wake_metrics(sequence, bedtime): def _get_sleep_offset_with_wake(sleep_indexes, sequence, bedtime): + if not _has_slept(sequence): + return { + 'sleepOffset': INVALID_TIMESTAMP, + 'wakeAfterSleepOffset': 0, + } + sleep_nb_epochs = (sleep_indexes[-1] + 1) if len(sleep_indexes) else len(sequence) wake_after_sleep_offset_nb_epochs = ( len(sequence) - sleep_indexes[-1] - 1 @@ -96,8 +105,7 @@ def _get_transition_based_metrics(sequence): nb_awakenings = sum(awakenings_occurences) is_last_stage_sleep = sequence[-1] != SleepStage.W.name - has_slept = len(np.unique(sequence)) != 1 or np.unique(sequence)[0] != SleepStage.W.name - if is_last_stage_sleep and has_slept: + if is_last_stage_sleep and _has_slept(sequence): nb_stage_shifts += 1 nb_awakenings += 1 @@ -107,14 +115,17 @@ def _get_transition_based_metrics(sequence): def _get_onsets(independent_metrics, bedtime): return { "sleepOnset": ( - independent_metrics['sleepLatency'] if independent_metrics['sleepLatency'] >= 0 else 0 - ) + bedtime, + independent_metrics['sleepLatency'] + + bedtime if independent_metrics['sleepLatency'] >= 0 else INVALID_TIMESTAMP), "remOnset": ( - independent_metrics['remLatency'] if independent_metrics['remLatency'] >= 0 else 0 - ) + bedtime - } + independent_metrics['remLatency'] + + bedtime if independent_metrics['remLatency'] >= 0 else INVALID_TIMESTAMP)} def _get_latency_of_stage(sequence_is_stage): epochs_of_stage_of_interest = np.where(sequence_is_stage)[0] return int(-1 if epochs_of_stage_of_interest.shape[0] == 0 else epochs_of_stage_of_interest[0] * EPOCH_DURATION) + + +def _has_slept(sequence): + return len(np.unique(sequence)) != 1 or np.unique(sequence)[0] != SleepStage.W.name diff --git a/backend/tests/test_response.py b/backend/tests/test_response.py index 26c6e315..e994b9af 100644 --- a/backend/tests/test_response.py +++ b/backend/tests/test_response.py @@ -141,50 +141,21 @@ class TestReportLatenciesOnset(): params = { "test_sequence_starts_with_stage": [ - dict( - sequence=['REM', 'REM', 'W', 'W'], - test_rem=True, - ), dict( - sequence=['REM', 'W', 'N1', 'W'], - test_rem=True, - ), dict( - sequence=['REM', 'W', 'N1', 'N2', 'N3', 'REM', 'W'], - test_rem=False, - ), dict( - sequence=['N1', 'W', 'N1', 'N2', 'N3', 'REM', 'W'], - test_rem=False, - ), dict( - sequence=['N2', 'W', 'N1', 'N2', 'N3', 'REM', 'W'], - test_rem=False, - ), dict( - sequence=['N3', 'W', 'N1', 'N2', 'N3', 'REM', 'W'], - test_rem=False, - ), + dict(sequence=['REM', 'REM', 'W', 'W'], test_rem=True), + dict(sequence=['REM', 'W', 'N1', 'W'], test_rem=True), + dict(sequence=['REM', 'W', 'N1', 'N2', 'N3', 'REM', 'W'], test_rem=False), + dict(sequence=['N1', 'W', 'N1', 'N2', 'N3', 'REM', 'W'], test_rem=False), + dict(sequence=['N2', 'W', 'N1', 'N2', 'N3', 'REM', 'W'], test_rem=False), + dict(sequence=['N3', 'W', 'N1', 'N2', 'N3', 'REM', 'W'], test_rem=False), ], "test_sequence_has_no_stage": [ - dict( - sequence=['W', 'N1', 'N2', 'N3', 'W'], - test_rem=True, - ), dict( - sequence=['W', 'W', 'W', 'W', 'W'], - test_rem=False, - ), dict( - sequence=['W', ], - test_rem=False, - ), + dict(sequence=['W', 'N1', 'N2', 'N3', 'W'], test_rem=True), + dict(sequence=['W', 'W', 'W', 'W', 'W'], test_rem=False), + dict(sequence=['W'], test_rem=False), ], "test_sequence_ends_with_stage": [ - dict( - sequence=['W', 'W', 'REM'], - test_rem=True, - ), dict( - sequence=['W', 'W', 'N1'], - test_rem=False, - ), dict( - sequence=['W', 'W', 'N2'], - test_rem=False, - ), dict( - sequence=['W', 'W', 'N3'], - test_rem=False, - ), + dict(sequence=['W', 'W', 'REM'], test_rem=True), + dict(sequence=['W', 'W', 'N1'], test_rem=False), + dict(sequence=['W', 'W', 'N2'], test_rem=False), + dict(sequence=['W', 'W', 'N3'], test_rem=False), ], "test_sequence_with_stage_at_middle": [ dict( sequence=['W', 'N1', 'N2', 'N1', 'REM', 'W'], @@ -217,7 +188,7 @@ def test_sequence_starts_with_stage(self, sequence, test_rem): def test_sequence_has_no_stage(self, sequence, test_rem): expected_latency = -1 - expected_onset = self.MOCK_REQUEST.bedtime + expected_onset = -1 self.assert_latency_equals_expected(expected_latency, expected_onset, sequence, test_rem) def test_sequence_ends_with_stage(self, sequence, test_rem): @@ -297,7 +268,7 @@ def test_awakes_and_goes_back_to_sleep_and_doesnt_awake(self, sequence): self.assert_sleep_offset_with_wake(sequence, expected_sleep_offset, expected_wake_after_sleep_offset) def test_always_awake(self, sequence): - expected_sleep_offset = self.MOCK_REQUEST.bedtime + EPOCH_DURATION * len(sequence) + expected_sleep_offset = -1 expected_wake_after_sleep_offset = 0 self.assert_sleep_offset_with_wake(sequence, expected_sleep_offset, expected_wake_after_sleep_offset) @@ -374,10 +345,8 @@ class TestReportAwakenings(): "awakenings": 7, // number of times the subject woke up between sleep onset & offset """ params = { - 'test_sleep_time_null': [ - dict(sequence=['W', 'W', 'W']), - dict(sequence=['W']), - ], 'test_one_awakening': [ + 'test_sleep_time_null': [dict(sequence=['W', 'W', 'W']), dict(sequence=['W'])], + 'test_one_awakening': [ dict(sequence=['W', 'N1', 'W']), dict(sequence=['W', 'N1', 'N2', 'N3', 'W', 'W']), ], 'test_doesnt_awaken': [ From 9c9e589e65a7f0d4c589b8e835a65676a4ef06ac Mon Sep 17 00:00:00 2001 From: Claudia Onorato Date: Wed, 11 Nov 2020 19:29:56 -0500 Subject: [PATCH 23/29] refactored metrics for the 2nd time ...... --- backend/backend/metric.py | 257 ++++++++++++++++++--------------- backend/backend/response.py | 35 ++--- backend/tests/test_response.py | 14 +- 3 files changed, 162 insertions(+), 144 deletions(-) diff --git a/backend/backend/metric.py b/backend/backend/metric.py index d06cc63f..5364e080 100644 --- a/backend/backend/metric.py +++ b/backend/backend/metric.py @@ -6,126 +6,143 @@ INVALID_TIMESTAMP = -1 -def get_metrics(sleep_stages, bedtime): - independent_metrics = { - **_get_rem_latency(sleep_stages), - **_get_time_passed_in_stage(sleep_stages), - **_get_sleep_vs_wake_metrics(sleep_stages, bedtime), - **_get_transition_based_metrics(sleep_stages) - } - onsets = _get_onsets(independent_metrics, bedtime) - sleep_time = independent_metrics['sleepOffset'] - \ - onsets['sleepOnset'] if INVALID_TIMESTAMP == onsets['sleepOnset'] else 0 - waso = sleep_time - independent_metrics['efficientSleepTime'] - - return { - **independent_metrics, - **onsets, - # not tested - "WASO": waso, - "SleepTime": sleep_time, - } - - -def _get_sleep_vs_wake_metrics(sequence, bedtime): - """ - "sleepEfficiency": 0.8733, # Overall sense of how well the patient slept(totalSleepTime / bedTime) - "efficientSleepTime": 27113, # Total amount of seconds passed in non - wake stages - """ - sleep_condition = sequence != SleepStage.W.name - sleep_indexes = np.where(sleep_condition)[0] - - return { - "sleepEfficiency": sleep_indexes.shape[0] / sequence.shape[0], - "efficientSleepTime": sleep_indexes.shape[0] * EPOCH_DURATION, - "sleepLatency": _get_latency_of_stage(sleep_condition), - **_get_sleep_offset_with_wake(sleep_indexes, sequence, bedtime) - } - - -def _get_sleep_offset_with_wake(sleep_indexes, sequence, bedtime): - if not _has_slept(sequence): +class Metrics(): + def __init__(self, sleep_stages, bedtime): + self.sleep_stages = sleep_stages + self.bedtime = bedtime + self.has_slept = len(np.unique(self.sleep_stages)) != 1 or np.unique(self.sleep_stages)[0] != SleepStage.W.name + + self.is_sleeping_stages = self.sleep_stages != SleepStage.W.name + self.sleep_indexes = np.where(self.is_sleeping_stages)[0] + + self._initialize_sleep_offset() + self._initialize_sleep_latency() + self._initialize_rem_latency() + self._initialize_transition_based_metrics() + + @property + def report(self): return { - 'sleepOffset': INVALID_TIMESTAMP, - 'wakeAfterSleepOffset': 0, + 'sleepOffset': self._sleep_offset, + 'sleepLatency': self._sleep_latency, + 'remLatency': self._rem_latency, + 'awakenings': self._awakenings, + 'stageShifts': self._stage_shifts, + 'sleepTime': self._sleep_time, + 'WASO': self._wake_after_sleep_onset, + 'sleepEfficiency': self._sleep_efficiency, + 'efficientSleepTime': self._efficient_sleep_time, + 'wakeAfterSleepOffset': self._wake_after_sleep_offset, + 'sleepOnset': self._sleep_onset, + 'remOnset': self._rem_onset, + **self._time_passed_in_stage, } - sleep_nb_epochs = (sleep_indexes[-1] + 1) if len(sleep_indexes) else len(sequence) - wake_after_sleep_offset_nb_epochs = ( - len(sequence) - sleep_indexes[-1] - 1 - ) if len(sleep_indexes) and sequence[-1] == SleepStage.W.name else 0 - - return { - 'sleepOffset': sleep_nb_epochs * EPOCH_DURATION + bedtime, - 'wakeAfterSleepOffset': wake_after_sleep_offset_nb_epochs * EPOCH_DURATION, - } - - -def _get_rem_latency(sequence): - """Tests the time it took to enter a specific stage - Input: - - sequence: np.array of the SleepStage labels - """ - - return { - "remLatency": _get_latency_of_stage(sequence == SleepStage.REM.name), - } - - -def _get_time_passed_in_stage(sequence): - """Calculates time passed in each stage for all of the sequence - Input: - - sequence: list or np.array of the SleepStage labels - """ - nb_epoch_passed_by_stage = Counter(sequence) - - def get_time_passed(stage): - return EPOCH_DURATION * nb_epoch_passed_by_stage[stage] if stage in nb_epoch_passed_by_stage else 0 - - return { - f"{stage.upper()}Time": get_time_passed(stage) - for stage in SleepStage.tolist() - } - - -def _get_transition_based_metrics(sequence): - consecutive_stages_occurences = Counter(zip(sequence[:-1], sequence[1:])) - occurences_by_transition = { - consecutive_stages: consecutive_stages_occurences[consecutive_stages] - for consecutive_stages in consecutive_stages_occurences if consecutive_stages[0] != consecutive_stages[1] - } - transition_occurences = list(occurences_by_transition.values()) - awakenings_occurences = [ - occurences_by_transition[transition_stages] - for transition_stages in occurences_by_transition - if transition_stages[0] != SleepStage.W.name - and transition_stages[1] == SleepStage.W.name - ] - nb_stage_shifts = sum(transition_occurences) - nb_awakenings = sum(awakenings_occurences) - - is_last_stage_sleep = sequence[-1] != SleepStage.W.name - if is_last_stage_sleep and _has_slept(sequence): - nb_stage_shifts += 1 - nb_awakenings += 1 - - return dict(stageShifts=nb_stage_shifts, awakenings=nb_awakenings) - - -def _get_onsets(independent_metrics, bedtime): - return { - "sleepOnset": ( - independent_metrics['sleepLatency'] - + bedtime if independent_metrics['sleepLatency'] >= 0 else INVALID_TIMESTAMP), - "remOnset": ( - independent_metrics['remLatency'] - + bedtime if independent_metrics['remLatency'] >= 0 else INVALID_TIMESTAMP)} - - -def _get_latency_of_stage(sequence_is_stage): - epochs_of_stage_of_interest = np.where(sequence_is_stage)[0] - return int(-1 if epochs_of_stage_of_interest.shape[0] == 0 else epochs_of_stage_of_interest[0] * EPOCH_DURATION) - - -def _has_slept(sequence): - return len(np.unique(sequence)) != 1 or np.unique(sequence)[0] != SleepStage.W.name + @property + def _sleep_time(self): + if not self.has_slept: + return 0 + + return self._sleep_offset - self._sleep_onset + + @property + def _wake_after_sleep_onset(self): + if not self.has_slept: + return 0 + + return self._sleep_time - self._efficient_sleep_time + + @property + def _time_passed_in_stage(self): + """Calculates time passed in each stage for all of the sequence""" + nb_epoch_passed_by_stage = Counter(self.sleep_stages) + + def get_time_passed(stage): + return EPOCH_DURATION * nb_epoch_passed_by_stage[stage] if stage in nb_epoch_passed_by_stage else 0 + + return { + f"{stage.upper()}Time": get_time_passed(stage) + for stage in SleepStage.tolist() + } + + @property + def _sleep_efficiency(self): + return self.sleep_indexes.shape[0] / self.sleep_stages.shape[0] + + @property + def _efficient_sleep_time(self): + return self.sleep_indexes.shape[0] * EPOCH_DURATION + + @property + def _wake_after_sleep_offset(self): + if not self.has_slept: + return 0 + + wake_after_sleep_offset_nb_epochs = ( + len(self.sleep_stages) - self.sleep_indexes[-1] - 1 + ) if len(self.sleep_indexes) and self.sleep_stages[-1] == SleepStage.W.name else 0 + + return wake_after_sleep_offset_nb_epochs * EPOCH_DURATION + + @property + def _sleep_onset(self): + if not self.has_slept: + return INVALID_TIMESTAMP + + return self._sleep_latency + self.bedtime + + @property + def _rem_onset(self): + rem_latency = self._rem_latency + if rem_latency < 0: + return INVALID_TIMESTAMP + + return rem_latency + self.bedtime + + def _initialize_sleep_offset(self): + if not self.has_slept: + sleep_offset = INVALID_TIMESTAMP + else: + sleep_nb_epochs = (self.sleep_indexes[-1] + 1) if len(self.sleep_indexes) else len(self.sleep_stages) + sleep_offset = sleep_nb_epochs * EPOCH_DURATION + self.bedtime + + self._sleep_offset = sleep_offset + + def _initialize_sleep_latency(self): + self._sleep_latency = self._get_latency_of_stage(self.is_sleeping_stages) + + def _initialize_rem_latency(self): + """Time it took to enter REM stage""" + self._rem_latency = self._get_latency_of_stage(self.sleep_stages == SleepStage.REM.name) + + def _initialize_transition_based_metrics(self): + consecutive_stages_occurences = Counter(zip(self.sleep_stages[:-1], self.sleep_stages[1:])) + occurences_by_transition = { + consecutive_stages: consecutive_stages_occurences[consecutive_stages] + for consecutive_stages in consecutive_stages_occurences if consecutive_stages[0] != consecutive_stages[1] + } + transition_occurences = list(occurences_by_transition.values()) + awakenings_occurences = [ + occurences_by_transition[transition_stages] + for transition_stages in occurences_by_transition + if transition_stages[0] != SleepStage.W.name + and transition_stages[1] == SleepStage.W.name + ] + nb_stage_shifts = sum(transition_occurences) + nb_awakenings = sum(awakenings_occurences) + + is_last_stage_sleep = self.sleep_stages[-1] != SleepStage.W.name + if is_last_stage_sleep and self.has_slept: + nb_stage_shifts += 1 + nb_awakenings += 1 + + self._stage_shifts = nb_stage_shifts + self._awakenings = nb_awakenings + + def _get_latency_of_stage(self, sequence_is_stage): + epochs_of_stage_of_interest = np.where(sequence_is_stage)[0] + + if not epochs_of_stage_of_interest.shape[0]: + return -1 + + return int(epochs_of_stage_of_interest[0] * EPOCH_DURATION) diff --git a/backend/backend/response.py b/backend/backend/response.py index 24f418d6..c0439270 100644 --- a/backend/backend/response.py +++ b/backend/backend/response.py @@ -1,6 +1,6 @@ import numpy as np -from backend.metric import get_metrics +from backend.metric import Metrics from classification.config.constants import EPOCH_DURATION, SleepStage @@ -17,6 +17,7 @@ def __init__(self, request, predictions, spectrogram): self.spectrogram = spectrogram self.predictions = predictions + self.metrics = Metrics(self.sleep_stages, self.bedtime) @property def sleep_stages(self): @@ -24,12 +25,23 @@ def sleep_stages(self): return ordered_sleep_stage_names[self.predictions] @property - def epochs(self): + def response(self): + return { + 'epochs': self._epochs, + 'report': self._report, + 'metadata': self._metadata, + 'subject': self.subject, + 'board': self.board.name, + 'spectrograms': self.spectrogram, + } + + @property + def _epochs(self): timestamps = np.arange(self.n_epochs * EPOCH_DURATION, step=EPOCH_DURATION) + self.bedtime return {'timestamps': timestamps.tolist(), 'stages': self.sleep_stages.tolist()} @property - def metadata(self): + def _metadata(self): return { "sessionStartTime": self.stream_start, "sessionEndTime": self.stream_duration + self.stream_start, @@ -40,23 +52,12 @@ def metadata(self): } @property - def subject(self): + def _subject(self): return { 'age': self.age, 'sex': self.sex.name, } @property - def report(self): - return get_metrics(self.sleep_stages, self.bedtime) - - @property - def response(self): - return { - 'epochs': self.epochs, - 'report': self.report, - 'metadata': self.metadata, - 'subject': self.subject, - 'board': self.board.name, - 'spectrograms': self.spectrogram, - } + def _report(self): + return self.metrics.report diff --git a/backend/tests/test_response.py b/backend/tests/test_response.py index e994b9af..a87c1699 100644 --- a/backend/tests/test_response.py +++ b/backend/tests/test_response.py @@ -102,7 +102,7 @@ def setup_class(cls): def test_null_time_passed_in_stage(self, sequence, WTime, REMTime, N1Time, N2Time, N3Time): value_sequence = convert_sleep_stage_name_to_values(sequence) response = ClassificationResponse(self.MOCK_REQUEST, value_sequence, None) - report = response.report + report = response.metrics.report assert report[sequence[0].upper() + 'Time'] == len(sequence) * EPOCH_DURATION self.assert_times(sequence, report, WTime, REMTime, N1Time, N2Time, N3Time) @@ -110,7 +110,7 @@ def test_null_time_passed_in_stage(self, sequence, WTime, REMTime, N1Time, N2Tim def test_partial_time_passed_in_stage(self, sequence, WTime, REMTime, N1Time, N2Time, N3Time): sequence = convert_sleep_stage_name_to_values(sequence) response = ClassificationResponse(self.MOCK_REQUEST, sequence, None) - report = response.report + report = response.metrics.report self.assert_times(sequence, report, WTime, REMTime, N1Time, N2Time, N3Time) def assert_times(self, sequence, report, WTime, REMTime, N1Time, N2Time, N3Time): @@ -209,7 +209,7 @@ def get_onset_report_key(self, test_rem): def assert_latency_equals_expected(self, expected_latency, expected_onset, sequence, test_rem): sequence = convert_sleep_stage_name_to_values(sequence) response = ClassificationResponse(self.MOCK_REQUEST, sequence, None) - report = response.report + report = response.metrics.report assert report[self.get_latency_report_key(test_rem)] == expected_latency, ( f"Latency of {'rem' if test_rem else 'sleep'} is not as expected" @@ -280,7 +280,7 @@ def test_doesnt_awaken(self, sequence): def assert_sleep_offset_with_wake(self, sequence, expected_sleep_offset, expected_wake_after_sleep_offset): sequence = convert_sleep_stage_name_to_values(sequence) response = ClassificationResponse(self.MOCK_REQUEST, sequence, None) - report = response.report + report = response.metrics.report assert report['sleepOffset'] == expected_sleep_offset assert report['wakeAfterSleepOffset'] == expected_wake_after_sleep_offset @@ -334,7 +334,7 @@ def test_sleep_time_not_null(self, sequence, expected_efficiency, expected_effic def assert_sleep_efficiency(self, sequence, expected_efficiency, expected_efficient_sleep_time): sequence = convert_sleep_stage_name_to_values(sequence) response = ClassificationResponse(self.MOCK_REQUEST, sequence, None) - report = response.report + report = response.metrics.report assert report['sleepEfficiency'] == expected_efficiency assert report['efficientSleepTime'] == expected_efficient_sleep_time @@ -377,7 +377,7 @@ def test_many_awakening(self, sequence, nb_awakenings): def assert_sleep_efficiency(self, sequence, expected_nb_awakenings): sequence = convert_sleep_stage_name_to_values(sequence) response = ClassificationResponse(self.MOCK_REQUEST, sequence, None) - report = response.report + report = response.metrics.report assert report['awakenings'] == expected_nb_awakenings @@ -430,6 +430,6 @@ def test_many_sleep_stages(self, sequence, sleep_shifts): def assert_sleep_efficiency(self, sequence, expected_sleep_shifts): sequence = convert_sleep_stage_name_to_values(sequence) response = ClassificationResponse(self.MOCK_REQUEST, sequence, None) - report = response.report + report = response.metrics.report assert report['stageShifts'] == expected_sleep_shifts From 8eebed8f577cea3cbc66ed9b63f59e8f93e59ece Mon Sep 17 00:00:00 2001 From: Claudia Onorato Date: Wed, 11 Nov 2020 20:00:09 -0500 Subject: [PATCH 24/29] fixed server errors --- backend/backend/metric.py | 9 +++++++- backend/backend/response.py | 2 +- backend/tests/test_response.py | 40 ++++++++++------------------------ 3 files changed, 20 insertions(+), 31 deletions(-) diff --git a/backend/backend/metric.py b/backend/backend/metric.py index 5364e080..625bfa35 100644 --- a/backend/backend/metric.py +++ b/backend/backend/metric.py @@ -22,7 +22,7 @@ def __init__(self, sleep_stages, bedtime): @property def report(self): - return { + report = { 'sleepOffset': self._sleep_offset, 'sleepLatency': self._sleep_latency, 'remLatency': self._rem_latency, @@ -38,6 +38,13 @@ def report(self): **self._time_passed_in_stage, } + for metric in report: + # json does not recognize NumPy data types + if isinstance(report[metric], np.int64): + report[metric] = int(report[metric]) + + return report + @property def _sleep_time(self): if not self.has_slept: diff --git a/backend/backend/response.py b/backend/backend/response.py index c0439270..76b2f3f3 100644 --- a/backend/backend/response.py +++ b/backend/backend/response.py @@ -30,7 +30,7 @@ def response(self): 'epochs': self._epochs, 'report': self._report, 'metadata': self._metadata, - 'subject': self.subject, + 'subject': self._subject, 'board': self.board.name, 'spectrograms': self.spectrogram, } diff --git a/backend/tests/test_response.py b/backend/tests/test_response.py index a87c1699..68feee9c 100644 --- a/backend/tests/test_response.py +++ b/backend/tests/test_response.py @@ -13,8 +13,10 @@ from classification.config.constants import EPOCH_DURATION, SleepStage -def convert_sleep_stage_name_to_values(sequence): - return np.array([SleepStage[stage].value for stage in sequence]) +def get_report(request, sequence): + value_sequence = np.array([SleepStage[stage].value for stage in sequence]) + response = ClassificationResponse(request, value_sequence, None) + return response.metrics.report class TestReportTimePassedInStage(): @@ -100,17 +102,12 @@ def setup_class(cls): cls.MOCK_REQUEST = get_mock_request() def test_null_time_passed_in_stage(self, sequence, WTime, REMTime, N1Time, N2Time, N3Time): - value_sequence = convert_sleep_stage_name_to_values(sequence) - response = ClassificationResponse(self.MOCK_REQUEST, value_sequence, None) - report = response.metrics.report - + report = get_report(self.MOCK_REQUEST, sequence) assert report[sequence[0].upper() + 'Time'] == len(sequence) * EPOCH_DURATION self.assert_times(sequence, report, WTime, REMTime, N1Time, N2Time, N3Time) def test_partial_time_passed_in_stage(self, sequence, WTime, REMTime, N1Time, N2Time, N3Time): - sequence = convert_sleep_stage_name_to_values(sequence) - response = ClassificationResponse(self.MOCK_REQUEST, sequence, None) - report = response.metrics.report + report = get_report(self.MOCK_REQUEST, sequence) self.assert_times(sequence, report, WTime, REMTime, N1Time, N2Time, N3Time) def assert_times(self, sequence, report, WTime, REMTime, N1Time, N2Time, N3Time): @@ -207,10 +204,7 @@ def get_onset_report_key(self, test_rem): return 'remOnset' if test_rem else 'sleepOnset' def assert_latency_equals_expected(self, expected_latency, expected_onset, sequence, test_rem): - sequence = convert_sleep_stage_name_to_values(sequence) - response = ClassificationResponse(self.MOCK_REQUEST, sequence, None) - report = response.metrics.report - + report = get_report(self.MOCK_REQUEST, sequence) assert report[self.get_latency_report_key(test_rem)] == expected_latency, ( f"Latency of {'rem' if test_rem else 'sleep'} is not as expected" ) @@ -278,10 +272,7 @@ def test_doesnt_awaken(self, sequence): self.assert_sleep_offset_with_wake(sequence, expected_sleep_offset, expected_wake_after_sleep_offset) def assert_sleep_offset_with_wake(self, sequence, expected_sleep_offset, expected_wake_after_sleep_offset): - sequence = convert_sleep_stage_name_to_values(sequence) - response = ClassificationResponse(self.MOCK_REQUEST, sequence, None) - report = response.metrics.report - + report = get_report(self.MOCK_REQUEST, sequence) assert report['sleepOffset'] == expected_sleep_offset assert report['wakeAfterSleepOffset'] == expected_wake_after_sleep_offset @@ -332,10 +323,7 @@ def test_sleep_time_not_null(self, sequence, expected_efficiency, expected_effic self.assert_sleep_efficiency(sequence, expected_efficiency, expected_efficient_sleep_time) def assert_sleep_efficiency(self, sequence, expected_efficiency, expected_efficient_sleep_time): - sequence = convert_sleep_stage_name_to_values(sequence) - response = ClassificationResponse(self.MOCK_REQUEST, sequence, None) - report = response.metrics.report - + report = get_report(self.MOCK_REQUEST, sequence) assert report['sleepEfficiency'] == expected_efficiency assert report['efficientSleepTime'] == expected_efficient_sleep_time @@ -375,10 +363,7 @@ def test_many_awakening(self, sequence, nb_awakenings): self.assert_sleep_efficiency(sequence, expected_nb_awakenings=nb_awakenings) def assert_sleep_efficiency(self, sequence, expected_nb_awakenings): - sequence = convert_sleep_stage_name_to_values(sequence) - response = ClassificationResponse(self.MOCK_REQUEST, sequence, None) - report = response.metrics.report - + report = get_report(self.MOCK_REQUEST, sequence) assert report['awakenings'] == expected_nb_awakenings @@ -428,8 +413,5 @@ def test_many_sleep_stages(self, sequence, sleep_shifts): self.assert_sleep_efficiency(sequence, expected_sleep_shifts=sleep_shifts) def assert_sleep_efficiency(self, sequence, expected_sleep_shifts): - sequence = convert_sleep_stage_name_to_values(sequence) - response = ClassificationResponse(self.MOCK_REQUEST, sequence, None) - report = response.metrics.report - + report = get_report(self.MOCK_REQUEST, sequence) assert report['stageShifts'] == expected_sleep_shifts From 691b31e4fdce1d34a367cab5c81f78678395e418 Mon Sep 17 00:00:00 2001 From: Claudia Onorato Date: Wed, 11 Nov 2020 20:16:30 -0500 Subject: [PATCH 25/29] removed acquisition board (now we autodetect) && added tests instructions to readme --- backend/app.py | 4 +--- backend/backend/request.py | 3 +-- backend/backend/response.py | 2 -- backend/classification/config/constants.py | 5 ----- backend/readme.md | 8 ++++++++ 5 files changed, 10 insertions(+), 12 deletions(-) diff --git a/backend/app.py b/backend/app.py index 16401d8a..134de7b0 100644 --- a/backend/app.py +++ b/backend/app.py @@ -8,7 +8,7 @@ from backend.spectrogram_generator import SpectrogramGenerator from classification.parser import get_raw_array from classification.exceptions import ClassificationError -from classification.config.constants import Sex, AcquisitionBoard, ALLOWED_FILE_EXTENSIONS +from classification.config.constants import Sex, ALLOWED_FILE_EXTENSIONS from classification.model import SleepStagesClassifier from classification.features.preprocessing import preprocess @@ -51,7 +51,6 @@ def analyze_sleep(): form_data = request.form.to_dict() raw_array = get_raw_array(file) - print(AcquisitionBoard[form_data['device']]) try: classification_request = ClassificationRequest( @@ -60,7 +59,6 @@ def analyze_sleep(): stream_start=int(form_data['stream_start']), bedtime=int(form_data['bedtime']), wakeup=int(form_data['wakeup']), - board=AcquisitionBoard[form_data['device']], raw_eeg=raw_array, ) except (KeyError, ValueError, ClassificationError): diff --git a/backend/backend/request.py b/backend/backend/request.py index ce253b71..157ad582 100644 --- a/backend/backend/request.py +++ b/backend/backend/request.py @@ -12,13 +12,12 @@ class ClassificationRequest(): - def __init__(self, sex, age, stream_start, bedtime, wakeup, board, raw_eeg, stream_duration=None): + def __init__(self, sex, age, stream_start, bedtime, wakeup, raw_eeg, stream_duration=None): self.sex = sex self.age = age self.stream_start = stream_start self.bedtime = bedtime self.wakeup = wakeup - self.board = board self.raw_eeg = raw_eeg self.stream_duration = stream_duration if stream_duration else self.raw_eeg.times[-1] + ( 1 / self.raw_eeg.info['sfreq']) diff --git a/backend/backend/response.py b/backend/backend/response.py index 76b2f3f3..9b5c6455 100644 --- a/backend/backend/response.py +++ b/backend/backend/response.py @@ -12,7 +12,6 @@ def __init__(self, request, predictions, spectrogram): self.stream_duration = request.stream_duration self.bedtime = request.bedtime self.wakeup = request.wakeup - self.board = request.board self.n_epochs = request.n_epochs self.spectrogram = spectrogram @@ -31,7 +30,6 @@ def response(self): 'report': self._report, 'metadata': self._metadata, 'subject': self._subject, - 'board': self.board.name, 'spectrograms': self.spectrogram, } diff --git a/backend/classification/config/constants.py b/backend/classification/config/constants.py index 0586de81..2d7bd3f9 100644 --- a/backend/classification/config/constants.py +++ b/backend/classification/config/constants.py @@ -8,11 +8,6 @@ class Sex(Enum): M = 2 -class AcquisitionBoard(Enum): - OPENBCI_CYTON = 1 - OPENBCI_GANGLION = 2 - - class SleepStage(Enum): W = 0 N1 = 1 diff --git a/backend/readme.md b/backend/readme.md index b7d97173..07e72f14 100644 --- a/backend/readme.md +++ b/backend/readme.md @@ -40,3 +40,11 @@ If you want to run the backend with hot reload enabled (you must have installed ```bash hupper -m waitress app:app ``` + +## Run the tests + +You can run our unit tests with the following command, after installing the development requirements: + +```bash +pytest +``` From 5d651434bf8bfa330cdc6f8cd0d89380b3a09197 Mon Sep 17 00:00:00 2001 From: Claudia Onorato Date: Wed, 11 Nov 2020 20:33:55 -0500 Subject: [PATCH 26/29] extracted stream duration --- backend/backend/request.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/backend/request.py b/backend/backend/request.py index 157ad582..9e872cc4 100644 --- a/backend/backend/request.py +++ b/backend/backend/request.py @@ -19,11 +19,14 @@ def __init__(self, sex, age, stream_start, bedtime, wakeup, raw_eeg, stream_dura self.bedtime = bedtime self.wakeup = wakeup self.raw_eeg = raw_eeg - self.stream_duration = stream_duration if stream_duration else self.raw_eeg.times[-1] + ( - 1 / self.raw_eeg.info['sfreq']) + self.stream_duration = stream_duration if stream_duration else self._get_stream_duration() self._validate() + def _get_stream_duration(self): + PERIOD_DURATION = 1 / self.raw_eeg.info['sfreq'] + return self.raw_eeg.times[-1] + PERIOD_DURATION + @property def in_bed_seconds(self): """timespan, in seconds, from which the subject started the recording and went to bed""" From 9101fa37d2fe14c291b4c4206ae12d5b874fd928 Mon Sep 17 00:00:00 2001 From: Claudia Onorato Date: Wed, 11 Nov 2020 20:35:52 -0500 Subject: [PATCH 27/29] fixed tests --- backend/tests/setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/tests/setup.py b/backend/tests/setup.py index 26caabcc..3aa1526a 100644 --- a/backend/tests/setup.py +++ b/backend/tests/setup.py @@ -1,7 +1,7 @@ from unittest.mock import patch from backend.request import ClassificationRequest -from classification.config.constants import Sex, AcquisitionBoard +from classification.config.constants import Sex def pytest_generate_tests(metafunc): @@ -21,7 +21,6 @@ def get_mock_request(): stream_start=1582418280, bedtime=1582423980, wakeup=1582452240, - board=AcquisitionBoard.OPENBCI_CYTON, raw_eeg=None, stream_duration=35760, ) From 10e74e2c0577467a15dcee1f44bcc524ae0088b8 Mon Sep 17 00:00:00 2001 From: Claudia Onorato Date: Thu, 12 Nov 2020 11:15:20 -0500 Subject: [PATCH 28/29] code review --- backend/backend/metric.py | 20 +++++++++----------- backend/tests/test_response.py | 6 +++--- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/backend/backend/metric.py b/backend/backend/metric.py index 625bfa35..65b79efb 100644 --- a/backend/backend/metric.py +++ b/backend/backend/metric.py @@ -3,8 +3,6 @@ from classification.config.constants import SleepStage, EPOCH_DURATION -INVALID_TIMESTAMP = -1 - class Metrics(): def __init__(self, sleep_stages, bedtime): @@ -74,11 +72,11 @@ def get_time_passed(stage): @property def _sleep_efficiency(self): - return self.sleep_indexes.shape[0] / self.sleep_stages.shape[0] + return len(self.sleep_indexes) / len(self.sleep_stages) @property def _efficient_sleep_time(self): - return self.sleep_indexes.shape[0] * EPOCH_DURATION + return len(self.sleep_indexes) * EPOCH_DURATION @property def _wake_after_sleep_offset(self): @@ -94,21 +92,21 @@ def _wake_after_sleep_offset(self): @property def _sleep_onset(self): if not self.has_slept: - return INVALID_TIMESTAMP + return None return self._sleep_latency + self.bedtime @property def _rem_onset(self): rem_latency = self._rem_latency - if rem_latency < 0: - return INVALID_TIMESTAMP + if rem_latency is None: + return None return rem_latency + self.bedtime def _initialize_sleep_offset(self): if not self.has_slept: - sleep_offset = INVALID_TIMESTAMP + sleep_offset = None else: sleep_nb_epochs = (self.sleep_indexes[-1] + 1) if len(self.sleep_indexes) else len(self.sleep_stages) sleep_offset = sleep_nb_epochs * EPOCH_DURATION + self.bedtime @@ -149,7 +147,7 @@ def _initialize_transition_based_metrics(self): def _get_latency_of_stage(self, sequence_is_stage): epochs_of_stage_of_interest = np.where(sequence_is_stage)[0] - if not epochs_of_stage_of_interest.shape[0]: - return -1 + if len(epochs_of_stage_of_interest) == 0: + return None - return int(epochs_of_stage_of_interest[0] * EPOCH_DURATION) + return epochs_of_stage_of_interest[0] * EPOCH_DURATION diff --git a/backend/tests/test_response.py b/backend/tests/test_response.py index 68feee9c..44cfee2e 100644 --- a/backend/tests/test_response.py +++ b/backend/tests/test_response.py @@ -184,8 +184,8 @@ def test_sequence_starts_with_stage(self, sequence, test_rem): self.assert_latency_equals_expected(expected_latency, expected_onset, sequence, test_rem) def test_sequence_has_no_stage(self, sequence, test_rem): - expected_latency = -1 - expected_onset = -1 + expected_latency = None + expected_onset = None self.assert_latency_equals_expected(expected_latency, expected_onset, sequence, test_rem) def test_sequence_ends_with_stage(self, sequence, test_rem): @@ -262,7 +262,7 @@ def test_awakes_and_goes_back_to_sleep_and_doesnt_awake(self, sequence): self.assert_sleep_offset_with_wake(sequence, expected_sleep_offset, expected_wake_after_sleep_offset) def test_always_awake(self, sequence): - expected_sleep_offset = -1 + expected_sleep_offset = None expected_wake_after_sleep_offset = 0 self.assert_sleep_offset_with_wake(sequence, expected_sleep_offset, expected_wake_after_sleep_offset) From 26bef353f730f9aa1406ece143bf59dd1159ddfe Mon Sep 17 00:00:00 2001 From: Claudia Onorato Date: Thu, 12 Nov 2020 11:32:10 -0500 Subject: [PATCH 29/29] code review --- backend/backend/metric.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/backend/metric.py b/backend/backend/metric.py index 65b79efb..c9c5615a 100644 --- a/backend/backend/metric.py +++ b/backend/backend/metric.py @@ -12,6 +12,7 @@ def __init__(self, sleep_stages, bedtime): self.is_sleeping_stages = self.sleep_stages != SleepStage.W.name self.sleep_indexes = np.where(self.is_sleeping_stages)[0] + self.is_last_stage_sleep = self.sleep_stages[-1] != SleepStage.W.name self._initialize_sleep_offset() self._initialize_sleep_latency() @@ -85,7 +86,7 @@ def _wake_after_sleep_offset(self): wake_after_sleep_offset_nb_epochs = ( len(self.sleep_stages) - self.sleep_indexes[-1] - 1 - ) if len(self.sleep_indexes) and self.sleep_stages[-1] == SleepStage.W.name else 0 + ) if not self.is_last_stage_sleep else 0 return wake_after_sleep_offset_nb_epochs * EPOCH_DURATION @@ -136,8 +137,7 @@ def _initialize_transition_based_metrics(self): nb_stage_shifts = sum(transition_occurences) nb_awakenings = sum(awakenings_occurences) - is_last_stage_sleep = self.sleep_stages[-1] != SleepStage.W.name - if is_last_stage_sleep and self.has_slept: + if self.is_last_stage_sleep and self.has_slept: nb_stage_shifts += 1 nb_awakenings += 1