Preprocess Signals
Signal preprocessing runs during the Extract phase. It operates on raw RecordSignal objects. The main preprocessing stage is per-channel, where you can apply steps like filtering and resampling. There is also a whole-record manipulation stage for operations that require the full signal (e.g. trimming based on sleep events or discarding records on specific criteria). After preprocessing, all recordings of a single dataset are saved as a single file to data-processed and data-features, either by a MemMap or H5 writer (see Big File Extractor).
How it works
SleePyPhases dispatches preprocessing steps to methods on a SignalPreprocessing class. You extend the base class provided by SleePyPhases (which itself extends pyPhasesRecordloader’s base) and add or override methods. For more details on how to connect Signal Processing to SleePyPhases, see Swappable Classes.
pyPhasesRecordloader.SignalPreprocessing (base)
└── SleePyPhases.SignalPreprocessing (adds rls, iir, fftConvolution)
└── yourproject.SignalPreprocessing (your custom steps)
Configuring preprocessing steps
All preprocessing is driven by the preprocessing config block:
preprocessing:
targetFrequency: 32 # Hz — target sample rate for all channels
labelFrequency: 0.03333 # Hz — 1/30 for 30-second epoch labels
stepsPerType:
eog: [resample, bandpass] # list of method names to call, in order
eeg: # applied to all EEG channels
- name: bandpass # different annotation style: dict with name and method params
low: 0.3 # parameters passed to the method
high: 35.0
- name: resample
emg: []
EEG Fpz-Cz: [resample] # applied to specific channel by name
manipulationSteps:
- name: trimSPT # post-resample whole-record manipulations
before: 30 # seconds to keep before first sleep event
after: 30 # seconds to keep after last sleep event
targetChannels:
- [EEG Fpz-Cz] # which channels survive to the datasetEach entry in stepsPerType maps a channel type or channel name to a list of method names. The method is looked up on your SignalPreprocessing instance and called with (signal, recordSignal, **kwargs).
Config keys
| Key | Type | Default | Description |
|---|---|---|---|
preprocessing.targetFrequency |
float |
— | Target sample rate in Hz after resampling |
preprocessing.labelFrequency |
float |
— | Label frequency in Hz (e.g. 0.03333 = 1 epoch/30 s) |
preprocessing.stepsPerType |
dict[str, list[str]] |
{} |
Per-channel-type ordered list of preprocessing method names |
preprocessing.manipulationSteps |
list[{name}] |
[] |
Whole-record manipulation steps applied after per-channel steps |
preprocessing.targetChannels |
list[list[str]] |
— | Channels that are kept; others are discarded |
Target frequency only set the values for the recordSignal.targetFrequency, but does not trigger any resampling by itself. You must add a resampling method to the stepsPerType for it to be applied.
Writing preprocessing methods
Create yourproject/SignalPreprocessing.py:
from scipy.signal import resample
from SleePyPhases import SignalPreprocessing as SPPSignalPreprocessing
class SignalPreprocessing(SPPSignalPreprocessing):
def resample(self, signal, recordSignal):
"""Resample signal to recordSignal.targetFrequency."""
factor = recordSignal.targetFrequency / signal.frequency
signal.signal = resample(signal.signal,
int(len(signal.signal) * factor))
signal.frequency = recordSignal.targetFrequency
def bandpass(self, signal, recordSignal, low=0.3, high=35.0):
"""Example: apply a bandpass filter."""
from scipy.signal import butter, filtfilt
nyq = signal.frequency / 2
b, a = butter(4, [low / nyq, high / nyq], btype="band")
signal.signal = filtfilt(b, a, signal.signal)Method signature: (self, signal, recordSignal) where:
signal.signal— numpy array of raw samplessignal.frequency— original sample frequency (Hz)recordSignal.targetFrequency— target frequency from config
Modify signal.signal and signal.frequency in place.
Built-in steps (from SleePyPhases.SignalPreprocessing)
| Method | Description |
|---|---|
rls |
Recursive Least Squares adaptive filter |
iir |
IIR filter |
fftConvolution |
Frequency-domain convolution |
Event Manipulation and whole record manipulation (PreManipulation)
Preprocessing steps that operate on the whole record (e.g. trimming, merging events, etc.) can be added as methods to a PreManipulation class and configured in the preprocessing.manipulationSteps list. See Swappable Classes for details on how to connect this to SleePyPhases.
from pyPhasesRecordloader import RecordSignal, Event, ChannelsNotPresent
from SleePyPhases import PreManipulation as sppPreManipulation
class PreManipulation(SPPPreManipulation):
def trimSPT(self, recordSignal: RecordSignal, events: List[Event], before = 0, after = 0):
allSleepStages = ["R", "N1", "N2", "N3", "W"]
sleepEvents = [e for e in events if e.name in allSleepStages]
offsetStart = sleepEvents[0].end()-before if sleepEvents[0].name == "W" else 0
offsetEnd = sleepEvents[-1].start+ after if sleepEvents[-1].name == "W" else sleepEvents[-1].end()
offsetStart = max(offsetStart, 0)
epochModMax = recordSignal.getShape()[1] // recordSignal.targetFrequency // 30 * 30 - offsetStart
offsetEnd = min(offsetEnd, sleepEvents[-1].end(), epochModMax)
recordSignal.signalOffset(offsetStart, offsetEnd, offsetFrequency=1)
events = self._trimEventsByOffset(events, offsetStart, offsetEnd)
return recordSignal, eventsCombine channels
The combineChannels config key allows you to specify methods that combine multiple channels into a single one, which is part of the data harmonization process. For example, you can derive a bipolar EEG channel from two monopolar channels:
combineChannels:
- name: F3-M2
combineType: derived
type: eeg
channel: [F3, M2]Feature channels
featureChannels allows ML-based or transform-based feature extraction inside preprocessing. The methodname is specified using the name field. The method receives the full RecordSignal object, so it can access, add or manipulate any channel. By default the channels field specifies the name(s) of the new channel(s) to be created which should be returend by the method.
featureChannels:
- name: spectogram
epoch_second: 30
win_size: 2
overlap: 1
channel: [EEG, EEG Fpz-Cz]
channels: [spectogram-eeg]This example config extracts spectrogram features from the EEG and EEG Fpz-Cz channels using a sliding window approach with 2-second windows and 1-second overlap. The resulting spectrogram features are saved as new channels named spectogram-eeg.
from pyPhasesML import FeatureExtraction as pyPhasesFeatureExtraction
from pyPhasesRecordloader import RecordSignal, Signal
from SleePyPhases import FeatureExtraction as pyPhasesFeatureExtraction
class FeatureExtraction(pyPhasesFeatureExtraction):
def spectogram(self, recordSignal: RecordSignal, channel, epoch_second, win_size, overlap):
import numpy as np
signal = recordSignal.getFirstSignalByName(channel)
fs = signal.frequency
X = signal.signal
# see sleeptransformer for full implementation:
# https://gitlab.com/sleep-is-all-you-need/reproduce/spp-sleeptranformer/-/blob/main/src/FeatureExtraction.py
# ...
spectrograms = np.transpose(spectrograms, (0, 1, 2))
return spectrogramsThe Feature channels can be used to generate compute heavy features such as machine learning features that are stored before the training.
When using GPU for feature extraction use following one of following configurations to avoid GPU memory issues:
Extract:
useMultiThreading: false # no multi-threading depending on gpu memory
# or
Extract:
useMultiThreading: true
spawnMultiprocessing: true
threads: 4 # set to a number small enough to fit in gpu memoryWiring the classes
The Init phase wires your class into the framework (already generated by phases create):
from SleePyPhases import SignalPreprocessing as SP, PreManipulation as PM
from myproject.SignalPreprocessing import SignalPreprocessing
from myproject.PreManipulation import PreManipulation
class Init(Phase):
def prepareConfig(self):
SP.setClass(SignalPreprocessing)
PM.setClass(PreManipulation)Next step
→ Manipulate data — segment, normalize, and format tensors for ML