From 5bfce3ff633bd0f583a67ddae27c57145eb8d377 Mon Sep 17 00:00:00 2001 From: Werr Date: Tue, 13 Jan 2026 16:14:42 +0100 Subject: [PATCH 01/14] Rewrote the usleep_utils.py to have one big wrapper function, which calls the actual functions Logical switches are whether you are using an EDF-file or an mne-Object, the backend you want to use and whether you want to use individual channels or channels groups --- sleep_utils/usleep_utils.py | 259 +++++++++++++++++++++++++++++++----- 1 file changed, 229 insertions(+), 30 deletions(-) diff --git a/sleep_utils/usleep_utils.py b/sleep_utils/usleep_utils.py index 3c09677..44d493c 100644 --- a/sleep_utils/usleep_utils.py +++ b/sleep_utils/usleep_utils.py @@ -13,6 +13,8 @@ import numpy as np import warnings import pandas as pd +import requests +from io import BytesIO def tempfile_wrapper(func): @wraps(func) @@ -43,10 +45,64 @@ def patched_request(self, endpoint, method, as_json=False, USleepAPI._request = patched_request warnings.warn('patched SSL to accept insecure connections') +def score_sleep(raw = None, + edf_file = None, + api_token = None, + backend = 'sleepyland', + backend_url = None, + eeg_chs=None, + eog_chs=None, + ch_groups=None, + model=None, + saveto=None, + seconds_per_label=30, + tmp_edf=None, + return_proba=False): + print ("1") + print (type(raw)) + if (raw): + return _score_sleep_raw(raw, + api_token = api_token, + backend = backend, + backend_url = backend_url, + eeg_chs = eeg_chs, + eog_chs = eog_chs, + ch_groups = ch_groups, + model = model, + saveto = saveto, + seconds_per_label = seconds_per_label, + return_proba = return_proba) + elif (edf_file): + return _score_sleep_file(edf_file, + api_token = api_token, + backend = backend, + backend_url = backend_url, + eeg_chs = eeg_chs, + eog_chs = eog_chs, + ch_groups = ch_groups, + model = model, + saveto = saveto, + seconds_per_label = seconds_per_label, + return_proba = return_proba) + else: + print("Requires either a valid EDF-file OR mne.io.Raw object") + + @tempfile_wrapper -def predict_usleep_raw(raw, api_token, eeg_chs=None, eog_chs=None, - ch_groups=None, model='U-Sleep v2.0', saveto=None, - seconds_per_label=30, tmp_edf=None, return_proba=False): +def _score_sleep_raw(raw, + api_token = None, + backend = 'sleepyland', + backend_url = None, + eeg_chs=None, + eog_chs=None, + ch_groups=None, + model='U-Sleep v2.0', + saveto=None, + seconds_per_label=30, + tmp_edf=None, + return_proba=False): + print ("2") + print (type(raw)) """ Run U-Sleep prediction on an mne.io.Raw object. @@ -60,6 +116,10 @@ def predict_usleep_raw(raw, api_token, eeg_chs=None, eog_chs=None, The raw EEG recording. api_token : str U-Sleep API token (https://sleep.ai.ku.dk). + backend : str + Which backend should be used for scoring. + backend_url : str + URL for different backends. eeg_chs : list of str, optional EEG channel names for prediction. eog_chs : list of str, optional @@ -88,11 +148,12 @@ def predict_usleep_raw(raw, api_token, eeg_chs=None, eog_chs=None, raw = raw.copy() # work on copy as we resample data etc. # convert to EDF file if not print('converting file to EDF') - chs = list(set(eeg_chs + eog_chs)) + if (eeg_chs and eog_chs): + chs = list(set(eeg_chs + eog_chs)) # chs_idx = [i for i, ch in enumerate(raw.ch_names) if ch in chs] # only keep channels that are actually requested - if any([ch not in raw.ch_names for ch in chs]): - raw.drop_channels([ch for ch in raw.ch_names if not ch in chs]) +# if any([ch not in raw.ch_names for ch in chs]): +# raw.drop_channels([ch for ch in raw.ch_names if not ch in chs]) # is resampled anyway internally, reduce data size if raw.info['sfreq']>128: @@ -100,10 +161,17 @@ def predict_usleep_raw(raw, api_token, eeg_chs=None, eog_chs=None, raw.resample(128, n_jobs=-2) mne.export.export_raw(tmp_edf, raw, fmt='edf', overwrite=True) - return predict_usleep(tmp_edf, api_token, eeg_chs=eeg_chs, eog_chs=eog_chs, - ch_groups=None, model=model, saveto=saveto, - seconds_per_label=seconds_per_label, - return_proba=return_proba) + return _score_sleep_file(tmp_edf, + api_token = api_token, + backend = backend, + backend_url = backend_url, + eeg_chs=eeg_chs, + eog_chs=eog_chs, + ch_groups=ch_groups, + model=model, + saveto=saveto, + seconds_per_label=seconds_per_label, + return_proba=return_proba) def delete_all_sessions(api_token): """convenience function to delete all sessions and data""" @@ -111,9 +179,17 @@ def delete_all_sessions(api_token): api = USleepAPI(api_token=api_token) api.delete_all_sessions() -def predict_usleep(edf_file, api_token, eeg_chs=None, eog_chs=None, - ch_groups=None, model='U-Sleep v2.0', saveto=None, - seconds_per_label=30, return_proba=False): +def _score_sleep_file(edf_file, + api_token = None, + backend = 'sleepyland', + backend_url=None, + eeg_chs=None, + eog_chs=None, + ch_groups=None, + model='U-Sleep v2.0', + saveto=None, + seconds_per_label=30, + return_proba=False): """ Run U-Sleep prediction on an EDF file via the U-Sleep API. @@ -124,6 +200,10 @@ class probabilities. ---------- edf_file : str Path to a local EDF file. + backend : str + Which backend should be used for scoring. + backend_url : str + URL for different backends. api_token : str U-Sleep API token (https://sleep.ai.ku.dk). eeg_chs : list of str, optional @@ -149,18 +229,79 @@ class probabilities. Label probabilities (if return_proba is True). """ from sleep_utils import write_hypno - try: - from usleep_api import USleepAPI - except ModuleNotFoundError as e: - raise(ModuleNotFoundError(f"{e}\n If missing, please install via 'pip install usleep_api --no-deps'")) # parameter checks +# from tools import write_hypno - if len(eeg_chs)==0 or len(eog_chs)==0: - raise ValueError('One element missing: {len(eeg_chs)=}, {len(eog_chs)=}') +# if len(eeg_chs)==0 or len(eog_chs)==0: +# raise ValueError('One element missing: {len(eeg_chs)=}, {len(eog_chs)=}') assert 0 Date: Wed, 14 Jan 2026 12:19:44 +0100 Subject: [PATCH 02/14] Update sleep_utils/usleep_utils.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- sleep_utils/usleep_utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sleep_utils/usleep_utils.py b/sleep_utils/usleep_utils.py index 44d493c..2f60de5 100644 --- a/sleep_utils/usleep_utils.py +++ b/sleep_utils/usleep_utils.py @@ -391,7 +391,8 @@ def _score_sleepyland(filename, 'models': model, 'channels': pairs, } - files = {'edf-files': open(filename, 'rb')} + with open(filename, 'rb') as f: + files = {'edf-files': f} print("waiting for prediction") From 7fcff3576dd22ecc8110a91c7ed7d53d7d3e0713 Mon Sep 17 00:00:00 2001 From: lamasmithueten <118809721+lamasmithueten@users.noreply.github.com> Date: Wed, 14 Jan 2026 12:19:54 +0100 Subject: [PATCH 03/14] Update sleep_utils/usleep_utils.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- sleep_utils/usleep_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sleep_utils/usleep_utils.py b/sleep_utils/usleep_utils.py index 2f60de5..09f14c9 100644 --- a/sleep_utils/usleep_utils.py +++ b/sleep_utils/usleep_utils.py @@ -239,10 +239,10 @@ class probabilities. 'must either supply eeg_chs and eog_chs OR ch_groups' #check if file exists - assert os.path.exists(edf_file), f"Error: '{filename}' does not exist." + assert os.path.exists(edf_file), f"Error: '{edf_file}' does not exist." #check if it is indeed an edf file - assert os.path.splitext(edf_file)[1].lower() == '.edf', f"File '{filename}' does not have an '.edf' extension." + assert os.path.splitext(edf_file)[1].lower() == '.edf', f"File '{edf_file}' does not have an '.edf' extension." try: raw = mne.io.read_raw_edf(edf_file) From 7f986eff2ff19f3a65ffeb4bda5bbe800f31ca53 Mon Sep 17 00:00:00 2001 From: lamasmithueten <118809721+lamasmithueten@users.noreply.github.com> Date: Wed, 14 Jan 2026 12:20:04 +0100 Subject: [PATCH 04/14] Update sleep_utils/usleep_utils.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- sleep_utils/usleep_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sleep_utils/usleep_utils.py b/sleep_utils/usleep_utils.py index 09f14c9..ff71bb5 100644 --- a/sleep_utils/usleep_utils.py +++ b/sleep_utils/usleep_utils.py @@ -415,6 +415,7 @@ def _score_sleepyland(filename, api_url = 'http://' + host + ':8888/files/output/' + folder_name + '/usleep/majority/' + file result = requests.get(api_url) + result.raise_for_status() # This will raise an HTTPError for bad responses (4xx or 5xx) #Create Probabilities From f0edf91d419432c2b64259f46624b3bd0b7353c5 Mon Sep 17 00:00:00 2001 From: lamasmithueten <118809721+lamasmithueten@users.noreply.github.com> Date: Wed, 14 Jan 2026 12:22:39 +0100 Subject: [PATCH 05/14] Update usleep_utils.py --- sleep_utils/usleep_utils.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/sleep_utils/usleep_utils.py b/sleep_utils/usleep_utils.py index ff71bb5..0c01f6b 100644 --- a/sleep_utils/usleep_utils.py +++ b/sleep_utils/usleep_utils.py @@ -58,8 +58,7 @@ def score_sleep(raw = None, seconds_per_label=30, tmp_edf=None, return_proba=False): - print ("1") - print (type(raw)) + if (raw): return _score_sleep_raw(raw, api_token = api_token, @@ -101,8 +100,7 @@ def _score_sleep_raw(raw, seconds_per_label=30, tmp_edf=None, return_proba=False): - print ("2") - print (type(raw)) + """ Run U-Sleep prediction on an mne.io.Raw object. @@ -424,3 +422,4 @@ def _score_sleepyland(filename, hypno = np.argmax(proba, axis=1).tolist() return (hypno, proba) if return_proba else hypno + From 7e6b62d4af55677bfd1d9b4f5ba5ba373c370e79 Mon Sep 17 00:00:00 2001 From: lamasmithueten <118809721+lamasmithueten@users.noreply.github.com> Date: Wed, 14 Jan 2026 13:37:10 +0100 Subject: [PATCH 06/14] Update sleep_utils/usleep_utils.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- sleep_utils/usleep_utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sleep_utils/usleep_utils.py b/sleep_utils/usleep_utils.py index 0c01f6b..9b15065 100644 --- a/sleep_utils/usleep_utils.py +++ b/sleep_utils/usleep_utils.py @@ -146,8 +146,10 @@ def _score_sleep_raw(raw, raw = raw.copy() # work on copy as we resample data etc. # convert to EDF file if not print('converting file to EDF') - if (eeg_chs and eog_chs): + if eeg_chs and eog_chs: chs = list(set(eeg_chs + eog_chs)) + elif ch_groups: + chs = {channel for channel_value in ch_groups for channel in channel_value} # chs_idx = [i for i, ch in enumerate(raw.ch_names) if ch in chs] # only keep channels that are actually requested # if any([ch not in raw.ch_names for ch in chs]): From a345cc9e0c5910a1e8a8372a8d5a257b35b9956e Mon Sep 17 00:00:00 2001 From: Werr Date: Wed, 14 Jan 2026 13:46:53 +0100 Subject: [PATCH 07/14] Changed file opening so it works again --- sleep_utils/usleep_utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sleep_utils/usleep_utils.py b/sleep_utils/usleep_utils.py index 9b15065..967f122 100644 --- a/sleep_utils/usleep_utils.py +++ b/sleep_utils/usleep_utils.py @@ -391,8 +391,7 @@ def _score_sleepyland(filename, 'models': model, 'channels': pairs, } - with open(filename, 'rb') as f: - files = {'edf-files': f} + files = {'edf-files': open(filename, 'rb')} print("waiting for prediction") From a7df8411e97ecf8f064b330ac1f3b45f6f5b5a84 Mon Sep 17 00:00:00 2001 From: lamasmithueten <118809721+lamasmithueten@users.noreply.github.com> Date: Thu, 15 Jan 2026 17:46:31 +0100 Subject: [PATCH 08/14] Update sleep_utils/usleep_utils.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- sleep_utils/usleep_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sleep_utils/usleep_utils.py b/sleep_utils/usleep_utils.py index 967f122..aad7786 100644 --- a/sleep_utils/usleep_utils.py +++ b/sleep_utils/usleep_utils.py @@ -247,7 +247,7 @@ class probabilities. try: raw = mne.io.read_raw_edf(edf_file) except Exception as e: - assert False, f"Error: File '{edf_file}' is not a valid EDF file. Details: {str(e)}" + raise ValueError(f"Error: File '{edf_file}' is not a valid EDF file. Details: {str(e)}") #create a list of all requested channels if (eeg_chs and eog_chs): From e3189f0da1ac6d583dede683e38f96e0e2a8373e Mon Sep 17 00:00:00 2001 From: lamasmithueten <118809721+lamasmithueten@users.noreply.github.com> Date: Thu, 15 Jan 2026 17:47:11 +0100 Subject: [PATCH 09/14] Update sleep_utils/usleep_utils.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- sleep_utils/usleep_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sleep_utils/usleep_utils.py b/sleep_utils/usleep_utils.py index aad7786..4ceaf35 100644 --- a/sleep_utils/usleep_utils.py +++ b/sleep_utils/usleep_utils.py @@ -406,6 +406,7 @@ def _score_sleepyland(filename, except requests.exceptions.RequestException as e: print(f"Request failed: {e}") + raise #Download Data From 313e5230f788cfc25e5b150cc53a35d6c98a9f7f Mon Sep 17 00:00:00 2001 From: lamasmithueten <118809721+lamasmithueten@users.noreply.github.com> Date: Thu, 15 Jan 2026 17:47:36 +0100 Subject: [PATCH 10/14] Update sleep_utils/usleep_utils.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- sleep_utils/usleep_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sleep_utils/usleep_utils.py b/sleep_utils/usleep_utils.py index 4ceaf35..452aca8 100644 --- a/sleep_utils/usleep_utils.py +++ b/sleep_utils/usleep_utils.py @@ -262,7 +262,7 @@ class probabilities. for i in channels: assert i in all_channels, f"Error: {i} not a channel in file" - if (backend == 'sleepyland' or backend == 'sleepyland.zi.local' or backend == 'local'): + if backend in ('sleepyland', 'sleepyland.zi.local', 'local'): if (return_proba): hypno, proba = _score_sleepyland(edf_file, eeg_chs=eeg_chs, eog_chs=eog_chs, ch_groups=ch_groups, return_proba=return_proba) else: From 1955b140ee82eb75475caf8a918f0e85fad4404d Mon Sep 17 00:00:00 2001 From: Werr Date: Mon, 9 Feb 2026 13:01:28 +0100 Subject: [PATCH 11/14] Removed wrapper for tempfiles and moved logic into _score_sleep_raw --- sleep_utils/usleep_utils.py | 44 ++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/sleep_utils/usleep_utils.py b/sleep_utils/usleep_utils.py index 452aca8..dd3129e 100644 --- a/sleep_utils/usleep_utils.py +++ b/sleep_utils/usleep_utils.py @@ -16,17 +16,6 @@ import requests from io import BytesIO -def tempfile_wrapper(func): - @wraps(func) - def wrapped(*args, **kwargs): - try: - tempfile_name = tempfile.NamedTemporaryFile().name + '.edf' - res = func(*args, **kwargs, tmp_edf=tempfile_name) - finally: - if os.path.isfile(tempfile_name): - os.remove(tempfile_name) - return res - return wrapped def disable_ssl_verify(): """will monkey-patch requests made by usleep-api to veryify=False""" @@ -55,8 +44,8 @@ def score_sleep(raw = None, ch_groups=None, model=None, saveto=None, - seconds_per_label=30, tmp_edf=None, + seconds_per_label=30, return_proba=False): if (raw): @@ -87,7 +76,7 @@ def score_sleep(raw = None, print("Requires either a valid EDF-file OR mne.io.Raw object") -@tempfile_wrapper +#@tempfile_wrapper def _score_sleep_raw(raw, api_token = None, backend = 'sleepyland', @@ -160,18 +149,23 @@ def _score_sleep_raw(raw, print('downsampling to 128 hz') raw.resample(128, n_jobs=-2) - mne.export.export_raw(tmp_edf, raw, fmt='edf', overwrite=True) - return _score_sleep_file(tmp_edf, - api_token = api_token, - backend = backend, - backend_url = backend_url, - eeg_chs=eeg_chs, - eog_chs=eog_chs, - ch_groups=ch_groups, - model=model, - saveto=saveto, - seconds_per_label=seconds_per_label, - return_proba=return_proba) + try: + tmp_edf = tempfile.NamedTemporaryFile().name + '.edf' + + mne.export.export_raw(tmp_edf, raw, fmt='edf', overwrite=True) + return _score_sleep_file(tmp_edf, + api_token = api_token, + backend = backend, + backend_url = backend_url, + eeg_chs=eeg_chs, + eog_chs=eog_chs, + ch_groups=ch_groups, + model=model, + saveto=saveto, + seconds_per_label=seconds_per_label, + return_proba=return_proba) + finally: + os.remove(tmp_edf) def delete_all_sessions(api_token): """convenience function to delete all sessions and data""" From c28c64ef3d00b2d3726123fdec09ca2d0237dbb2 Mon Sep 17 00:00:00 2001 From: lamasmithueten <118809721+lamasmithueten@users.noreply.github.com> Date: Tue, 10 Feb 2026 10:23:18 +0100 Subject: [PATCH 12/14] Update sleep_utils/usleep_utils.py Co-authored-by: Simon Kern <14980558+skjerns@users.noreply.github.com> --- sleep_utils/usleep_utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sleep_utils/usleep_utils.py b/sleep_utils/usleep_utils.py index dd3129e..ebbd6ef 100644 --- a/sleep_utils/usleep_utils.py +++ b/sleep_utils/usleep_utils.py @@ -60,7 +60,8 @@ def score_sleep(raw = None, saveto = saveto, seconds_per_label = seconds_per_label, return_proba = return_proba) - elif (edf_file): + elif edf_file is not None: + return _score_sleep_file(edf_file, api_token = api_token, backend = backend, From c08cb5be751b4b089966776363bdb3837155663c Mon Sep 17 00:00:00 2001 From: lamasmithueten <118809721+lamasmithueten@users.noreply.github.com> Date: Tue, 10 Feb 2026 10:23:34 +0100 Subject: [PATCH 13/14] Update sleep_utils/usleep_utils.py Co-authored-by: Simon Kern <14980558+skjerns@users.noreply.github.com> --- sleep_utils/usleep_utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sleep_utils/usleep_utils.py b/sleep_utils/usleep_utils.py index ebbd6ef..4498f0d 100644 --- a/sleep_utils/usleep_utils.py +++ b/sleep_utils/usleep_utils.py @@ -48,7 +48,8 @@ def score_sleep(raw = None, seconds_per_label=30, return_proba=False): - if (raw): + if raw is not None: + return _score_sleep_raw(raw, api_token = api_token, backend = backend, From b09a20e86d8c0270e0c24f2483e23487679fc07b Mon Sep 17 00:00:00 2001 From: Werr Date: Tue, 10 Feb 2026 10:36:49 +0100 Subject: [PATCH 14/14] Added XOR for raw and file --- sleep_utils/usleep_utils.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/sleep_utils/usleep_utils.py b/sleep_utils/usleep_utils.py index 4498f0d..ac2bfff 100644 --- a/sleep_utils/usleep_utils.py +++ b/sleep_utils/usleep_utils.py @@ -48,7 +48,10 @@ def score_sleep(raw = None, seconds_per_label=30, return_proba=False): - if raw is not None: + if (raw is None) == (edf_file is None): + raise ValueError('either raw or edf_file has to be provided, not both or neither') + + elif raw is not None: return _score_sleep_raw(raw, api_token = api_token, @@ -74,8 +77,6 @@ def score_sleep(raw = None, saveto = saveto, seconds_per_label = seconds_per_label, return_proba = return_proba) - else: - print("Requires either a valid EDF-file OR mne.io.Raw object") #@tempfile_wrapper