Source code for intan.ml._prediction_modes

#!/usr/bin/env python3
"""
intan.ml._prediction_modes

Prediction pipeline functions for EMG gesture classification.
Provides standardized interfaces for file, batch, record, and stream prediction modes.
"""

import os
import logging
import numpy as np
from typing import Optional


[docs] def predict_file( root_dir: str, file_path: str, label: str = "", window_ms: Optional[int] = None, step_ms: Optional[int] = None, events_file: Optional[str] = None, save_predictions: bool = True, verbose: bool = False ): """ Offline prediction from single RHD file with optional event comparison. Args: root_dir: Directory containing trained model/metadata file_path: Path to .rhd file to evaluate label: Model label/tag (e.g., '128ch') window_ms: Override window size (else from metadata) step_ms: Override step size (else from metadata) events_file: Optional path to events file for evaluation save_predictions: Save predictions and evaluation to files verbose: Enable verbose logging Returns: dict with keys: y_pred, window_starts_sec, starts_samples, eval_results (if events_file) """ from intan.io import ( load_rhd_file, lock_params_to_meta, load_metadata_json, select_training_channels_by_name, ) from intan.processing import EMGPreprocessor from intan.ml import ModelManager, EMGClassifier, evaluate_against_events # Configure logging lvl = logging.DEBUG if verbose else logging.INFO logging.basicConfig(format='[%(levelname)s] %(message)s', level=lvl) # Load metadata meta = load_metadata_json(root_dir, label=label) window_ms, step_ms, _, env_cut = lock_params_to_meta( meta.get('data', {}), window_ms, step_ms, None ) logging.info(f"Training-locked params: window_ms={window_ms} step_ms={step_ms} env_cut={env_cut}") # Load raw EMG from .rhd data = load_rhd_file(file_path, verbose=verbose) emg_fs = data['frequency_parameters']['amplifier_sample_rate'] emg = data["amplifier_data"] raw_channel_names = list(data.get("channel_names", [])) or [f"CH{i}" for i in range(emg.shape[0])] # Timestamps if "t_amplifier" in data and data["t_amplifier"].size: emg_t = data["t_amplifier"] else: emg_t = np.arange(emg.shape[1], dtype=float) / emg_fs dur_s = emg.shape[1] / emg_fs t0 = float(emg_t[0]) logging.info(f"RHD: fs={emg_fs:.1f} Hz, shape={emg.shape}, duration={dur_s:.2f}s, t0={t0:.3f}s") # Reorder channels to match training if "channel_names" not in meta.get("data", {}): raise RuntimeError("metadata missing data.channel_names (training channel order).") trained_names = meta["data"]["channel_names"] emg, sel_idx = select_training_channels_by_name(emg, raw_channel_names, trained_names) logging.info(f"Using {len(sel_idx)} channels locked to training order.") # Preprocess + features pre = EMGPreprocessor(fs=emg_fs, envelope_cutoff=env_cut, verbose=verbose) emg_pp = pre.preprocess(emg) X = pre.extract_emg_features( emg_pp, window_ms=window_ms, step_ms=step_ms, progress=verbose, tqdm_kwargs={"desc": "Extracting features", "leave": False} ) # Compute window_starts (sample indices and seconds) step_samples = int((step_ms / 1000.0) * emg_fs) n_windows = X.shape[0] starts_samples = np.arange(n_windows) * step_samples window_starts_sec = (starts_samples / emg_fs) + t0 # Load model and predict manager = ModelManager(root_dir=root_dir, label=label, model_cls=EMGClassifier, config={"verbose": verbose}) manager.load_model() y_pred = manager.predict(X) # Prepare results results = { 'y_pred': y_pred, 'window_starts_sec': window_starts_sec, 'starts_samples': starts_samples, 'window_ms': window_ms, 'step_ms': step_ms, } # Save predictions if requested if save_predictions: output_dir = os.path.join(root_dir, "predictions") os.makedirs(output_dir, exist_ok=True) basename = os.path.splitext(os.path.basename(file_path))[0] pred_file = os.path.join(output_dir, f"{basename}_predictions.txt") with open(pred_file, 'w') as f: f.write(f"# Predictions for: {file_path}\n") f.write(f"# Window size: {window_ms} ms, Step size: {step_ms} ms\n") f.write(f"# Format: timestamp(s) | sample_index | prediction\n") f.write("#" + "="*60 + "\n") for t, idx, pred in zip(window_starts_sec, starts_samples, y_pred): f.write(f"{t:.3f}\t{idx}\t{pred}\n") logging.info(f"Predictions saved to: {pred_file}") results['pred_file'] = pred_file # Evaluate against events if provided if events_file: logging.info(f"Evaluating against events in: {events_file}") eval_results = evaluate_against_events( events_file, starts_samples, y_pred, return_metrics=True, return_arrays=True, verbose=True ) if eval_results and save_predictions: # Save evaluation results eval_file = os.path.join(output_dir, f"{basename}_evaluation.json") import json with open(eval_file, 'w') as f: json.dump(eval_results, f, indent=2) logging.info(f"Evaluation results saved to: {eval_file}") results['eval_file'] = eval_file results['eval_results'] = eval_results else: # Quick preview uniq, cnt = np.unique(y_pred, return_counts=True) summary = ", ".join(f"{u}: {c}" for u, c in zip(uniq, cnt)) logging.info(f"Predictions summary: {summary}") return results
[docs] def predict_batch( root_dir: str, rhd_glob: Optional[str] = None, rhd_files: Optional[list] = None, events_dir: Optional[str] = None, label: str = "", window_ms: Optional[int] = None, step_ms: Optional[int] = None, zero_division: int = 0, save_eval: bool = False, verbose: bool = False ): """ Batch prediction across multiple RHD files with aggregated metrics. Args: root_dir: Directory containing trained model/metadata rhd_glob: Glob pattern for RHD files (for example, ``raw/**/*.rhd``) rhd_files: Explicit list of RHD file paths events_dir: Directory containing event files label: Model label/tag window_ms: Override window size step_ms: Override step size zero_division: Value for sklearn zero_division parameter save_eval: Save aggregated results to JSON verbose: Enable verbose logging Returns: dict with aggregated metrics """ import glob from intan.io import ( load_rhd_file, lock_params_to_meta, load_metadata_json, select_training_channels_by_name, ) from intan.processing import EMGPreprocessor from intan.ml import ( evaluate_against_events, classification_report_safe, ModelManager, EMGClassifier, ) logging.basicConfig(format="[%(levelname)s] %(message)s", level=(logging.DEBUG if verbose else logging.INFO)) # Expand file list if rhd_glob: files = sorted(glob.glob(rhd_glob, recursive=True)) else: files = rhd_files or [] if not files: raise FileNotFoundError("No .rhd files matched.") logging.info(f"Found {len(files)} RHD files.") # Load metadata and model once meta = load_metadata_json(root_dir, label=label) window_ms, step_ms, _, env_cut = lock_params_to_meta( meta.get('data', {}), window_ms, step_ms, None ) trained_names = meta["data"]["channel_names"] logging.info(f"Training-locked params: window_ms={window_ms} step_ms={step_ms} env_cut={env_cut}") manager = ModelManager(root_dir=root_dir, label=label, model_cls=EMGClassifier, config={"verbose": verbose}) manager.load_model() y_true_all = [] y_pred_all = [] for i, rhd_path in enumerate(files, 1): # Find event file ev_path = None if events_dir: basename = os.path.splitext(os.path.basename(rhd_path))[0] ev_path = os.path.join(events_dir, basename + ".event") if not os.path.exists(ev_path): stem = basename.split('_')[0] candidates = [f for f in os.listdir(events_dir) if f.startswith(stem) and f.endswith('.event')] if candidates: ev_path = os.path.join(events_dir, candidates[0]) else: ev_path = None if ev_path is None or not os.path.exists(ev_path): logging.warning(f"[{i}/{len(files)}] No event file for: {rhd_path} — skipping") continue # Process file try: data = load_rhd_file(rhd_path, verbose=False) emg = data["amplifier_data"] emg_fs = data['frequency_parameters']['amplifier_sample_rate'] raw_channel_names = list(data.get("channel_names", [])) or [f"CH{i}" for i in range(emg.shape[0])] emg, sel_idx = select_training_channels_by_name(emg, raw_channel_names, trained_names) pre = EMGPreprocessor(fs=emg_fs, envelope_cutoff=env_cut, verbose=False) emg_pp = pre.preprocess(emg) X = pre.extract_emg_features(emg_pp, window_ms=window_ms, step_ms=step_ms, progress=False) step_samples = int((step_ms / 1000.0) * emg_fs) starts_samples = np.arange(X.shape[0]) * step_samples y_pred = manager.predict(X) y_true, y_pred_matched = evaluate_against_events( ev_path, starts_samples, y_pred, verbose=False, return_arrays=True ) y_true_all.extend(y_true) y_pred_all.extend(y_pred_matched) logging.info(f"[{i}/{len(files)}] Processed: {os.path.basename(rhd_path)} ({len(y_true)} windows)") except Exception as e: logging.error(f"[{i}/{len(files)}] Error processing {rhd_path}: {e}") continue # Aggregated metrics if not y_true_all: logging.warning("No valid predictions to aggregate.") return None logging.info(f"\n{'='*60}") logging.info(f"AGGREGATED RESULTS ({len(y_true_all)} total windows)") logging.info(f"{'='*60}") report = classification_report_safe(y_true_all, y_pred_all, zero_division=zero_division) logging.info(f"\n{report}") results = { "n_windows": len(y_true_all), "n_files": len(files), "classification_report": report } if save_eval: import json out_path = os.path.join(root_dir, f"batch_eval_{label}.json") with open(out_path, 'w') as f: json.dump(results, f, indent=2) logging.info(f"Saved evaluation to: {out_path}") results['eval_file'] = out_path return results
[docs] def predict_from_device( root_dir: str, label: str = "", seconds: float = 10.0, event_file: Optional[str] = None, window_ms: Optional[int] = None, step_ms: Optional[int] = None, verbose: bool = False ): """ Record from device and predict gestures (fixed duration). Args: root_dir: Directory containing trained model/metadata label: Model label/tag seconds: Recording duration in seconds event_file: Optional events file for evaluation window_ms: Override window size step_ms: Override step size verbose: Enable verbose logging Returns: dict with predictions and optional evaluation results """ from intan.interface import IntanRHXDevice from intan.io import lock_params_to_meta, load_metadata_json from intan.processing import EMGPreprocessor from intan.ml import ModelManager, EMGClassifier, evaluate_against_events logging.basicConfig(format='[%(levelname)s] %(message)s', level=(logging.DEBUG if verbose else logging.INFO)) # Load metadata meta = load_metadata_json(root_dir, label=label) window_ms, step_ms, _, env_cut = lock_params_to_meta( meta.get('data', {}), window_ms, step_ms, None ) trained_names = meta["data"]["channel_names"] logging.info(f"Training-locked params: window_ms={window_ms} step_ms={step_ms} env_cut={env_cut}") # Record from device logging.info(f"Recording {seconds}s from device...") with IntanRHXDevice(sample_rate=meta["data"].get("emg_fs", 4000), num_channels=len(trained_names)) as device: device.enable_wide_channel(range(len(trained_names))) emg, emg_fs, t0 = device.record(duration_sec=seconds) logging.info(f"Recorded: fs={emg_fs:.1f} Hz, shape={emg.shape}, t0={t0:.3f}s") # Preprocess and extract features pre = EMGPreprocessor(fs=emg_fs, envelope_cutoff=env_cut, verbose=verbose) emg_pp = pre.preprocess(emg) X = pre.extract_emg_features(emg_pp, window_ms=window_ms, step_ms=step_ms, progress=verbose) # Compute window starts step_samples = int((step_ms / 1000.0) * emg_fs) starts_samples = np.arange(X.shape[0]) * step_samples window_starts = (starts_samples / emg_fs) + t0 # Predict manager = ModelManager(root_dir=root_dir, label=label, model_cls=EMGClassifier, config={"verbose": verbose}) manager.load_model() y_pred = manager.predict(X) results = { 'y_pred': y_pred, 'window_starts': window_starts, 'starts_samples': starts_samples } # Optional evaluation if event_file: logging.info(f"Evaluating against events in: {event_file}") eval_results = evaluate_against_events(event_file, starts_samples, y_pred, return_metrics=True) results['eval_results'] = eval_results else: uniq, cnt = np.unique(y_pred, return_counts=True) summary = ", ".join(f"{u}: {c}" for u, c in zip(uniq, cnt)) logging.info(f"Predictions summary: {summary}") return results
[docs] def predict_realtime_stream( root_dir: str, label: str = "", window_ms: Optional[int] = None, step_ms: Optional[int] = None, infer_period_s: Optional[float] = None, smooth_k: int = 1, seconds_total: Optional[float] = None, use_lsl: bool = False, verbose: bool = False ): """ Real-time streaming prediction from device. Args: root_dir: Directory containing trained model/metadata label: Model label/tag window_ms: Override window size step_ms: Override step size infer_period_s: Inference period in seconds smooth_k: Majority vote window size for smoothing seconds_total: Total streaming duration (None = infinite) use_lsl: Enable LSL marker publishing verbose: Enable verbose logging """ from intan.interface import IntanRHXDevice, LSLMarkerPublisher from intan.io import lock_params_to_meta, load_metadata_json from intan.ml import EMGRealTimePredictor import time logging.basicConfig(format='[%(levelname)s] %(message)s', level=(logging.DEBUG if verbose else logging.INFO)) # Load metadata meta = load_metadata_json(root_dir, label=label) window_ms, step_ms, _, env_cut = lock_params_to_meta( meta.get('data', {}), window_ms, step_ms, None ) trained_names = meta["data"]["channel_names"] emg_fs = meta["data"].get("emg_fs", 4000) logging.info(f"Training-locked params: window_ms={window_ms} step_ms={step_ms} env_cut={env_cut}") # Initialize predictor predictor = EMGRealTimePredictor( root_dir=root_dir, label=label, window_ms=window_ms, step_ms=step_ms, env_cut=env_cut, smooth_k=smooth_k, verbose=verbose ) # Optional LSL publisher lsl_pub = None if use_lsl: lsl_pub = LSLMarkerPublisher(stream_name="EMG_Predictions") logging.info("LSL marker stream enabled.") # Stream and predict logging.info("Starting real-time prediction stream...") with IntanRHXDevice(sample_rate=emg_fs, num_channels=len(trained_names)) as device: device.enable_wide_channel(range(len(trained_names))) device.set_run_mode() start_time = time.time() infer_period_s = infer_period_s or (step_ms / 1000.0) try: while True: if seconds_total and (time.time() - start_time) >= seconds_total: break emg_window = device.get_latest_data(duration_ms=window_ms) if emg_window is None: time.sleep(0.01) continue prediction = predictor.predict(emg_window) logging.info(f"Prediction: {prediction}") if lsl_pub: lsl_pub.push_marker(prediction) time.sleep(infer_period_s) except KeyboardInterrupt: logging.info("\nStopped by user.") logging.info("Streaming complete.")