Download code/paya/physiology.py from yeabwang/Paya: direct link, hf CLI and curl.
- Browser
- Download file 5.99 kB
-
https://huggingface.co/yeabwang/Paya/resolve/main/code/paya/physiology.py
- Command line
-
hf download hf://yeabwang/Paya/code/paya/physiology.py
-
curl -L -o physiology.py https://huggingface.co/yeabwang/Paya/resolve/main/code/paya/physiology.py
5.99 kB
| """Native-rate signal cleaning and physiological quality observations.""" | |
| import warnings | |
| import neurokit2 as nk | |
| import numpy as np | |
| from .inputs import UnsupportedSignalError | |
| def _filter(values, rate, low=None, high=None, order=4): | |
| return nk.signal_filter(values, sampling_rate=rate, lowcut=low, highcut=high, | |
| method="butterworth", order=order) | |
| def _events(info, keys, rate): | |
| result = {} | |
| for key in keys: | |
| indices = np.asarray(info[key], dtype=float) | |
| result[key] = (indices[np.isfinite(indices)]/rate).tolist() | |
| return result | |
| def _cardiac_quality(cleaned, peaks, rate, kind): | |
| if len(peaks) < 4: | |
| raise ValueError("At least four detected beats are needed for template quality") | |
| if kind == "ecg": | |
| quality = nk.ecg_quality(cleaned, rpeaks=peaks, sampling_rate=rate, method="templatematch") | |
| else: | |
| quality = nk.ppg_quality(cleaned, peaks=peaks, sampling_rate=rate, method="templatematch") | |
| quality = np.asarray(quality, dtype=float) | |
| valid = quality[np.isfinite(quality)] | |
| if not len(valid): | |
| raise ValueError("No finite template quality values") | |
| return {"method": "templatematch", "median": float(np.median(valid)), | |
| "finite_fraction": float(len(valid)/len(quality))} | |
| def _beats(audit, cleaned, rate, kind): | |
| detect, method, key = ((nk.ecg_peaks, "neurokit", "ECG_R_Peaks") if kind == "ecg" | |
| else (nk.ppg_peaks, "elgendi", "PPG_Peaks")) | |
| _, info = detect(cleaned, sampling_rate=rate, method=method, correct_artifacts=False) | |
| detected = np.asarray(info[key], dtype=int) | |
| # Exclude incomplete boundary beats from measurements, retaining their times. | |
| interior = (detected >= 0.25*rate) & (detected < len(cleaned)-0.25*rate) | |
| peaks = detected[interior] | |
| _observe(audit, "template_quality", lambda: _cardiac_quality(cleaned, peaks, rate, kind)) | |
| return {"method": method, **_events({key: peaks}, [key], rate), | |
| "boundary_peaks_s": (detected[~interior]/rate).tolist(), "boundary_margin_s": 0.25, | |
| "correct_artifacts": False} | |
| def _observe(audit, name, operation): | |
| # Detector failure can reflect abnormal physiology; preserve the waveform. | |
| with warnings.catch_warnings(record=True) as caught: | |
| warnings.simplefilter("always") | |
| try: | |
| audit[name] = {"status": "available", **operation()} | |
| except (ValueError, IndexError, ZeroDivisionError) as error: | |
| audit[name] = {"status": "unavailable", "reason": str(error)} | |
| if caught: | |
| audit[name]["warnings"] = list(dict.fromkeys(str(w.message) for w in caught)) | |
| def clean_channel(channel): | |
| """Return one cleaned waveform and native-time physiological observations.""" | |
| raw = channel.samples.astype(np.float64) | |
| rate, kind = channel.rate, channel.sensor_type | |
| audit = {"sensor_type": kind, "sampling_rate_hz": rate, | |
| "observations": len(raw), "flatline": bool(np.ptp(raw) == 0), | |
| "minimum": float(raw.min()), "maximum": float(raw.max())} | |
| values = raw | |
| powerline = channel.metadata.get("powerline_hz", 0) | |
| if powerline and kind in ("ecg", "eeg", "ieeg"): | |
| if powerline+1 >= rate/2: | |
| raise UnsupportedSignalError("Declared powerline band is outside the native Nyquist limit") | |
| # NeuroKit selects a narrow band-stop when lowcut exceeds highcut. | |
| values = _filter(values, rate, low=powerline+1, high=powerline-1, order=2) | |
| audit["powerline_hz"] = powerline | |
| detection = None | |
| if kind == "ecg": | |
| broad = _filter(values, rate, high=min(150, 0.4*rate)) | |
| if rate > 2: | |
| # This is NeuroKit's ECG high-pass stage, without its implicit 50 Hz smoothing. | |
| detection = _filter(values, rate, low=0.5, order=5) | |
| audit["cleaning"] = "broad: DC-preserving low-pass <=150 Hz; detection: NeuroKit 0.5 Hz high-pass" | |
| _observe(audit, "beats", lambda: _beats(audit, detection, rate, kind)) | |
| else: | |
| audit["cleaning"] = "broad low-pass; native rate insufficient for ECG detection" | |
| elif kind == "ppg": | |
| broad = _filter(values, rate, high=min(40, 0.4*rate)) | |
| if rate > 16: | |
| detection = nk.ppg_clean(values, sampling_rate=rate, method="elgendi") | |
| _observe(audit, "beats", lambda: _beats(audit, detection, rate, kind)) | |
| audit["cleaning"] = "broad: DC-preserving low-pass <=40 Hz; detection: Elgendi 0.5-8 Hz when native rate permits" | |
| elif kind == "eeg": | |
| broad = _filter(values, rate, high=min(100, 0.4*rate)) | |
| audit["cleaning"] = "DC-preserving low-pass <=100 Hz; supplied reference retained" | |
| elif kind == "ieeg": | |
| broad = _filter(values, rate, high=0.4*rate) | |
| audit["cleaning"] = "DC-preserving low-pass at 0.4*fs; supplied intracranial reference retained" | |
| else: | |
| raise UnsupportedSignalError(f"Unsupported sensor type: {kind}") | |
| if broad.shape != raw.shape or not np.isfinite(broad).all(): | |
| raise UnsupportedSignalError(f"Invalid {kind} waveform after native-rate cleaning") | |
| return broad, audit | |
| def check_eeg_channels(channels, waveforms, audits): | |
| """Flag aligned EEG channel outliers without removing channels.""" | |
| indices = [i for i, c in enumerate(channels) if c.sensor_type == "eeg"] | |
| if len(indices) < 4 or len({channels[i].metadata.get("reference") for i in indices}) != 1: | |
| return | |
| report = {} | |
| _observe(report, "channels", lambda: {"bad_indices": nk.eeg_badchannels( | |
| np.stack([waveforms[i] for i in indices]))[0]}) | |
| result = report["channels"] | |
| for local, i in enumerate(indices): | |
| audits[i]["channel_quality"] = {"status": result["status"], "method": "eeg_badchannels", | |
| "flagged": local in result.get("bad_indices", [])} | |
| if result["status"] == "unavailable": | |
| audits[i]["channel_quality"]["reason"] = result["reason"] | |