Getting Started with SleePyPhases

From installation to a trained sleep staging model

This page is the central entry point for SleePyPhases. It walks through a complete sleep staging project end-to-end, explains every part of project.yaml, and links to detail pages for advanced use cases.


Installation

Requirements: Python ≥ 3.9

pip install SleePyPhases pyPhasesRecordloaderSleepEDF

Installing SleePyPhases automatically installs the full stack:

Package Role
pyPhases Core framework — Phase, Config, Project, Exporter
phases CLI — phases create, phases run
pyPhasesML ML extension — model management, training loop, evaluation
SleepHarmonizer PSG channel and annotation harmonization
SleePyPhases Multi-dataset sleep framework (this package)

Dataset loaders are installed separately as needed, e.g.:

pip install pyPhasesRecordloaderSleepEDF   # SleepEDF (PhysioNet)
pip install pyPhasesRecordloaderSHHS      # SHHS
# see the Plugins & Loaders reference for the full list

Create a project

phases create myproject

Answer the prompts (select sleep study, enter project name, create Init phase). The wizard generates a complete project skeleton:

myproject/
├── project.yaml                  ← central configuration file
└── myproject/
    ├── __init__.py
    ├── phases/
    │   └── Init.py               ← wires your custom classes into the framework
    ├── SignalPreprocessing.py    ← subclass for custom signal preprocessing steps
    ├── PreManipulation.py        ← subclass for whole-record manipulation before segmentation
    ├── DataManipulation.py       ← subclass for per-segment and batch manipulation
    └── models/
        └── MyModel/
            └── MyModel.py        ← PyTorch model stub

As an alternative, you can use a programmatic approach or a Python notebook to create a project and run phases without using the CLI. See the “flat / notebook approach” section in Setup for details.


Project structure and core concepts

project.yaml is the single source of truth for an experiment. Every config value participates in cache hashing: change any value and all downstream data that depend on it is automatically invalidated and regenerated.

The project is defined in YAML.

How it works

flowchart LR
    yaml["project.yaml<br/>(plugins + config)"] --> project["Project<br/>(pyPhases)"]
    project --> phases["Phases<br/>(Python classes)"]
    phases -->|registerData| exporter["Exporter <br/>(Pickle / Memmap / HDF5)"]
    exporter -->|hash-keyed cache| disk[(disk)]
    phases -->|getData| exporter

pyPhases core concepts explains phases, config hashing, and exporters in detail.

Loading additional config files

A project can split its configuration across multiple YAML files. Use importAfter (or importBefore) inside any YAML file to merge additional configs on load, or pass a file at runtime with the -c flag:

# project.yaml
name: myproject
plugins: [...]
importAfter:
  - configs/model.yaml    # merged after this file
  - configs/dataset.yaml
phases run Training -c configs/shhs.yaml   # merged at runtime, overrides base

Individual keys can also be overridden via environment variables: PHASE_CONFIG_preprocessing_targetFrequency=64.

This pattern is commonly used to keep a single project.yaml and swap dataset or model configs without editing the base file.

plugins

plugins:
  - pyPhasesRecordloaderSleepEDF   # dataset loader
  - pyPhasesRecordloader           # base loader support
  - SleepHarmonizer                # channel harmonization
  - pyPhasesML                     # ML training/eval infrastructure
  - SleePyPhases                   # provides the built-in pipeline phases

SleePyPhases registers the following built-in phases that you can run directly:

Phase What it does
LoadData Loads metadata and record IDs
Extract Loads raw records, runs signal preprocessing, writes harmonized cache
Training Model training loop — saves best checkpoint
EvalReport Generates HTML/Markdown evaluation report
Eval Intermediate phase that runs the evaluation on test split
ThresholdOptimisation Intermediate phase that finds optimal per-class thresholds on the validation fold
BuildDataset Intermediate phase that splits records into train/val/test folds

For a complete walkthrough, only the Training phase, and the EvalReport phases need to be run. LI automatically resolves dependencies: for example, running the Training will trigger BuildDataset, which triggers Extract if their outputs are not cached. Only EvalReport does not allow a full training run, to prevent unintended training runs. This can be changed if Eval.allowTraining is set to True.

