accusleepy 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,589 @@
1
+ import os
2
+ import re
3
+ import warnings
4
+ from dataclasses import dataclass
5
+ from operator import attrgetter
6
+
7
+ import numpy as np
8
+ import pandas as pd
9
+ from PIL import Image
10
+ from scipy.signal import butter, filtfilt
11
+ from tqdm import trange
12
+
13
+ from accusleepy.brain_state_set import BrainStateSet
14
+ from accusleepy.constants import (
15
+ DEFAULT_MODEL_TYPE,
16
+ DOWNSAMPLING_START_FREQ,
17
+ EMG_COPIES,
18
+ FILENAME_COL,
19
+ LABEL_COL,
20
+ MIN_WINDOW_LEN,
21
+ UPPER_FREQ,
22
+ )
23
+ from accusleepy.fileio import Recording, load_labels, load_recording
24
+ from accusleepy.multitaper import spectrogram
25
+
26
+ # clip mixture z-scores above and below this level
27
+ # in the matlab implementation, I used 4.5
28
+ ABS_MAX_Z_SCORE = 3.5
29
+ # upper frequency limit when generating EEG spectrograms
30
+ SPECTROGRAM_UPPER_FREQ = 64
31
+ # filename used to store info about training image datasets
32
+ ANNOTATIONS_FILENAME = "annotations.csv"
33
+
34
+
35
+ def resample(
36
+ eeg: np.array, emg: np.array, sampling_rate: int | float, epoch_length: int | float
37
+ ) -> (np.array, np.array, float):
38
+ """Resample recording so that epochs contain equal numbers of samples
39
+
40
+ If the number of samples per epoch is not an integer, epoch-level calculations
41
+ are much more difficult. To avoid this, we can resample the EEG and EMG signals
42
+ and adjust the sampling rate accordingly.
43
+
44
+ :param eeg: EEG signal
45
+ :param emg: EMG signal
46
+ :param sampling_rate: original sampling rate, in Hz
47
+ :param epoch_length: epoch length, in seconds
48
+ :return: resampled EEG & EMG and updated sampling rate
49
+ """
50
+ samples_per_epoch = sampling_rate * epoch_length
51
+ if samples_per_epoch % 1 == 0:
52
+ return eeg, emg, sampling_rate
53
+
54
+ resampled = list()
55
+ for arr in [eeg, emg]:
56
+ x = np.arange(0, arr.size)
57
+ x_new = np.linspace(
58
+ 0,
59
+ arr.size - 1,
60
+ round(arr.size * np.ceil(samples_per_epoch) / samples_per_epoch),
61
+ )
62
+ resampled.append(np.interp(x_new, x, arr))
63
+
64
+ eeg = resampled[0]
65
+ emg = resampled[1]
66
+ new_sampling_rate = np.ceil(samples_per_epoch) / samples_per_epoch * sampling_rate
67
+ return eeg, emg, new_sampling_rate
68
+
69
+
70
+ def standardize_signal_length(
71
+ eeg: np.array, emg: np.array, sampling_rate: int | float, epoch_length: int | float
72
+ ) -> (np.array, np.array):
73
+ """Truncate or pad EEG/EMG signals to have an integer number of epochs
74
+
75
+ :param eeg: EEG signal
76
+ :param emg: EMG signal
77
+ :param sampling_rate: original sampling rate, in Hz
78
+ :param epoch_length: epoch length, in seconds
79
+ :return: EEG and EMG signals
80
+ """
81
+ # since resample() was called, this will be extremely close to an integer
82
+ samples_per_epoch = round(sampling_rate * epoch_length)
83
+
84
+ # pad the signal at the end in case we need more samples
85
+ eeg = np.concatenate((eeg, np.ones(samples_per_epoch) * eeg[-1]))
86
+ emg = np.concatenate((emg, np.ones(samples_per_epoch) * emg[-1]))
87
+ padded_signal_length = eeg.size
88
+
89
+ # count samples that don't fit in any epoch
90
+ excess_samples = padded_signal_length % samples_per_epoch
91
+ # we will definitely remove those
92
+ last_index = padded_signal_length - excess_samples
93
+ # and if the last epoch of real data had more than half of
94
+ # its samples missing, delete it
95
+ if excess_samples < samples_per_epoch / 2:
96
+ last_index -= samples_per_epoch
97
+
98
+ return eeg[:last_index], emg[:last_index]
99
+
100
+
101
+ def resample_and_standardize(
102
+ eeg: np.array, emg: np.array, sampling_rate: int | float, epoch_length: int | float
103
+ ) -> (np.array, np.array, float):
104
+ """Preprocess EEG and EMG signals
105
+
106
+ Adjust the length and sampling rate of the EEG and EMG signals so that
107
+ each epoch contains an integer number of samples and each recording
108
+ contains an integer number of epochs.
109
+
110
+ :param eeg: EEG signal
111
+ :param emg: EMG signal
112
+ :param sampling_rate: sampling rate, in Hz
113
+ :param epoch_length: epoch length, in seconds
114
+ :return: processed EEG & EMG signals, and the new sampling rate
115
+ """
116
+ eeg, emg, sampling_rate = resample(
117
+ eeg=eeg, emg=emg, sampling_rate=sampling_rate, epoch_length=epoch_length
118
+ )
119
+ eeg, emg = standardize_signal_length(
120
+ eeg=eeg, emg=emg, sampling_rate=sampling_rate, epoch_length=epoch_length
121
+ )
122
+ return eeg, emg, sampling_rate
123
+
124
+
125
+ def create_spectrogram(
126
+ eeg: np.array,
127
+ sampling_rate: int | float,
128
+ epoch_length: int | float,
129
+ time_bandwidth=2,
130
+ n_tapers=3,
131
+ ) -> (np.array, np.array):
132
+ """Create an EEG spectrogram image
133
+
134
+ :param eeg: EEG signal
135
+ :param sampling_rate: sampling rate, in Hz
136
+ :param epoch_length: epoch length, in seconds
137
+ :param time_bandwidth: time-half bandwidth product
138
+ :param n_tapers: number of DPSS tapers to use
139
+ :return: spectrogram and its frequency axis
140
+ """
141
+ window_length_sec = max(MIN_WINDOW_LEN, epoch_length)
142
+ # pad the EEG signal so that the first spectrogram window is centered
143
+ # on the first epoch
144
+ # it's possible there's some jank here, if this isn't close to an integer
145
+ pad_length = round((sampling_rate * (window_length_sec - epoch_length) / 2))
146
+ padded_eeg = np.concatenate(
147
+ [eeg[:pad_length][::-1], eeg, eeg[(len(eeg) - pad_length) :][::-1]]
148
+ )
149
+
150
+ spec, _, f = spectrogram(
151
+ padded_eeg,
152
+ sampling_rate,
153
+ frequency_range=[0, SPECTROGRAM_UPPER_FREQ],
154
+ time_bandwidth=time_bandwidth,
155
+ num_tapers=n_tapers,
156
+ window_params=[window_length_sec, epoch_length],
157
+ min_nfft=0,
158
+ detrend_opt="off",
159
+ multiprocess=True,
160
+ plot_on=False,
161
+ return_fig=False,
162
+ verbose=False,
163
+ )
164
+
165
+ # resample frequencies for consistency
166
+ target_frequencies = np.arange(0, SPECTROGRAM_UPPER_FREQ, 1 / MIN_WINDOW_LEN)
167
+ freq_idx = list()
168
+ for i in target_frequencies:
169
+ freq_idx.append(np.argmin(np.abs(f - i)))
170
+ f = f[freq_idx]
171
+ spec = spec[freq_idx, :]
172
+
173
+ return spec, f
174
+
175
+
176
+ def get_emg_power(
177
+ emg: np.array, sampling_rate: int | float, epoch_length: int | float
178
+ ) -> np.array:
179
+ """Calculate EMG power for each epoch
180
+
181
+ This applies a 20-50 Hz bandpass filter to the EMG, calculates the RMS
182
+ in each epoch, and takes the log of the result.
183
+
184
+ :param emg: EMG signal
185
+ :param sampling_rate: sampling rate, in Hz
186
+ :param epoch_length: epoch length, in seconds
187
+ :return: EMG "power" for each epoch
188
+ """
189
+ # filter parameters
190
+ order = 8
191
+ bp_lower = 20
192
+ bp_upper = 50
193
+
194
+ b, a = butter(
195
+ N=order,
196
+ Wn=[bp_lower, bp_upper],
197
+ btype="bandpass",
198
+ output="ba",
199
+ fs=sampling_rate,
200
+ )
201
+ filtered = filtfilt(b, a, x=emg, padlen=int(np.ceil(sampling_rate)))
202
+
203
+ # since resample() was called, this will be extremely close to an integer
204
+ samples_per_epoch = round(sampling_rate * epoch_length)
205
+ reshaped = np.reshape(
206
+ filtered,
207
+ [round(len(emg) / samples_per_epoch), samples_per_epoch],
208
+ )
209
+ rms = np.sqrt(np.mean(np.power(reshaped, 2), axis=1))
210
+
211
+ return np.log(rms)
212
+
213
+
214
+ def create_eeg_emg_image(
215
+ eeg: np.array,
216
+ emg: np.array,
217
+ sampling_rate: int | float,
218
+ epoch_length: int | float,
219
+ ) -> np.array:
220
+ """Stack EEG spectrogram and EMG power into an image
221
+
222
+ This assumes that each epoch contains an integer number of samples and
223
+ each recording contains an integer number of epochs. Note that a log
224
+ transformation is applied to the spectrogram.
225
+
226
+ :param eeg: EEG signal
227
+ :param emg: EMG signal
228
+ :param sampling_rate: sampling rate, in Hz
229
+ :param epoch_length: epoch length, in seconds
230
+ :return: combined EEG + EMG image for a recording
231
+ """
232
+ spec, f = create_spectrogram(eeg, sampling_rate, epoch_length)
233
+ f_lower_idx = sum(f < DOWNSAMPLING_START_FREQ)
234
+ f_upper_idx = sum(f < UPPER_FREQ)
235
+
236
+ modified_spectrogram = np.log(
237
+ spec[
238
+ np.concatenate(
239
+ [np.arange(0, f_lower_idx), np.arange(f_lower_idx, f_upper_idx, 2)]
240
+ ),
241
+ :,
242
+ ]
243
+ )
244
+
245
+ emg_log_rms = get_emg_power(emg, sampling_rate, epoch_length)
246
+ output = np.concatenate(
247
+ [modified_spectrogram, np.tile(emg_log_rms, (EMG_COPIES, 1))]
248
+ )
249
+ return output
250
+
251
+
252
+ def get_mixture_values(
253
+ img: np.array, labels: np.array, brain_state_set: BrainStateSet
254
+ ) -> (np.array, np.array):
255
+ """Compute weighted feature means and SDs for mixture z-scoring
256
+
257
+ The outputs of this function can be used to standardize features
258
+ extracted from all recordings from one subject under the same
259
+ recording conditions. Note that labels must be in "class" format
260
+ (i.e., integers between 0 and the number of scored states).
261
+
262
+ :param img: combined EEG + EMG image - see create_eeg_emg_image()
263
+ :param labels: brain state labels, in "class" format
264
+ :param brain_state_set: set of brain state options
265
+ :return: mixture means, mixture standard deviations
266
+ """
267
+
268
+ means = list()
269
+ variances = list()
270
+ mixture_weights = brain_state_set.mixture_weights
271
+
272
+ # get feature means, variances by class
273
+ for i in range(brain_state_set.n_classes):
274
+ means.append(np.mean(img[:, labels == i], axis=1))
275
+ variances.append(np.var(img[:, labels == i], axis=1))
276
+ means = np.array(means)
277
+ variances = np.array(variances)
278
+
279
+ # mixture means are just weighted averages across classes
280
+ mixture_means = means.T @ mixture_weights
281
+ # mixture variance is given by the law of total variance
282
+ mixture_sds = np.sqrt(
283
+ variances.T @ mixture_weights
284
+ + (
285
+ (mixture_means - np.tile(mixture_means, (brain_state_set.n_classes, 1)))
286
+ ** 2
287
+ ).T
288
+ @ mixture_weights
289
+ )
290
+
291
+ return mixture_means, mixture_sds
292
+
293
+
294
+ def mixture_z_score_img(
295
+ img: np.array,
296
+ brain_state_set: BrainStateSet,
297
+ labels: np.array = None,
298
+ mixture_means: np.array = None,
299
+ mixture_sds: np.array = None,
300
+ ) -> np.array:
301
+ """Perform mixture z-scoring on a combined EEG+EMG image
302
+
303
+ If brain state labels are provided, they will be used to calculate
304
+ mixture means and SDs. Otherwise, you must provide those inputs.
305
+ Note that pixel values in the output are in the 0-1 range and will
306
+ clip z-scores beyond ABS_MAX_Z_SCORE.
307
+
308
+ :param img: combined EEG + EMG image - see create_eeg_emg_image()
309
+ :param brain_state_set: set of brain state options
310
+ :param labels: labels, in "class" format
311
+ :param mixture_means: mixture means
312
+ :param mixture_sds: mixture standard deviations
313
+ :return:
314
+ """
315
+ if labels is None and (mixture_means is None or mixture_sds is None):
316
+ raise Exception("must provide either labels or mixture means+SDs")
317
+ if labels is not None and ((mixture_means is not None) ^ (mixture_sds is not None)):
318
+ warnings.warn("labels were given, mixture means / SDs will be ignored")
319
+
320
+ if labels is not None:
321
+ mixture_means, mixture_sds = get_mixture_values(
322
+ img=img, labels=labels, brain_state_set=brain_state_set
323
+ )
324
+
325
+ img = ((img.T - mixture_means) / mixture_sds).T
326
+ img = (img + ABS_MAX_Z_SCORE) / (2 * ABS_MAX_Z_SCORE)
327
+ img = np.clip(img, 0, 1)
328
+
329
+ return img
330
+
331
+
332
+ def format_img(img: np.array, epochs_per_img: int, add_padding: bool) -> np.array:
333
+ """Adjust the format of an EEG+EMG image
334
+
335
+ This function converts the values in a combined EEG+EMG image to uint8.
336
+ This is a convenient format both for storing individual images as files,
337
+ and for using the images as input to a classifier.
338
+ This function also optionally adds new epochs to the beginning/end of the
339
+ recording's image so that an image can be created for every epoch. For
340
+ real-time scoring, padding should not be used.
341
+
342
+ :param img: combined EEG + EMG image
343
+ :param epochs_per_img: number of epochs in each individual image
344
+ :param add_padding: whether to pad each side by (epochs_per_img - 1) / 2
345
+ :return: formatted EEG + EMG image
346
+ """
347
+ # pad beginning and end
348
+ if add_padding:
349
+ pad_width = round((epochs_per_img - 1) / 2)
350
+ img = np.concatenate(
351
+ [
352
+ np.tile(img[:, 0], (pad_width, 1)).T,
353
+ img,
354
+ np.tile(img[:, -1], (pad_width, 1)).T,
355
+ ],
356
+ axis=1,
357
+ )
358
+
359
+ # use 8-bit values
360
+ img = np.clip(img * 255, 0, 255)
361
+ img = img.astype(np.uint8)
362
+
363
+ return img
364
+
365
+
366
+ def create_training_images(
367
+ recordings: list[Recording],
368
+ output_path: str,
369
+ epoch_length: int | float,
370
+ epochs_per_img: int,
371
+ brain_state_set: BrainStateSet,
372
+ model_type: str,
373
+ ) -> list[int]:
374
+ """Create training dataset
375
+
376
+ By default, the current epoch is located in the central column
377
+ of pixels in each image. For real-time scoring applications,
378
+ the current epoch is at the right edge of each image.
379
+
380
+ :param recordings: list of recordings in the training set
381
+ :param output_path: where to store training images
382
+ :param epoch_length: epoch length, in seconds
383
+ :param epochs_per_img: # number of epochs shown in each image
384
+ :param brain_state_set: set of brain state options
385
+ :param model_type: default or real-time
386
+ :return: list of the names of any recordings that could not
387
+ be used to create training images.
388
+ """
389
+ # recordings that had to be skipped
390
+ failed_recordings = list()
391
+ # image filenames for valid epochs
392
+ filenames = list()
393
+ # all valid labels from all valid recordings
394
+ all_labels = list()
395
+ # try to load each recording and create training images
396
+ for i in trange(len(recordings)):
397
+ recording = recordings[i]
398
+ try:
399
+ eeg, emg = load_recording(recording.recording_file)
400
+ sampling_rate = recording.sampling_rate
401
+ eeg, emg, sampling_rate = resample_and_standardize(
402
+ eeg=eeg,
403
+ emg=emg,
404
+ sampling_rate=sampling_rate,
405
+ epoch_length=epoch_length,
406
+ )
407
+
408
+ labels = load_labels(recording.label_file)
409
+ labels = brain_state_set.convert_digit_to_class(labels)
410
+ img = create_eeg_emg_image(eeg, emg, sampling_rate, epoch_length)
411
+ img = mixture_z_score_img(
412
+ img=img, brain_state_set=brain_state_set, labels=labels
413
+ )
414
+ img = format_img(img=img, epochs_per_img=epochs_per_img, add_padding=True)
415
+
416
+ # the model type determines which epochs are used in each image
417
+ if model_type == DEFAULT_MODEL_TYPE:
418
+ # here, j is the index of the current epoch in 'labels'
419
+ # and the index of the leftmost epoch in 'img'
420
+ for j in range(img.shape[1] - (epochs_per_img - 1)):
421
+ if labels[j] is None:
422
+ continue
423
+ im = img[:, j : (j + epochs_per_img)]
424
+ filename = f"recording_{recording.name}_{j}_{labels[j]}.png"
425
+ filenames.append(filename)
426
+ all_labels.append(labels[j])
427
+ Image.fromarray(im).save(os.path.join(output_path, filename))
428
+ else:
429
+ # here, j is the index of the current epoch in 'labels'
430
+ # but we throw away a few epochs at the start since they
431
+ # would require even more padding on the left side.
432
+ one_side_padding = round((epochs_per_img - 1) / 2)
433
+ for j in range(one_side_padding, len(labels)):
434
+ if labels[j] is None:
435
+ continue
436
+ im = img[:, (j - one_side_padding) : j + one_side_padding + 1]
437
+ filename = f"recording_{recording.name}_{j}_{labels[j]}.png"
438
+ filenames.append(filename)
439
+ all_labels.append(labels[j])
440
+ Image.fromarray(im).save(os.path.join(output_path, filename))
441
+
442
+ except Exception as e:
443
+ print(e)
444
+ failed_recordings.append(recording.name)
445
+
446
+ # annotation file containing info on all images
447
+ pd.DataFrame({FILENAME_COL: filenames, LABEL_COL: all_labels}).to_csv(
448
+ os.path.join(output_path, ANNOTATIONS_FILENAME),
449
+ index=False,
450
+ )
451
+
452
+ return failed_recordings
453
+
454
+
455
+ @dataclass
456
+ class Bout:
457
+ """Stores information about a brain state bout"""
458
+
459
+ length: int # length, in number of epochs
460
+ start_index: int # index where bout starts
461
+ end_index: int # index where bout ends
462
+ surrounding_state: int # brain state on both sides of the bout
463
+
464
+
465
+ def find_last_adjacent_bout(sorted_bouts: list[Bout], bout_index: int) -> int:
466
+ """Find index of last consecutive same-length bout
467
+
468
+ When running the post-processing step that enforces a minimum duration
469
+ for brain state bouts, there is a special case when bouts below the
470
+ duration threshold occur consecutively. This function performs a
471
+ recursive search for the index of a bout at the end of such a sequence.
472
+ When initially called, bout_index will always be 0. If, for example, the
473
+ first three bouts in the list are consecutive, the function will return 2.
474
+
475
+ :param sorted_bouts: list of brain state bouts, sorted by start time
476
+ :param bout_index: index of the bout in question
477
+ :return: index of the last consecutive same-length bout
478
+ """
479
+ # if we're at the end of the bout list, stop
480
+ if bout_index == len(sorted_bouts) - 1:
481
+ return bout_index
482
+
483
+ # if there is an adjacent bout
484
+ if sorted_bouts[bout_index].end_index == sorted_bouts[bout_index + 1].start_index:
485
+ # look for more adjacent bouts using that one as a starting point
486
+ return find_last_adjacent_bout(sorted_bouts, bout_index + 1)
487
+ else:
488
+ return bout_index
489
+
490
+
491
+ def enforce_min_bout_length(
492
+ labels: np.array, epoch_length: int | float, min_bout_length: int | float
493
+ ) -> np.array:
494
+ """Ensure brain state bouts meet the min length requirement
495
+
496
+ As a post-processing step for sleep scoring, we can require that any
497
+ bout (continuous period) of a brain state have a minimum duration.
498
+ This function sets any bout shorter than the minimum duration to the
499
+ surrounding brain state (if the states on the left and right sides
500
+ are the same). In the case where there are consecutive short bouts,
501
+ it either creates a transition at the midpoint or removes all short
502
+ bouts, depending on whether the number is even or odd. For example:
503
+ ...AAABABAAA... -> ...AAAAAAAAA...
504
+ ...AAABABABBB... -> ...AAAAABBBBB...
505
+
506
+ :param labels: brain state labels (digits in the 0-9 range)
507
+ :param epoch_length: epoch length, in seconds
508
+ :param min_bout_length: minimum bout length, in seconds
509
+ :return: updated brain state labels
510
+ """
511
+ # if recording is very short, don't change anything
512
+ if labels.size < 3:
513
+ return labels
514
+
515
+ if epoch_length == min_bout_length:
516
+ return labels
517
+
518
+ # get minimum number of epochs in a bout
519
+ min_epochs = int(np.ceil(min_bout_length / epoch_length))
520
+ # get set of states in the labels
521
+ brain_states = set(labels.tolist())
522
+
523
+ while True: # so true
524
+ # convert labels to a string for regex search
525
+ # There is probably a regex that can find all patterns like ab+a
526
+ # without consuming each "a" but I haven't found it :(
527
+ label_string = "".join(labels.astype(str))
528
+
529
+ bouts = list()
530
+
531
+ for state in brain_states:
532
+ for other_state in brain_states:
533
+ if state == other_state:
534
+ continue
535
+ # get start and end indices of each bout
536
+ expression = (
537
+ f"(?<={other_state}){state}{{1,{min_epochs - 1}}}(?={other_state})"
538
+ )
539
+ matches = re.finditer(expression, label_string)
540
+ spans = [match.span() for match in matches]
541
+
542
+ # if some bouts were found
543
+ for span in spans:
544
+ bouts.append(
545
+ Bout(
546
+ length=span[1] - span[0],
547
+ start_index=span[0],
548
+ end_index=span[1],
549
+ surrounding_state=other_state,
550
+ )
551
+ )
552
+
553
+ if len(bouts) == 0:
554
+ break
555
+
556
+ # only keep the shortest bouts
557
+ min_length_in_list = np.min([bout.length for bout in bouts])
558
+ bouts = [i for i in bouts if i.length == min_length_in_list]
559
+ # sort by start index
560
+ sorted_bouts = sorted(bouts, key=attrgetter("start_index"))
561
+
562
+ while len(sorted_bouts) > 0:
563
+ # get row index of latest adjacent bout (of same length)
564
+ last_adjacent_bout_index = find_last_adjacent_bout(sorted_bouts, 0)
565
+ # if there's an even number of adjacent bouts
566
+ if (last_adjacent_bout_index + 1) % 2 == 0:
567
+ midpoint = sorted_bouts[
568
+ round((last_adjacent_bout_index + 1) / 2)
569
+ ].start_index
570
+ labels[sorted_bouts[0].start_index : midpoint] = sorted_bouts[
571
+ 0
572
+ ].surrounding_state
573
+ labels[midpoint : sorted_bouts[last_adjacent_bout_index].end_index] = (
574
+ sorted_bouts[last_adjacent_bout_index].surrounding_state
575
+ )
576
+ else:
577
+ labels[
578
+ sorted_bouts[0].start_index : sorted_bouts[
579
+ last_adjacent_bout_index
580
+ ].end_index
581
+ ] = sorted_bouts[0].surrounding_state
582
+
583
+ # delete the bouts we just fixed
584
+ if last_adjacent_bout_index == len(sorted_bouts) - 1:
585
+ sorted_bouts = []
586
+ else:
587
+ sorted_bouts = sorted_bouts[(last_adjacent_bout_index + 1) :]
588
+
589
+ return labels
@@ -0,0 +1,57 @@
1
+ Metadata-Version: 2.3
2
+ Name: accusleepy
3
+ Version: 0.1.0
4
+ Summary: Python implementation of AccuSleep
5
+ License: GPL-3.0-only
6
+ Author: Zeke Barger
7
+ Author-email: zekebarger@gmail.com
8
+ Requires-Python: >=3.10,<3.14
9
+ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Requires-Dist: fastparquet (>=2024.11.0,<2025.0.0)
16
+ Requires-Dist: joblib (>=1.4.2,<2.0.0)
17
+ Requires-Dist: matplotlib (>=3.10.1,<4.0.0)
18
+ Requires-Dist: numpy (>=2.2.4,<3.0.0)
19
+ Requires-Dist: pandas (>=2.2.3,<3.0.0)
20
+ Requires-Dist: pillow (>=11.1.0,<12.0.0)
21
+ Requires-Dist: pre-commit (>=4.2.0,<5.0.0)
22
+ Requires-Dist: pyside6 (>=6.8.3,<7.0.0)
23
+ Requires-Dist: scipy (>=1.15.2,<2.0.0)
24
+ Requires-Dist: torch (>=2.6.0,<3.0.0)
25
+ Requires-Dist: torchvision (>=0.21.0,<0.22.0)
26
+ Requires-Dist: tqdm (>=4.67.1,<5.0.0)
27
+ Description-Content-Type: text/markdown
28
+
29
+ # AccuSleePy
30
+
31
+ ## Description
32
+
33
+ AccuSleePy is a python implementation of AccuSleep--a set of graphical user interfaces for scoring rodent
34
+ sleep using EEG and EMG recordings. If you use AccuSleep in your research, please cite our
35
+ [publication](https://journals.plos.org/plosone/article?id=10.1371/journal.pone.0224642):
36
+
37
+ Barger, Z., Frye, C. G., Liu, D., Dan, Y., & Bouchard, K. E. (2019). Robust, automated sleep scoring by a compact neural network with distributional shift correction. *PLOS ONE, 14*(12), 1–18.
38
+
39
+ The data used for training and testing AccuSleep are available at https://osf.io/py5eb/
40
+
41
+ Please contact zekebarger (at) gmail (dot) com with any questions or comments about the software.
42
+
43
+ ## Installation instructions
44
+
45
+ WIP
46
+
47
+ ## Tips & Troubleshooting
48
+
49
+ WIP
50
+
51
+ ## Acknowledgements
52
+
53
+ We would like to thank [Franz Weber](https://www.med.upenn.edu/weberlab/) for creating an
54
+ early version of the manual labeling interface.
55
+ Jim Bohnslav's [deepethogram](https://github.com/jbohnslav/deepethogram) served as an
56
+ incredibly useful reference when reimplementing this project in python.
57
+
@@ -0,0 +1,37 @@
1
+ accusleepy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ accusleepy/__main__.py,sha256=dKzl2N2Hg9lD264CWYNxThRyDKzWwyMwHRXmJxOmMis,104
3
+ accusleepy/brain_state_set.py,sha256=fRkrArHLIbEKimub804yt_mUXoyfsjJEfiJnTjeCMkY,3233
4
+ accusleepy/classification.py,sha256=xrmPyMHlzYh0QfNCID1PRIYEIyNkWduOi7g1Bdb6xfg,8573
5
+ accusleepy/config.json,sha256=GZV0d1E44NEj-P7X5GKjbxbjjYKWUtXAm29a3vGABVA,430
6
+ accusleepy/constants.py,sha256=ceRcWmhec5sATiAsIynf3XFKl0H2IsT_JDfeg7xYbqg,1189
7
+ accusleepy/fileio.py,sha256=ojuzDkcJkXok9dny75g8LDieKeMT7P2SFVLyJek6Nd8,6011
8
+ accusleepy/gui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ accusleepy/gui/icons/brightness_down.png,sha256=PLT1fb83RHIhSRuU7MMMx0G7oJAY7o9wUcnqM8veZfM,12432
10
+ accusleepy/gui/icons/brightness_up.png,sha256=64GnUqgPvN5xZ6Um3wOzwqvUmdAWYZT6eFmWpBsHyks,12989
11
+ accusleepy/gui/icons/double_down_arrow.png,sha256=fGiPIP7_RJ3UAonNhORFVX0emXEmtzRlHI3Tfjai064,4964
12
+ accusleepy/gui/icons/double_up_arrow.png,sha256=n7QEo0bZZDve4thwTCKghPKVjTNbQMgyQNsn46iqXbI,5435
13
+ accusleepy/gui/icons/down_arrow.png,sha256=XwS_Gq2j6PoNHRaeaAGoh5kcXJNXWAHWWbJbUsvrRPU,3075
14
+ accusleepy/gui/icons/home.png,sha256=yd3nmHlD9w2a2j3cBd-w_Cuidr-J0apryRoWJoPb66w,5662
15
+ accusleepy/gui/icons/question.png,sha256=IJcIRgQOC9KlzA4vtA5Qu-DQ1-SclhVLeovIsEfl3TU,17105
16
+ accusleepy/gui/icons/save.png,sha256=J3EA8iU1BqLYRSsrq_OdoZlqrv2yfL7oV54DklTy_DI,13555
17
+ accusleepy/gui/icons/up_arrow.png,sha256=V9yF9t1WgjPaUu-mF1YGe_DfaRHg2dUpR_sUVVcvVvY,3329
18
+ accusleepy/gui/icons/zoom_in.png,sha256=MFWnKZp7Rvh4bLPq4Cqo4sB_jQYedUUtT8-ZO8tNYyc,13589
19
+ accusleepy/gui/icons/zoom_out.png,sha256=IB8Jecb3i0U4qjWRR46ridjLpvLCSe7PozBaLqQqYSw,13055
20
+ accusleepy/gui/main.py,sha256=clnk3Tuzu1W6IrXjoRU1IaFNkhZZfohNriSZiE2mj6I,53008
21
+ accusleepy/gui/manual_scoring.py,sha256=Ce-X4A7pm3Hb0PtiqebF59rpSEMcHhWWhoO-L7rYtTE,40677
22
+ accusleepy/gui/mplwidget.py,sha256=f9O3u_96whQGUwpi3o_QGc7yjiETX5vE0oj3ePXTJWE,12279
23
+ accusleepy/gui/primary_window.py,sha256=5aXAkX4tI6MkA_zNexlrNzaLFPaIUAtxUdcu8QAJWYI,97441
24
+ accusleepy/gui/primary_window.ui,sha256=TswlioQasBB1lMfP8K1BYnh-xTk5odsmjCctP54w53o,135230
25
+ accusleepy/gui/resources.qrc,sha256=ByNEmJqr0YbKBqoZGvONZtjyNYr4ST4enO6TEdYSqWg,802
26
+ accusleepy/gui/resources_rc.py,sha256=Z2e34h30U4snJjnYdZVV9B6yjATKxxfvgTRt5uXtQdo,329727
27
+ accusleepy/gui/text/config_guide.txt,sha256=4Tbo1LctcJ_gdcr4Z3jBoLog5CUr8Pccqrgdi3uYU6g,903
28
+ accusleepy/gui/text/main_guide.txt,sha256=bIDL8SQKK88MhVqmgKYQWygR4oSfS-CsKNuUUOi2hzU,7684
29
+ accusleepy/gui/text/manual_scoring_guide.txt,sha256=onBnUZJyX18oN1CgjD2HSnlEQHsUscHpOYf11kTKZ4U,1460
30
+ accusleepy/gui/viewer_window.py,sha256=5PkbuYMXUegH1CExCoqSGDZ9GeJqCCUz0-3WWkM8Vfc,24049
31
+ accusleepy/gui/viewer_window.ui,sha256=D1LwUFR-kZ_GWGZFFtXvGJdFWghLrOWZTblQeLQt9kI,30525
32
+ accusleepy/models.py,sha256=Muapsw088AUHqRIbW97Rkbv0oiwCtQvO9tEoBCC-MYg,1476
33
+ accusleepy/multitaper.py,sha256=V6MJDk0OSWhg2MFhrnt9dvYrHiNsk2T7IxAA7paZVyE,25549
34
+ accusleepy/signal_processing.py,sha256=-aXnywfp1LBsk3DcbIMmZlgv3f8j6sZ6js0bizZId0o,21718
35
+ accusleepy-0.1.0.dist-info/METADATA,sha256=2m9Cniw45DzhegusOsXutTofuxtptRDi7I3I24s3C10,2158
36
+ accusleepy-0.1.0.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
37
+ accusleepy-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.1.2
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any