Swappable Classes
How SleePyPhases lets you extend processing behaviour without modifying framework code
SleePyPhases defines several swappable classes — base classes that implement built-in processing steps and can be replaced by your own subclass at runtime, on a per-project basis. The mechanism comes from the Swappable mixin in pyPhases:
class Swappable:
useClass = None
@classmethod
def getInstance(cls, *args, **kwargs):
if cls.useClass is None:
return cls(*args, **kwargs)
return cls.useClass(*args, **kwargs)
@classmethod
def setClass(cls, subclass):
cls.useClass = subclassWhen the framework needs an instance it calls MyClass.getInstance(...). If your project has called MyClass.setClass(YourSubclass), it gets an instance of YourSubclass instead. Registration might happen in a phase’s prepareConfig method, which runs before any other phase is executed:
# myproject/phases/Init.py
from SleePyPhases import SignalPreprocessing as SP, PreManipulation as PM, DataManipulation as DM
class MySignalPreprocessing(SP):
# override or add methods here
pass
class Init(Phase):
def prepareConfig(self):
SP.setClass(MySignalPreprocessing)
PM.setClass(MyPreManipulation)
DM.setClass(MyDataManipulation)Swappable classes in SleePyPhases
SignalPreprocessing
Module: SleePyPhases.SignalPreprocessing
Controlled by config key: preprocessing.stepsPerType
Handles per-channel signal preprocessing. Each step is a method on this class that receives (self, signal, recordSignal). Steps are applied per channel type (e.g. eeg, eog, emg) or per named channel (e.g. EEG Fpz-Cz).
from SleePyPhases import SignalPreprocessing as SPPSignalPreprocessing
class SignalPreprocessing(SPPSignalPreprocessing):
def bandpass(self, signal, recordSignal, low=0.3, high=35.0):
from scipy.signal import butter, sosfiltfilt
sos = butter(5, [low, high], btype="band", fs=signal.frequency, output="sos")
signal.signal = sosfiltfilt(sos, signal.signal)
def resample(self, signal, recordSignal):
from scipy.signal import resample as sp_resample
target = recordSignal.targetFrequency
n_samples = int(len(signal.signal) / signal.frequency * target)
signal.signal = sp_resample(signal.signal, n_samples)
signal.frequency = target→ Preprocess Signals covers the full step API and config format.
PreManipulation
Module: SleePyPhases.PreManipulation
Controlled by config key: preprocessing.manipulationSteps
Operates on the whole RecordSignal (and its event list) after per-channel preprocessing and before the record is written to the cache. Receives (recordSignal, events) and returns the same pair (or raises ChannelsNotPresent to discard the record).
Common uses: removing records that lack required sleep stages, trimming recordings to lights-off windows, merging annotation channels.
from SleePyPhases import PreManipulation as SPPPreManipulation
from pyPhasesRecordloader import ChannelsNotPresent
class PreManipulation(SPPPreManipulation):
def keep_full_night(self, recordSignal, events):
"""Discard recordings shorter than 6 hours."""
duration_h = recordSignal.getSignalLength() * recordSignal.targetFrequency / 3600
if duration_h < 6:
raise ChannelsNotPresent("recording too short")
return recordSignal, eventsDataManipulation
Module: SleePyPhases.DataManipulation
Controlled by config keys: segmentManipulation, batchManipulation, manipulationAfterPredict
Per-segment and per-batch transformation. Steps receive (X, Y, **kwargs) where X has shape (batch, samples, channels) and Y has shape (batch, samples, labels). Steps must mutate X and Y in-place (or re-assign them via return) — the framework picks up whichever is returned.
The same class handles three distinct pipeline stages, configured by three separate YAML keys:
| Config key | When called |
|---|---|
segmentManipulation |
Per segment, in the data loader, before batching |
batchManipulation |
Per batch, after collation, before the model |
manipulationAfterPredict |
After inference, before metric computation, where X is model prediction |
from SleePyPhases import DataManipulation as SPPDataManipulation
import numpy as np
class DataManipulation(SPPDataManipulation):
def normalize_ecg(self, X, Y, channel=0):
"""Z-normalize a specific channel."""
mu = X[:, :, channel].mean(axis=1, keepdims=True)
sigma = X[:, :, channel].std(axis=1, keepdims=True) + 1e-9
X[:, :, channel] = (X[:, :, channel] - mu) / sigma→ Manipulate Data lists all built-in steps and explains return semantics.
FeatureExtraction
Module: SleePyPhases.FeatureExtraction
Controlled by config key: preprocessing.featureChannels
Derives additional channels from the raw RecordSignal during the preprocessing step, before data is written to the cache. Each step receives a RecordSignal segment and returns a np.ndarray that is appended as a new channel.
from SleePyPhases import FeatureExtraction as SPPFeatureExtraction
import numpy as np
class FeatureExtraction(SPPFeatureExtraction):
def log_power(self, segmentSignal):
"""Return log-power of the first EEG channel as a feature channel."""
eeg = segmentSignal.getSignalByName("EEG").signal
power = np.convolve(eeg ** 2, np.ones(128) / 128, mode="same")
return np.log1p(power)LabelChannels
Module: SleePyPhases.LabelChannels
Controlled by config key: labelChannels
Converts the harmonized event-signal dict into per-sample label arrays during the Extract phase. Each method receives (eventSignal, signalLength) and returns a np.ndarray. Built-in channels (SleepStagesAASM, RespEvents, SleepArousals, etc.) are already implemented; subclass to add project-specific ones or override existing behaviour.
from SleePyPhases import LabelChannels as SPPLabelChannels
import numpy as np
class LabelChannels(SPPLabelChannels):
def SleepBin(self, eventSignal, signalLength):
"""Binary sleep / wake label."""
from SleepHarmonizer import PSGEventManager
arr = np.zeros(signalLength)
if "sleepStage" in eventSignal:
stage = eventSignal["sleepStage"]
arr[(stage == PSGEventManager.INDEX_REM)
| (stage == PSGEventManager.INDEX_NREM1)
| (stage == PSGEventManager.INDEX_NREM2)
| (stage == PSGEventManager.INDEX_NREM3)] = 1
return arr→ Adding Custom Label Channels covers the full API with all built-in channels listed.
RecordFeatureExtraction
Module: SleePyPhases.RecordFeatureExtraction
Operates on full recordings to extract tabular features stored in a DataFrame. Used in analysis pipelines, not in the model training path. Steps receive (recordSignal, events, **kwargs) and return a feature dict or pd.Series that is accumulated into the output DataFrame.
from SleePyPhases import RecordFeatureExtraction as SPPRecordFE
import numpy as np
class RecordFeatureExtraction(SPPRecordFE):
def sleep_efficiency(self, recordSignal, events, channel="SleepStagesAASM"):
sleep_events = [e for e in events if e.name in ["N1", "N2", "N3", "R"]]
total_sleep_s = sum(e.duration for e in sleep_events)
total_s = recordSignal.getDuration()
return {"sleep_efficiency": total_sleep_s / total_s}Registering in Init
All classes can be registered together in a single phase (e.g. ‘Init’). The wizard automatically generates this boilerplate code, so only add the classes that you want to customize.
# myproject/phases/Init.py
from pyPhases import Phase
from SleePyPhases import (
SignalPreprocessing as SP,
PreManipulation as PM,
DataManipulation as DM,
FeatureExtraction as FE,
LabelChannels as LC,
RecordFeatureExtraction as RFE,
)
from myproject.SignalPreprocessing import SignalPreprocessing
from myproject.PreManipulation import PreManipulation
from myproject.DataManipulation import DataManipulation
from myproject.FeatureExtraction import FeatureExtraction
from myproject.LabelChannels import LabelChannels
from myproject.RecordFeatureExtraction import RecordFeatureExtraction
class Init(Phase):
def prepareConfig(self):
SP.setClass(SignalPreprocessing)
PM.setClass(PreManipulation)
DM.setClass(DataManipulation)
FE.setClass(FeatureExtraction)
LC.setClass(LabelChannels)
RFE.setClass(RecordFeatureExtraction)If setClass is never called for a given swappable, the framework uses the built-in base class. You only need to register the classes you extend.