For transparency and understanding, we list the intermediate phases here, but they are not required to be run directly in the common case. LoadData and Extract do not need to be run separately, but are listed because they are often run separately. One of the first steps is often data exploration, where the metadata is analysed without requiring training. Often pipelines do not require a GPU when running Extract and can therefore be run separately with more optimised hardware.

phases (your custom phases)

On default the wizard generates an Init phase that wires your custom subclasses into the framework before any other phase runs:

# myproject/phases/Init.py
from pyPhases import Phase
from SleePyPhases import SignalPreprocessing as SP, PreManipulation as PM, DataManipulation as DM
from myproject.SignalPreprocessing import SignalPreprocessing
from myproject.PreManipulation import PreManipulation
from myproject.DataManipulation import DataManipulation

class Init(Phase):
    def prepareConfig(self):
        SP.setClass(SignalPreprocessing)
        PM.setClass(PreManipulation)
        DM.setClass(DataManipulation)

SignalPreprocessing, PreManipulation, and DataManipulation are swappable classes inside SleePyPhases — the framework instantiates them by calling getInstance(), which returns your subclass instead of the base class once setClass has been called. This means you can override or extend any individual processing step without touching the rest of the pipeline. → Swappable Classes explains all swappable classes, what each one controls, and how to extend them.

You can add your own phases to the phases list in project.yaml and implement them as Python classes. They can be used for custom data manipulation and analysis, additional evaluation steps, or even self-supervised pretraining. For more details see “Phase” in pyPhases core concepts.


project.yaml — full walkthrough

Below is the annotated project.yaml from the Single-Page project. Every section is explained with links for more detailed configuration.

name: simplecnn

plugins:
  - pyPhasesRecordloaderSleepEDF
  - pyPhasesRecordloader
  - SleepHarmonizer
  - pyPhasesML
  - SleePyPhases

phases:
  - name: Init

config: {}  

sleepedf-path — dataset location

useLoader: sleepedf          # which loader plugin to activate
sleepedf-path: /data/sleepedf

useLoader selects the active dataset plugin. The default path key is <loadername>-path and is consumed by the corresponding loader package.

Load Data lists all available loaders, channel loading vs. selection, multi-dataset training with datafold.datasets, and external evaluation with evalOn. → To add a new public dataset or configure a vendor device, see New Record Loader and Vendor Config.


dataversion — data splits

dataversion:
  version: Debug-split    # label for identification, part of cache hash
  seed: null              # random seed for shuffling, before splitting
  folds: 5                # number of cross-validation folds
  split:
    trainval: "0:170"     # record index range for train + validation
    test: "170:197"       # held-out test set

→ For advanced split options (groupBy, recordIds, filterQuery, validationSplit and named splits), see Dataset Splits.


labelChannels — ground-truth channels

labelChannels:
  - SleepStagesAASM

Defines which annotation channels are extracted from the raw recordings and used as ground truth. The available harmonized label channels are:

Channel Description
SleepStagesAASM AASM sleep staging (W / R / N1 / N2 / N3)
SleepArousals Arousal events
SleepArousalRera Distinguish between Arousal and RERA arousals
RespEvents Respiratory events (obstructive, central, mixed, hypopnea) + RERA flow limitation
SleepApnea Respiratory events (obstructive, central, mixed, hypopnea)
SleepLegMovements Leg movement events
SleepLegMovementsPLM PLM-filtered leg movements

→ For multi-label studies add multiple entries, each label is added to the Y as channel.
→ To define your own harmonized label channels, see Adding Custom Label Channels (WIP).


classification — label and class names

classification:
  name: sleep
  labelNames:
    - Sleep
  classNames:
    - [Wake, R, N1, N2, N3]

labelNames and classNames give human-readable identifiers used in evaluation reports and metric tables. The number of classes for each label is defined by the length of the corresponding classNames list.

For multi-task models (predicting sleep stages, arousals, respiratory events, and leg movements simultaneously), see Classification Specification, which covers scorerTypes, predictionSignals, predictionFrequencies, and their relationship to clinical metrics.


