Skip to content

Commit a5b8809

Browse files
conoratoabelfodil
andauthored
Backend/add logs (#86)
* added log config * added log in classification and backend package init * added logs * added logging to validation & file parser * added logs and log level to distinguish backend logs from classification logs * completed * Apply suggestions from code review Co-authored-by: Anes Belfodil <abelfodil@users.noreply.github.com> * compacted logs Co-authored-by: Anes Belfodil <abelfodil@users.noreply.github.com>
1 parent db6ea6d commit a5b8809

File tree

13 files changed

+120
-24
lines changed

13 files changed

+120
-24
lines changed

backend/backend/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from logging import INFO
2+
3+
from config.logging import config_logger
4+
5+
config_logger(
6+
logger_name=__name__,
7+
log_level=INFO,
8+
)

backend/backend/analyze_sleep.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22
import falcon
3+
import logging
34

45
from backend.request import ClassificationRequest
56
from backend.response import ClassificationResponse
@@ -10,9 +11,12 @@
1011
from classification.model import SleepStagesClassifier
1112
from classification.features.preprocessing import preprocess
1213

14+
_logger = logging.getLogger(__name__)
15+
1316

1417
class AnalyzeSleep:
1518
def __init__(self):
19+
_logger.info("Initializing sleep stage classifier.")
1620
self.sleep_stage_classifier = SleepStagesClassifier()
1721

1822
@staticmethod
@@ -55,6 +59,7 @@ def on_post(self, request, response):
5559
}
5660
"""
5761

62+
_logger.info("Validating and parsing form fields and EEG file")
5863
try:
5964
form_data, file = self._parse_form(request.get_media())
6065
raw_array = get_raw_array(file)
@@ -67,16 +72,27 @@ def on_post(self, request, response):
6772
raw_eeg=raw_array,
6873
)
6974
except (KeyError, ValueError, ClassificationError):
75+
_logger.warn(
76+
"An error occured when validating and parsing form fields. "
77+
"Request parameters are either missing or invalid."
78+
)
7079
response.status = falcon.HTTP_400
7180
response.content_type = falcon.MEDIA_TEXT
7281
response.body = 'Missing or invalid request parameters'
7382
return
7483

84+
_logger.info("Preprocessing of raw EEG data.")
7585
preprocessed_epochs = preprocess(classification_request)
86+
87+
_logger.info("Prediction of EEG data to sleep stages.")
7688
predictions = self.sleep_stage_classifier.predict(preprocessed_epochs, classification_request)
89+
90+
_logger.info("Computations of visualisation data & of sleep report metrics...")
7791
spectrogram_generator = SpectrogramGenerator(preprocessed_epochs)
7892
classification_response = ClassificationResponse(
7993
classification_request, predictions, spectrogram_generator.generate()
8094
)
8195

8296
response.body = json.dumps(classification_response.response)
97+
98+
_logger.info("Request completed")

backend/backend/app.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import falcon
2+
import logging
23

34
from backend.ping import Ping
45
from backend.analyze_sleep import AnalyzeSleep
56

7+
_logger = logging.getLogger(__name__)
8+
69

710
def App():
811
app = falcon.App(cors_enable=True)
@@ -13,4 +16,8 @@ def App():
1316
analyze = AnalyzeSleep()
1417
app.add_route('/analyze-sleep', analyze)
1518

19+
_logger.info(
20+
'Completed local server initialization. '
21+
'Please go back to your browser in order to submit your sleep EEG file. '
22+
)
1623
return app

backend/backend/request.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import logging
12

23
from classification.config.constants import EPOCH_DURATION
34
from classification.config.constants import (
@@ -10,6 +11,8 @@
1011
ClassificationError,
1112
)
1213

14+
_logger = logging.getLogger(__name__)
15+
1316

1417
class ClassificationRequest():
1518
def __init__(self, sex, age, stream_start, bedtime, wakeup, raw_eeg, stream_duration=None):
@@ -56,21 +59,27 @@ def _validate_timestamps(self):
5659
and has_got_out_of_bed_after_in_bed
5760
and has_respected_minimum_bed_time
5861
):
62+
_logger.warn("Received timestamps are invalid.")
5963
raise TimestampsError()
6064

6165
def _validate_file_with_timestamps(self):
6266
has_raw_respected_minimum_file_size = self.raw_eeg.times[-1] > FILE_MINIMUM_DURATION
6367

6468
if not has_raw_respected_minimum_file_size:
69+
_logger.warn(f"Uploaded file must at least have {FILE_MINIMUM_DURATION} seconds of data.")
6570
raise FileSizeError()
6671

6772
is_raw_at_least_as_long_as_out_of_bed = self.raw_eeg.times[-1] >= self.out_of_bed_seconds
6873

6974
if not is_raw_at_least_as_long_as_out_of_bed:
75+
_logger.warn(
76+
"Uploaded file must at least last the time between the start of the "
77+
f"stream and out of bed time, which is {self.out_of_bed_seconds} seconds.")
7078
raise TimestampsError()
7179

7280
def _validate_age(self):
7381
is_in_accepted_range = ACCEPTED_AGE_RANGE[0] <= int(self.age) <= ACCEPTED_AGE_RANGE[1]
7482

7583
if not(is_in_accepted_range):
84+
_logger.warn(f"Age must be in the following range: {ACCEPTED_AGE_RANGE}")
7685
raise ClassificationError('invalid age')

backend/backend/spectrogram_generator.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ def generate(self):
2020
self.epochs,
2121
fmin=self.spectrogram_min_freq,
2222
fmax=self.spectrogram_max_freq,
23+
verbose=False,
2324
)
2425
psds_db = self._convert_amplitudes_to_decibel(psds)
2526

backend/classification/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from logging import INFO
2+
3+
from config.logging import config_logger
4+
5+
config_logger(
6+
logger_name=__name__,
7+
log_level=INFO,
8+
message_sublevel=True,
9+
)

backend/classification/features/extraction.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,6 @@ def get_eeg_features(epochs, in_bed_seconds, out_of_bed_seconds):
3131

3232
features.append(channel_features)
3333

34-
print(
35-
f"Done extracting {channel_features.shape[1]} features "
36-
f"on {channel_features.shape[0]} epochs for {channel}\n"
37-
)
38-
3934
return np.hstack(tuple(features))
4035

4136

backend/classification/features/pipeline/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def get_psds_from_epochs(epochs):
1616
--------
1717
psds with associated frequencies calculated with the welch method.
1818
"""
19-
psds, freqs = psd_welch(epochs, fmin=0.5, fmax=30.)
19+
psds, freqs = psd_welch(epochs, fmin=0.5, fmax=30., verbose=False)
2020
return psds, freqs
2121

