Advanced EEG Processing Nodes
In this tutorial, we'll create more sophisticated Nodes for EEG signal processing. We'll develop a spectral analysis Node that extracts frequency band powers from EEG signals, which is a fundamental feature extraction technique in EEG research.
Frequency Band Analysis in EEG
EEG researchers commonly analyze the power in specific frequency bands, including:
- Delta (0.5-4 Hz): Associated with deep sleep, meditation
- Theta (4-8 Hz): Associated with drowsiness, meditation, creativity
- Alpha (8-13 Hz): Associated with relaxation, closed eyes
- Beta (13-30 Hz): Associated with active thinking, focus, alertness
- Gamma (30+ Hz): Associated with cognitive processing, learning
Let's create a Node that calculates the power in these frequency bands for each EEG channel.
Creating a Spectral Analysis Node
// src/spectral-analysis.ts
import { Node } from '@nexstem/wisdom-js';
import { TimeSeriesPacket } from './types/time-series-packet';
// Define frequency bands
const FREQUENCY_BANDS = {
delta: { min: 0.5, max: 4 },
theta: { min: 4, max: 8 },
alpha: { min: 8, max: 13 },
beta: { min: 13, max: 30 },
gamma: { min: 30, max: 45 },
};
interface SpectralAnalysisConfig {
sampleRate: number; // Sample rate in Hz
windowSize: number; // FFT window size (must be power of 2)
overlap: number; // Window overlap percentage (0-100)
customBands?: {
// Optional custom frequency bands
[name: string]: { min: number; max: number };
};
}
interface BandPower {
bandName: string;
power: number;
}
interface ChannelBandPowers {
channelId: number;
timestamp: number;
bands: BandPower[];
}
interface SpectralPacket {
channels: ChannelBandPowers[];
}
class SpectralAnalysisNode extends Node<SpectralAnalysisConfig> {
private isRunning = false;
private freqBands: { [name: string]: { min: number; max: number } };
private windowSize: number;
private overlap: number;
private hopSize: number;
private hannWindow: Float32Array;
constructor(params: any) {
super({
inputs: { min: 1, max: 1 }, // Requires exactly one input
outputs: { min: 1 }, // At least one output
...params,
});
// Combine default bands with any custom bands
this.freqBands = { ...FREQUENCY_BANDS, ...this.config.customBands };
// Ensure window size is a power of 2 for efficient FFT
this.windowSize = this.nearestPowerOf2(this.config.windowSize);
// Calculate overlap and hop size
this.overlap = this.config.overlap / 100;
this.hopSize = Math.floor(this.windowSize * (1 - this.overlap));
// Create Hann window for better spectral resolution
this.hannWindow = new Float32Array(this.windowSize);
for (let i = 0; i < this.windowSize; i++) {
this.hannWindow[i] =
0.5 * (1 - Math.cos((2 * Math.PI * i) / (this.windowSize - 1)));
}
}
// Helper function to find nearest power of 2
private nearestPowerOf2(n: number): number {
return Math.pow(2, Math.ceil(Math.log2(n)));
}
async onBuild(): Promise<void> {
console.log(
`Spectral Analysis built with window size: ${this.windowSize}, overlap: ${this.config.overlap}%`
);
}
onData(inletNumber: number, packet: any[]): void {
if (!this.isRunning) return;
try {
// Assume the packet contains a TimeSeriesPacket
const timeSeriesData = TimeSeriesPacket.decode(packet[0]);
// Process each channel
const channelBandPowers: ChannelBandPowers[] =
timeSeriesData.channels.map((channel) => {
// Extract values and timestamps
const values = channel.values;
const timestamps = channel.timestamp;
// Calculate band powers for this channel
const bandPowers = this.calculateBandPowers(values);
// Return channel with band powers
return {
channelId: channel.channelId,
timestamp: timestamps[timestamps.length - 1], // Use the latest timestamp
bands: bandPowers,
};
});
// Create spectral packet
const spectralPacket: SpectralPacket = {
channels: channelBandPowers,
};
// Send the spectral data to all outputs
// In a real implementation, you would use Protocol Buffers or similar
this.sendPacketToAllOutputs([
Buffer.from(JSON.stringify(spectralPacket)),
]);
} catch (error) {
console.error('Error processing data:', error);
}
}
private calculateBandPowers(values: number[]): BandPower[] {
// If we don't have enough data for a full window, return empty results
if (values.length < this.windowSize) {
return Object.keys(this.freqBands).map((bandName) => ({
bandName,
power: 0,
}));
}
// Use the most recent segment of data for our window
const segment = values.slice(-this.windowSize);
// Apply Hann window to reduce spectral leakage
const windowedSegment = segment.map(
(value, i) => value * this.hannWindow[i]
);
// Compute FFT
const fft = this.computeFFT(windowedSegment);
// Calculate the frequency resolution
const freqResolution = this.config.sampleRate / this.windowSize;
// Calculate power in each frequency band
const bandPowers = Object.entries(this.freqBands).map(
([bandName, { min, max }]) => {
// Convert frequency range to FFT bin indices
const minBin = Math.floor(min / freqResolution);
const maxBin = Math.ceil(max / freqResolution);
// Sum the power in the frequency band
let bandPower = 0;
for (let bin = minBin; bin <= maxBin && bin < fft.length; bin++) {
bandPower += fft[bin];
}
return {
bandName,
power: bandPower,
};
}
);
return bandPowers;
}
// Simple implementation of FFT magnitude calculation
// In a real application, you would use a more efficient FFT library
private computeFFT(signal: number[]): number[] {
// This is a simplified placeholder - you should use a proper FFT library
// like fft.js, kissfft-js, or dsp.js
// For this example, we'll generate synthetic magnitudes
// proportional to frequency to demonstrate the concept
const fftBins = this.windowSize / 2;
const magnitudes = new Array(fftBins);
// Compute the real FFT magnitude for each bin
for (let i = 0; i < fftBins; i++) {
// In a real implementation, this would be sqrt(re^2 + im^2)
// where re and im are the real and imaginary parts of the FFT output
// For this example, we'll create a synthetic power spectrum
// with peaks in alpha and beta bands for demonstration
const freq = i * (this.config.sampleRate / this.windowSize);
// Synthetic spectrum with 1/f falloff and peaks in alpha and beta
let power = 1 / (freq + 1); // 1/f pink noise background
// Add peak in alpha band (10 Hz)
power += 5 * Math.exp(-Math.pow((freq - 10) / 2, 2));
// Add peak in beta band (20 Hz)
power += 3 * Math.exp(-Math.pow((freq - 20) / 3, 2));
magnitudes[i] = power;
}
return magnitudes;
}
async onStart(): Promise<void> {
this.isRunning = true;
console.log('Spectral Analysis started');
}
async onStop(): Promise<void> {
this.isRunning = false;
console.log('Spectral Analysis stopped');
}
async onShutdown(): Promise<void> {
console.log('Spectral Analysis shutdown');
}
onSignal(signal: string, packet: any[]): void {
console.log(`Signal received: ${signal}`, packet);
}
}
export default SpectralAnalysisNode;
Understanding the Spectral Analysis Node
This Node implements a common EEG analysis technique: spectral analysis to calculate band powers. Let's examine the key components:
Configuration
interface SpectralAnalysisConfig {
sampleRate: number; // Sample rate in Hz
windowSize: number; // FFT window size (must be power of 2)
overlap: number; // Window overlap percentage (0-100)
customBands?: {
// Optional custom frequency bands
[name: string]: { min: number; max: number };
};
}
The configuration allows researchers to:
- Set the sample rate of the incoming EEG data
- Specify the window size for FFT calculation
- Set the overlap between consecutive windows
- Define custom frequency bands beyond the standard ones
Window Function
// Create Hann window for better spectral resolution
this.hannWindow = new Float32Array(this.windowSize);
for (let i = 0; i < this.windowSize; i++) {
this.hannWindow[i] =
0.5 * (1 - Math.cos((2 * Math.PI * i) / (this.windowSize - 1)));
}
We apply a Hann window to the signal before computing the FFT. This is a critical step in spectral analysis to minimize spectral leakage, ensuring more accurate frequency estimation.
Band Power Calculation
private calculateBandPowers(values: number[]): BandPower[] {
// If we don't have enough data for a full window, return empty results
if (values.length < this.windowSize) {
return Object.keys(this.freqBands).map(bandName => ({
bandName,
power: 0
}));
}
// Use the most recent segment of data for our window
const segment = values.slice(-this.windowSize);
// Apply Hann window to reduce spectral leakage
const windowedSegment = segment.map((value, i) => value * this.hannWindow[i]);
// Compute FFT
const fft = this.computeFFT(windowedSegment);
// Calculate the frequency resolution
const freqResolution = this.config.sampleRate / this.windowSize;
// Calculate power in each frequency band
const bandPowers = Object.entries(this.freqBands).map(([bandName, { min, max }]) => {
// Convert frequency range to FFT bin indices
const minBin = Math.floor(min / freqResolution);
const maxBin = Math.ceil(max / freqResolution);
// Sum the power in the frequency band
let bandPower = 0;
for (let bin = minBin; bin <= maxBin && bin < fft.length; bin++) {
bandPower += fft[bin];
}
return {
bandName,
power: bandPower
};
});
return bandPowers;
}
This method:
- Takes a segment of EEG data
- Applies a window function
- Computes the FFT
- Calculates power in each frequency band by summing the FFT bins that fall within each band's frequency range
Using the Spectral Analysis Node for EEG Research
This Node is particularly valuable for EEG researchers because:
-
Objective Quantification: It provides objective measures of brain activity in specific frequency bands.
-
Cross-Subject Comparison: Band powers can be compared across subjects and conditions.
-
Biomarker Extraction: Specific patterns of band powers can serve as biomarkers for cognitive states or neurological conditions.
-
Real-time Monitoring: Researchers can monitor changes in band powers during experiments.
-
Feature Extraction: The band powers can be used as features for machine learning algorithms.
Real-world Applications
Researchers can use this Node for various applications:
- Neurofeedback: Provide real-time feedback based on specific band powers
- Brain-Computer Interfaces: Use band powers as control signals
- Cognitive State Monitoring: Track attentional states, cognitive load, or drowsiness
- Clinical Assessment: Monitor changes in band powers related to neurological conditions
Adding Extra Features
To enhance this Node for research applications, consider adding:
- Normalization: Normalize band powers to make them comparable across channels
- Relative Band Power: Calculate band power relative to total power
- Asymmetry Metrics: Calculate interhemispheric asymmetry between corresponding electrodes
- Statistical Features: Compute statistical properties like peak frequency, band power ratio, etc.
Testing the Spectral Analysis Node
To test this Node, you can connect it to an EEG data source Node and observe the band power outputs. In the Instinct ecosystem, you'd typically create a pipeline like:
[EEG Source] → [High-pass Filter] → [Spectral Analysis] → [Visualization/Output]
Next Steps
In this tutorial, you've learned how to create an advanced EEG processing Node that performs spectral analysis, a cornerstone technique in EEG research. This demonstrates how you can implement sophisticated signal processing algorithms within the Instinct ecosystem.
In the next tutorial, we'll create a sink Node to output processed data for visualization or further analysis.