preprocessing — signal preprocessing

preprocessing:
  targetFrequency: 32          # Hz — all signals resampled to this rate
  labelFrequency: 0.03333      # Hz — 1/30 for 30-second epoch labels
  stepsPerType:
    eeg: [resample]            # method names on your SignalPreprocessing class
  manipulationSteps:
    - name: trimSPT            # whole-record manipulation before saving
  targetChannels:
    - [EEG Fpz-Cz]            # channels to keep in the output file

stepsPerType maps a channel type (eeg, eog, emg, ppg, ecg, …) or a specific channel name (e.g. EEG Fpz-Cz) to an ordered list of steps. Each step is either a plain string (method name) or a dict with name plus any keyword arguments forwarded to the method — the same principle as data manipulation steps. Methods receive (self, signal, recordSignal, **kwargs) — see Preprocess Signals for full API documentation.

# myproject/SignalPreprocessing.py
from scipy.signal import resample
from SleePyPhases import SignalPreprocessing as SPPSignalPreprocessing

class SignalPreprocessing(SPPSignalPreprocessing):
    def resample(self, signal, recordSignal):
        factor = recordSignal.targetFrequency / signal.frequency
        signal.signal = resample(signal.signal, int(len(signal.signal) * factor))
        signal.frequency = recordSignal.targetFrequency
Note

targetFrequency and labelFrequency are configuration constants — they do not automatically resample signals. Your preprocessing methods are responsible for the actual resampling (e.g. using recordSignal.targetFrequency).

manipulationSteps are whole-record operations applied after per-channel steps — before the data is written to the cache. Common uses include tailor recording to sleep scoring.

Preprocess Signals covers parameter passing and how to implement and configure seperate featureChannels and combineChannels.


recordWise — full recording vs. windowed segments

recordWise: true
Value Behaviour
true The dataset yields one full recording per sample. The model receives the entire night. Use with segmentManipulation to pad/trim to a fixed size.
false The dataset yields fixed-length windows. Use with segmentLength.

For windowed models, add:

recordWise: false
segmentLength: 3000 # window length in samples

More details on windowing and segment generation in Manipulate Data.


segmentManipulation — per-segment steps

segmentManipulation:
  - name: addBatchDimension # insert batch dimension
  - name: fixedSize         # pad/trim to fixed (X, Y) dimensions
    sizeX: 806400
    sizeY: 840

Each step is a method on your DataManipulation subclass, called with (X, Y, **kwargs) where kwargs come from the YAML dict. Steps are applied in order before a segment enters a batch.

# myproject/DataManipulation.py
import numpy as np, torch
from SleePyPhases import DataManipulation as SPPDataManipulation

class DataManipulation(SPPDataManipulation):
    def fixedSize(self, X, Y, sizeX, sizeY, fillValue=0, fillValueY=-1):
        # pad or truncate X and Y to fixed dimensions
        ...
        return X, Y

    def toTensor(self, X, Y):
        X = torch.tensor(np.stack(X), dtype=torch.float32)
        Y = torch.tensor(np.stack(Y), dtype=torch.float32)
        X = X.permute(0, 2, 1) # (batch, samples, channels) → (batch, channels, samples) as expected by the model

        return X, Y

Manipulate Data for more details.


batchManipulation — per-batch steps

batchManipulation:
  - name: toTensor

Same mechanism as segmentManipulation but applied to the assembled batch just before it is fed to the model.


modelPath / modelName — model class

modelPath: simplecnn/models    # directory containing model subdirectories
modelName: SimpleCNN           # must match subdirectory name and class name
inputShape: [806400, 1]        # [samples, channels] — passed to define()

The model class lives at {modelPath}/{modelName}/{modelName}.py and must subclass ModelTorchAdapter (PyTorch) or ModelTFAdapter (TensorFlow).

Method Purpose
define() Build architecture, assign to self.model
prepareX(X) Reshape/cast input batch before forward pass
prepareY(Y) Reshape/cast ground truth for loss computation
getLossFunction() Return loss function instance
mapOutputForLoss(output) Reshape model output to match loss expectations
prepareForScore(targets, pred) Reshape for metric computation

