""" domain/interfaces/services/signal_processor.py ──────────────────────────────────────────────── SignalProcessor — abstract contract for PPG signal preprocessing. Implements the **Template Method** pattern: ``process()`` defines the fixed pipeline (filter → normalize → segment) while ``filter_signal()``, ``normalize()``, and ``segment()`` are abstract and overridden by concrete implementations (e.g. ScipySignalProcessor). """ from __future__ import annotations from abc import ABC, abstractmethod import numpy as np class SignalProcessor(ABC): """ PPG signal preprocessing contract. Pipeline (Template Method): raw signal → filter_signal() → normalize() → segment() → segments array """ # ── Abstract Steps ──────────────────────────────────────────────────────── @abstractmethod def filter_signal( self, signal: np.ndarray, sampling_rate: float, ) -> np.ndarray: """ Apply a bandpass filter to remove noise and baseline wander. Args: signal: 1-D array of raw PPG amplitude values. sampling_rate: Sampling rate of the signal in Hz. Returns: Filtered signal as a 1-D NumPy array of the same shape. """ ... @abstractmethod def normalize(self, signal: np.ndarray) -> np.ndarray: """ Normalise the signal to a standard scale. Args: signal: 1-D filtered PPG signal. Returns: Normalised signal (e.g. Z-score: mean=0, std=1). """ ... @abstractmethod def segment( self, signal: np.ndarray, sampling_rate: float, ) -> np.ndarray: """ Split the signal into fixed-length windows. Args: signal: 1-D normalised PPG signal. sampling_rate: Sampling rate in Hz. Returns: 2-D NumPy array of shape ``(n_segments, window_size)``. """ ... # ── Template Method ─────────────────────────────────────────────────────── def process( self, raw_signal: list[float], sampling_rate: float, ) -> np.ndarray: """ Execute the full preprocessing pipeline. Steps: 1. Convert list → NumPy array 2. filter_signal() 3. normalize() 4. segment() Args: raw_signal: Raw PPG values from a PPGSignal entity. sampling_rate: Sampling rate of the recording in Hz. Returns: 2-D NumPy array of shape ``(n_segments, window_size)`` ready for model inference. """ arr = np.asarray(raw_signal, dtype=np.float64) filtered = self.filter_signal(arr, sampling_rate) normalised = self.normalize(filtered) segments = self.segment(normalised, sampling_rate) return segments