2222

backend/classification/features/preprocessing.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import logging
12
import mne
23
from scipy.signal import cheby1
34

@@ -11,6 +12,8 @@
1112
HIGH_PASS_MAX_RIPPLE_DB,
1213
)
1314

15+
_logger = logging.getLogger(__name__)
16+
1417

1518
def preprocess(classification_request):
1619
"""Returns preprocessed epochs of the specified channel
@@ -20,13 +23,20 @@ def preprocess(classification_request):
2023
"""
2124
raw_data = classification_request.raw_eeg.copy()
2225

26+
_logger.info("Cropping data from bed time to out of bed time.")
2327
raw_data = _crop_raw_data(
2428
raw_data,
2529
classification_request.in_bed_seconds,
2630
classification_request.out_of_bed_seconds,
2731
)
32+
33+
_logger.info(f"Applying high pass filter at {DATASET_HIGH_PASS_FREQ}Hz.")
2834
raw_data = _apply_high_pass_filter(raw_data)
35+
36+
_logger.info(f"Resampling data at the dataset's sampling rate of {DATASET_SAMPLE_RATE} Hz.")
2937
raw_data = raw_data.resample(DATASET_SAMPLE_RATE)
38+
39+
_logger.info(f"Epoching data with a {EPOCH_DURATION} seconds duration.")
3040
raw_data = _convert_to_epochs(raw_data)
3141

3242
return raw_data

backend/classification/load_model.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from datetime import datetime
2+
import logging
23
from os import path, makedirs
34
from pathlib import Path
45
import re
@@ -12,6 +13,10 @@
1213

1314
from classification.config.constants import HiddenMarkovModelProbability
1415

16+
17+
_logger = logging.getLogger(__name__)
18+
19+
1520
SCRIPT_PATH = Path(path.realpath(sys.argv[0])).parent
1621

1722
BUCKET_NAME = 'polydodo'
@@ -50,9 +55,13 @@ def _has_latest_object(filename, local_path):
5055

5156
def load_model():
5257
if not path.exists(MODEL_PATH) or not _has_latest_object(MODEL_FILENAME, MODEL_PATH):
53-
print("Downloading latest model...")
58+
_logger.info(
59+
"Downloading latest sleep stage classification model... "
60+
f"This could take a few minutes. (storing it at {MODEL_PATH})"
61+
)
5462
_download_file(MODEL_URL, MODEL_PATH)
55-
print("Loading model...")
63+
64+
_logger.info(f"Loading latest sleep stage classification model... (from {MODEL_PATH})")
5665
return onnxruntime.InferenceSession(str(MODEL_PATH))
5766

5867

@@ -67,8 +76,10 @@ def load_hmm():
6776
model_path = SCRIPT_PATH / HMM_FOLDER / hmm_file
6877

