Download code/paya/preprocessing.py from yeabwang/Paya: direct link, hf CLI and curl.
- Browser
- Download file 4.56 kB
-
https://huggingface.co/yeabwang/Paya/resolve/main/code/paya/preprocessing.py
- Command line
-
hf download hf://yeabwang/Paya/code/paya/preprocessing.py
-
curl -L -o preprocessing.py https://huggingface.co/yeabwang/Paya/resolve/main/code/paya/preprocessing.py
4.56 kB
| """Validate and clean one aligned native-rate recording for the signal encoder.""" | |
| from dataclasses import dataclass, asdict, replace | |
| import hashlib | |
| import json | |
| import neurokit2 as nk | |
| import numpy as np | |
| import scipy | |
| import torch | |
| from .inputs import Channel, UnsupportedSignalError, validate_state | |
| from .physiology import clean_channel, check_eeg_channels | |
| from .provenance import PREPARATION_CONTRACT, NATIVE_CONTRACT | |
| from .vendor import signal | |
| class PreparationConfig: | |
| max_channels: int = signal.MAX_CHANNELS | |
| max_samples: int = signal.MAX_SAMPLES | |
| def __post_init__(self): | |
| if any(isinstance(v, bool) or not isinstance(v, int) or v <= 0 for v in asdict(self).values()): | |
| raise ValueError("Preparation budgets must be positive integers") | |
| if self.max_channels > signal.MAX_CHANNELS: | |
| raise ValueError(f"Encoder permits at most {signal.MAX_CHANNELS} channels") | |
| if not signal.MIN_SAMPLES <= self.max_samples <= signal.MAX_SAMPLES: | |
| raise ValueError(f"Encoder sample budget must be between {signal.MIN_SAMPLES} and {signal.MAX_SAMPLES}") | |
| class EncoderInput: | |
| samples: torch.Tensor # [C,L], the complete aligned recording | |
| def to(self, device): | |
| return EncoderInput(self.samples.to(device)) | |
| class PreparedState: | |
| channels: list[Channel] # Samples retain their declared native rates | |
| identity: str | |
| audit: dict | |
| class SignalPreprocessor: | |
| """Bypass NeuroKit for processed channels and preserve every native sampling rate.""" | |
| def __init__(self, config=None): | |
| self.config = config or PreparationConfig() | |
| def identity(self): | |
| return {"contract": PREPARATION_CONTRACT, "config": asdict(self.config), | |
| "native_processing": NATIVE_CONTRACT, "neurokit2": nk.__version__, | |
| "numpy": np.__version__, "scipy": scipy.__version__, "signal_condition": ["raw", "processed"], | |
| "sampling": "native-no-resampling", "format": "one-aligned-recording", | |
| "channel_order": "as-supplied", "minimum_samples": signal.MIN_SAMPLES, | |
| "encoder": signal.IDENTITY} | |
| def prepare(self, state): | |
| """Validate all channels and run NeuroKit only on those marked raw.""" | |
| channels = validate_state(state, self.config.max_channels, self.config.max_samples) | |
| prepared, quality, raw_channels, raw_audits = [], [], [], [] | |
| for channel in channels: | |
| if channel.metadata["signal_condition"] == "raw": | |
| values, observations = clean_channel(channel) | |
| values = np.asarray(values, dtype=np.float32) | |
| if values.shape != channel.samples.shape or not np.isfinite(values).all(): | |
| raise UnsupportedSignalError("Native cleaning changed sample count or produced nonfinite values") | |
| current = replace(channel, samples=values) | |
| observations["signal_condition"] = "raw" | |
| raw_channels.append(current) | |
| raw_audits.append(observations) | |
| else: | |
| current = channel | |
| observations = {"sensor_type": channel.sensor_type, "sampling_rate_hz": channel.rate, | |
| "signal_condition": "processed", "cleaning": "skipped"} | |
| prepared.append(current) | |
| quality.append(observations) | |
| if raw_channels: | |
| check_eeg_channels(raw_channels, [c.samples for c in raw_channels], raw_audits) | |
| audit = {"duration_s": channels[0].metadata["duration_s"], "channels": len(prepared), | |
| "preparation": self.identity(), "quality": quality} | |
| digest = hashlib.sha256(json.dumps(self.identity(), sort_keys=True).encode()) | |
| for channel in prepared: | |
| digest.update(json.dumps(channel.metadata, sort_keys=True, default=lambda x: np.asarray(x).tolist()).encode()) | |
| digest.update(channel.samples.tobytes()) | |
| return PreparedState(prepared, digest.hexdigest(), audit) | |
| def format(self, prepared): | |
| """Stack the complete recording in supplied channel order without external padding.""" | |
| if prepared.audit.get("preparation") != self.identity(): | |
| raise ValueError("Prepared signal does not match current preprocessing") | |
| samples = torch.from_numpy(np.stack([c.samples for c in prepared.channels])) | |
| return EncoderInput(samples) | |