Additional model.* config keys are passed as options to the model — useful for architecture hyperparameters:

model:
  seq_len: 20
  num_blocks: 1
  fc_dropout: 0.1

Access inside the model: self.getOption("seq_len").

Generated model stub

The wizard produces a ready-to-fill PyTorch model class:

# myproject/models/MyModel/MyModel.py
import torch.nn as nn
from pyPhasesML.adapter.ModelTorchAdapter import ModelTorchAdapter

class MyModel(ModelTorchAdapter):
    def define(self):
        """Build your architecture here. self.inputShape is set from config."""
        self.model = nn.Sequential(
            # TODO: replace with your architecture
            nn.Flatten(),
            nn.Linear(self.inputShape[0] * self.inputShape[1], 5),
        )

    def prepareY(self, y, validation=False):
        y = super().prepareY(y, validation)
        return y.long().squeeze(-1)

    def getLossFunction(self):
        return nn.CrossEntropyLoss(ignore_index=self.ignoreClassIndex)

    def mapOutputForLoss(self, output):
        return output.permute(0, 2, 1)

    def prepareForScore(self, targets, prediction):
        return targets, prediction

Define a Model covers the full interface and all overridable methods.
→ For advanced use cases — overloading the training loop, self-supervised pretraining, two-stage training — see Define a Model.


trainingParameter — training loop config

trainingParameter:
  maxEpochs: 100
  lr: 0.001
  batchSize: 32
  optimizer: adams                # adams, sgd, rmsprop
  stopAfterNotImproving: 10       # early stopping patience
  shuffle: True                   # whether to shuffle training data each epoch
  validationMetrics:
    - kappa                       # metric used for model selection

All keys are consumed by pyPhasesML’s training loop.

Advanced options:

Key Description
cyclicLearningRate Enable CLR schedule
findCyclicLearningRate Run LR range finder before training
batchSizeAccumulation Gradient accumulation steps (effective batch = batchSize × batchSizeAccumulation)
weightDecay L2 regularization
gradientClip Max gradient norm
classWeights Per-class loss weights
shuffle / shuffleSeed Shuffle training data

Train for all keys and cross-validation usage.


fixedThreshold — classification thresholds

fixedThreshold: [0.5]

Per-class probability threshold applied when converting model output to class predictions. Set to null (or omit) to use the ThresholdOptimisation phase, which finds optimal thresholds on the validation fold:

phases run ThresholdOptimisation
phases run Eval

For multi-task models, one threshold per task:

fixedThreshold: null              # run ThresholdOptimisation instead
optimizeThresholdFor: [Arousal, LM]
optimizeOn: validation
thresholdMetric: [kappa, f1, f1, f1]

Evaluate covers threshold optimization in detail.


manipulationAfterPredict — post-prediction steps

manipulationAfterPredict:
  - name: toNumpy

DataManipulation steps applied to (prediction, ground_truth) after the model inference, before metrics are computed. Common uses:

Step Purpose
toNumpy Convert PyTorch tensors to numpy arrays
deleteIgnored Remove timesteps where ground truth is -1 (the ignoreClassIndex), restoring original recording length after padding
transposeX Transpose prediction axes
hotDecode Convert one-hot predictions to class indices
toEvent Convert per-sample predictions to event spans for event-level evaluation

Run the pipeline

# Run training
phases run Training

# Run for a specific cross-validation fold
phases run Training --fold 2

# Run evaluation
phases run EvalReport # this will not trigger training, unless its set in the config

The CLI resolves phase dependencies automatically: Training will trigger Extract and BuildDataset first if their outputs are not cached.


Next steps

More details on each config section and advanced use cases are covered in the linked pages below. - Load Data — dataset plugins, multi-dataset training - Preprocess Signals — signal preprocessing steps - Manipulate Data — per-segment and batch steps - Define a Model — model architecture and adapters - Train — training loop and cross-validation - Evaluate — metrics, thresholds, clinical metrics - Config Reference — all config keys in one table