6978
if not path.exists(model_path) or not _has_latest_object(hmm_file, model_path):
79+
_logger.info(f"Downloading postprocessing model... (storing it at {model_path})")
7080
_download_file(url=f"{BUCKET_URL}/{hmm_file}", output=model_path)
7181

82+
_logger.info(f"Loading postprocessing model... (from {model_path})")
7283
hmm_matrices[hmm_probability.name] = np.load(str(model_path))
7384

7485
return hmm_matrices

backend/classification/model.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
"""defines models which predict sleep stages based off EEG signals"""
2+
import logging
23

34
from classification.features import get_features
45
from classification.postprocessor import get_hmm_model
56
from classification.load_model import load_model, load_hmm
67

8+
_logger = logging.getLogger(__name__)
9+
710

811
class SleepStagesClassifier():
912
def __init__(self):
@@ -21,12 +24,14 @@ def predict(self, epochs, request):
2124
- request: instance of ClassificationRequest
2225
Returns: array of predicted sleep stages
2326
"""
24-
27+
_logger.info("Extracting features...")
2528
features = get_features(epochs, request)
29+
_logger.info(f"Finished extracting {features.shape[1]} features over {features.shape[0]} epochs.")
2630

27-
print(features, features.shape)
28-
31+
_logger.info("Classifying sleep stages from extracted features...")
2932
predictions = self._get_predictions(features)
33+
34+
_logger.info("Applying postprocessing step to the resulted sleep stages...")
3035
predictions = self._get_postprocessed_predictions(predictions)
3136

3237
return predictions

backend/classification/parser/__init__.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
The Cyton board logging format is also described here:
1111
[https://docs.openbci.com/docs/02Cyton/CytonSDCard#data-logging-format]
1212
"""
13+
import logging
14+
1315
from mne import create_info
1416
from mne.io import RawArray
1517

@@ -18,6 +20,9 @@
1820
from classification.parser.sample_rate import detect_sample_rate
1921

2022

23+
_logger = logging.getLogger(__name__)
24+
25+
2126
def get_raw_array(file):
2227
"""Converts a file following a logging format into a mne.RawArray
2328
Input:
@@ -27,15 +32,15 @@ def get_raw_array(file):
2732
"""
2833

2934
filetype = detect_file_type(file)
30-
print(f"""
31-
Detected {filetype.name} format.
32-
""")
3335

3436
sample_rate = detect_sample_rate(file, filetype)
35-
print(f"""
36-
Detected {sample_rate}Hz sample rate.
37-
""")
3837

38+
_logger.info(
39+
f"EEG data has been detected to be in the {filetype.name} format "
40+
f"and has a {sample_rate}Hz sample rate."
41+
)
42+
43+
_logger.info("Parsing EEG file to a mne.RawArray object...")
3944
eeg_raw = filetype.parser(file)
4045

4146
raw_object = RawArray(
@@ -47,12 +52,12 @@ def get_raw_array(file):
4752
verbose=False,
4853
)
4954

50-
print(f"""
51-
First sample values: {raw_object[:, 0]}
52-
Second sample values: {raw_object[:, 1]}
53-
Number of samples: {raw_object.n_times}
54-
Duration of signal (h): {raw_object.n_times / (3600 * sample_rate)}
55-
Channel names: {raw_object.ch_names}
56-
""")
55+
_logger.info(
56+
f"Finished converting EEG file to mne.RawArray object "
57+
f"with the first sample being {*(raw_object[:, 0][0]),}, "
58+
f"with {raw_object.n_times} samples, "
59+
f"with a {raw_object.n_times / (3600 * sample_rate):.2f} hours duration and "
60+
f"with channels named {raw_object.ch_names}."
61+
)
5762

5863
return raw_object

backend/config/logging.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import logging
2+
import sys
3+
4+
STD_OUTPUT_FORMAT = "[%(asctime)s - %(levelname)s]:\t%(message)s (%(name)s)"
5+
SUBLEVEL_OUTPUT_FORMAT = "[%(asctime)s - %(levelname)s]:\t\t%(message)s (%(name)s)"
6+
7+
8+
def config_logger(logger_name, log_level, message_sublevel=False):
9+
"""Configures logging with std output"""
10+
logger = logging.getLogger(logger_name)
11+
logger.setLevel(log_level)
12+
logger.addHandler(_get_console_handler(message_sublevel))
13+
logger.propagate = False
14+
15+
16+
def _get_console_handler(message_sublevel):
17+
console_handler = logging.StreamHandler(sys.stdout)
18+
formatter = SUBLEVEL_OUTPUT_FORMAT if message_sublevel else STD_OUTPUT_FORMAT
19+
console_handler.setFormatter(logging.Formatter(formatter))
20+
return console_handler

0 commit comments

Comments
 (0)