diff --git a/docs/logo.jpg b/docs/logo.jpg new file mode 100644 index 00000000..f301ba4f Binary files /dev/null and b/docs/logo.jpg differ diff --git a/examples/acquisition-example.py b/examples/acquisition-example.py index 21c18a39..5b3cdfd9 100644 --- a/examples/acquisition-example.py +++ b/examples/acquisition-example.py @@ -15,6 +15,14 @@ def my_exg_function(packet): ############# +def my_env_function(packet): + """A function that receives env packets(temperature, light, battery) and does some operations on the data""" + print("Received an environment packet: ", packet) + ############# + # YOUR CODE # + ############# + + def my_orn_function(packet): """A function that receives orientation packets and does some operations on the data""" timestamp, orn_data = packet.get_data() @@ -38,6 +46,7 @@ def main(): # Subscribe your function to the stream publisher exp_device.stream_processor.subscribe(callback=my_exg_function, topic=TOPICS.raw_ExG) exp_device.stream_processor.subscribe(callback=my_orn_function, topic=TOPICS.raw_orn) + exp_device.stream_processor.subscribe(callback=my_env_function, topic=TOPICS.env) try: while True: time.sleep(.5) diff --git a/examples/band_power_plot.py b/examples/band_power_plot.py new file mode 100644 index 00000000..0ffab75d --- /dev/null +++ b/examples/band_power_plot.py @@ -0,0 +1,108 @@ +import time +from collections import deque + +import matplotlib.pyplot as plt +import numpy as np +from matplotlib.animation import FuncAnimation + +import explorepy +from explorepy.packet import EEG +from explorepy.stream_processor import TOPICS + +rows, cols = 8, 1024 +data_buf = deque([deque(maxlen=cols) for _ in range(rows)]) +for row in data_buf: + row.extend(range(1024)) + + +def get_data_buf(): + output = np.array([[0] * 1024 for _ in range(rows)]) + for i in range(len(output)): + output[i] = np.array(data_buf[i]) + return output + + +def on_exg_received(packet: EEG): + _, data = packet.get_data() + for r in range(len(data)): + for column in range(len(data[r])): + data_buf[r].append(data[r][column]) + + +exp_device = explorepy.Explore() +# Subscribe your function to the stream publisher + +exp_device.connect(device_name="Explore_AAAK") +exp_device.stream_processor.subscribe(callback=on_exg_received, topic=TOPICS.raw_ExG) + +# Define the frequency bands +bands = { + 'Delta': (0.5, 4), + 'Theta': (4, 8), + 'Alpha': (8, 13), + 'Beta': (13, 30), + 'Gamma': (30, 50) +} + +# FFT and signal parameters +sampling_rate = 250 # in Hz +num_channels = 8 +num_samples = 1024 + +# Set up the figure and axes for subplots +fig, axs = plt.subplots(4, 2, figsize=(10, 15), sharex=True) +fig.tight_layout(pad=4.0) # Adjust layout padding + +# Flatten the 2D array of axes for easier iteration +axs = axs.flatten() + +# Initialize bar plots for each subplot +bars = [] +for ax in axs: + bars.append(ax.bar(bands.keys(), [0] * len(bands), color='skyblue')) + ax.set_ylim(0, 1) # Set an initial y-limit, can adjust based on your data + ax.set_xlabel('Frequency Band') + ax.set_ylabel('Power') + + +def update(frame): + eeg_signals = get_data_buf() # Generate data for all channels + + for idx, (axis, bar) in enumerate(zip(axs, bars)): + eeg_signal = eeg_signals[idx] + + # Perform FFT + fft_values = np.fft.fft(eeg_signal) + fft_frequencies = np.fft.fftfreq(num_samples, 1 / sampling_rate) + fft_magnitude = np.abs(fft_values) ** 2 # Power spectrum + + # Only consider positive frequencies + positive_frequencies = fft_frequencies[:num_samples // 2] + positive_magnitude = fft_magnitude[:num_samples // 2] + + # Calculate band powers + band_powers = [] + for band, (low, high) in bands.items(): + indices = np.where((positive_frequencies >= low) & (positive_frequencies <= high)) + band_power = np.sum(positive_magnitude[indices]) + band_powers.append(band_power) + + # Update the bar heights + for b, power in zip(bar, band_powers): + b.set_height(power) + + # Optionally adjust y-axis limits based on data + axis.set_ylim(0, max(band_powers) * 1.1) + axis.set_title(f'Channel {idx + 1}') + + return [b for bar in bars for b in bar] # Flatten the list of bars + + +# Create the animation object +ani = FuncAnimation(fig, update, interval=500, blit=True) # Update every 500 ms (0.5 seconds) + +# Display the plot +plt.show() + +while True: + time.sleep(1) diff --git a/examples/eegsynth_demo/MentalabEEGSynth.vcv b/examples/eegsynth_demo/MentalabEEGSynth.vcv new file mode 100644 index 00000000..4d143abd Binary files /dev/null and b/examples/eegsynth_demo/MentalabEEGSynth.vcv differ diff --git a/examples/eegsynth_demo/buffer.ini b/examples/eegsynth_demo/buffer.ini new file mode 100644 index 00000000..aa13d865 --- /dev/null +++ b/examples/eegsynth_demo/buffer.ini @@ -0,0 +1,10 @@ +[general] +debug=2 +delay=0.010 + +[redis] +hostname=localhost +port=6379 + +[fieldtrip] +port=1972,1973,1974 \ No newline at end of file diff --git a/examples/eegsynth_demo/historycontrol.ini b/examples/eegsynth_demo/historycontrol.ini new file mode 100644 index 00000000..62221209 --- /dev/null +++ b/examples/eegsynth_demo/historycontrol.ini @@ -0,0 +1,17 @@ +[general] +debug=2 + +[redis] +hostname=localhost +port=6379 + +[history] +window=10 +; window length for smoothing (s) +stepsize=0.05 +; update time (s) + +[input] +; control values to plot, separated by comma +freeze=0 +channels=spectral.channel1.alpha,spectral.channel2.theta,spectral.channel2.beta \ No newline at end of file diff --git a/examples/eegsynth_demo/lsl2ft.ini b/examples/eegsynth_demo/lsl2ft.ini new file mode 100644 index 00000000..919687ab --- /dev/null +++ b/examples/eegsynth_demo/lsl2ft.ini @@ -0,0 +1,16 @@ +[general] +debug=1 + +[fieldtrip] +hostname=localhost +port=1972 + +[redis] +hostname=localhost +port=6379 + +[lsl] +; this can be used to select the desired stream (in case there are multiple) +name= +type=ExG +timeout=30 ; in seconds diff --git a/examples/eegsynth_demo/outputmidi_loopback.ini b/examples/eegsynth_demo/outputmidi_loopback.ini new file mode 100644 index 00000000..225a9500 --- /dev/null +++ b/examples/eegsynth_demo/outputmidi_loopback.ini @@ -0,0 +1,33 @@ +[general] +debug=1 +delay=0.05 +monophonic=1 ; boolean + +[redis] +hostname=localhost +port=6379 + +[midi] +device=IAC Driver Bus 1 +channel=1 + +[control] +; you can specify different MIDI message types here: controlXXX, noteXXX, polytouchXXX, aftertouch, pitchwheel, start, continue, stop, reset, note + +[trigger] +; you can specify different MIDI message types here: controlXXX, noteXXX, polytouchXXX, aftertouch, pitchwheel, start, continue, stop, reset, note +note=post.channel1.alpha +start=post.channel1.alpha + +[duration] +note=0.5 ; the note will be switched off after the specified time (in seconds) + +[velocity] + +[scale] +; scale and offset can be used to map Redis values to MIDI values between 0 to 127 +; the default scale for all channels is 127 + +[offset] +; the default offset for all channels is 0 +note=40 \ No newline at end of file diff --git a/examples/eegsynth_demo/outputmidi_loopback_2.ini b/examples/eegsynth_demo/outputmidi_loopback_2.ini new file mode 100644 index 00000000..d140b0df --- /dev/null +++ b/examples/eegsynth_demo/outputmidi_loopback_2.ini @@ -0,0 +1,33 @@ +[general] +debug=1 +delay=0.05 +monophonic=1 ; boolean + +[redis] +hostname=localhost +port=6379 + +[midi] +device=IAC Driver Bus 2 +channel=1 + +[control] +; you can specify different MIDI message types here: controlXXX, noteXXX, polytouchXXX, aftertouch, pitchwheel, start, continue, stop, reset, note + +[trigger] +; you can specify different MIDI message types here: controlXXX, noteXXX, polytouchXXX, aftertouch, pitchwheel, start, continue, stop, reset, note +note=post.channel2.thetabetaratio +start=post.channel2.thetabetaratio + +[duration] +note=0.5 ; the note will be switched off after the specified time (in seconds) + +[velocity] + +[scale] +; scale and offset can be used to map Redis values to MIDI values between 0 to 127 +; the default scale for all channels is 127 + +[offset] +; the default offset for all channels is 0 +note=40 diff --git a/examples/eegsynth_demo/postprocessing.ini b/examples/eegsynth_demo/postprocessing.ini new file mode 100644 index 00000000..2d471dc9 --- /dev/null +++ b/examples/eegsynth_demo/postprocessing.ini @@ -0,0 +1,23 @@ +[general] +delay=0.05 +debug=2 + +[redis] +hostname=localhost +port=6379 + +[initial] +; here you can specify the initial values of some control values + +[input] +; the keys here can have an arbitrary name, but should map those in the output section +; the keys must be lower-case. values should not contain an equation, only one-to-one mappings +alpha_1=spectral.channel1.alpha +theta_2=spectral.channel2.theta +beta_2=spectral.channel2.beta + +[output] +; besides +, -, /, *, the equations also support log, log2, log10, exp, power from numpy +; and compress, limit, rescale, normalizerange, normalizestandard from EEGsynth +post.channel1.alpha = limit(alpha_1 / 1750, 0, 0.8) +post.channel2.thetabetaratio = limit(theta_2/beta_2 / 6, 0, 0.9) \ No newline at end of file diff --git a/examples/eegsynth_demo/preprocessing.ini b/examples/eegsynth_demo/preprocessing.ini new file mode 100644 index 00000000..d62453bf --- /dev/null +++ b/examples/eegsynth_demo/preprocessing.ini @@ -0,0 +1,36 @@ +[general] +delay=0.10 +debug=1 + +[redis] +hostname=localhost +port=6379 + +[input_fieldtrip] +hostname=localhost +port=1972 +timeout=30 + +[output_fieldtrip] +hostname=localhost +port=1973 + +[processing] +window=0.12 +;smoothing=0.2 +reference=none +lowpassfilter=45 +highpassfilter=2 +filterorder=511 +;downsample=1 +notchfilter=50 + +[scale] +highpassfilter=1 +lowpassfilter=1 +notchfilter=1 + +[offset] +highpassfilter=0 +lowpassfilter=0 +notchfilter=0 \ No newline at end of file diff --git a/examples/eegsynth_demo/push2lsl.py b/examples/eegsynth_demo/push2lsl.py new file mode 100644 index 00000000..41fd6f44 --- /dev/null +++ b/examples/eegsynth_demo/push2lsl.py @@ -0,0 +1,5 @@ +import explorepy + +expdev = explorepy.Explore() +expdev.connect("Explore_AAAI") +expdev.push2lsl() diff --git a/examples/eegsynth_demo/spectral.ini b/examples/eegsynth_demo/spectral.ini new file mode 100644 index 00000000..26be9855 --- /dev/null +++ b/examples/eegsynth_demo/spectral.ini @@ -0,0 +1,47 @@ +[general] +debug=2 +delay=0.1 + +[redis] +hostname=localhost +port=6379 + +[fieldtrip] +hostname=localhost +port=1973 +timeout=30 + +[input] +; this specifies the channels from the FieldTrip buffer +; the channel names (on the left) can be specified as you like +channel1=1 +channel2=2 +; channel3=3 +; channel4=4 +; channel5=5 +; channel6=6 +; channel7=7 +; channel8=8 + +[processing] +; the sliding window is specified in seconds +window=5.0 + +[scale] +window=1 + +[offset] +window=0 + +[band] +; the frequency bands can be specified as you like, but must be all lower-case +; you should give the lower and upper range of each band +delta=2-5 +theta=5-8 +alpha=8-12 +beta=12-30 +gamma=35-45 + +[output] +; the results will be written to Redis as "spectral.channel1.alpha" etc. +prefix=spectral \ No newline at end of file diff --git a/examples/ssvep_demo/ssvep.py b/examples/ssvep_demo/ssvep.py index 637081c0..0ef11ef9 100644 --- a/examples/ssvep_demo/ssvep.py +++ b/examples/ssvep_demo/ssvep.py @@ -4,6 +4,7 @@ """ import time from threading import Lock +from psychopy_visionscience.radial import RadialStim from psychopy import visual, event import numpy as np from analysis import CCAAnalysis @@ -29,9 +30,9 @@ def __init__(self, window, size, position, n_frame, log_time=False): pattern = np.ones((4, 4)) pattern[::2, ::2] *= -1 pattern[1::2, 1::2] *= -1 - self._stim1 = visual.RadialStim(win=self._window, tex=pattern, pos=position, + self._stim1 = RadialStim(win=self._window, tex=pattern, pos=position, size=size, radialCycles=1, texRes=256, opacity=1) - self._stim2 = visual.RadialStim(win=self._window, tex=pattern*-1, pos=position, + self._stim2 = RadialStim(win=self._window, tex=pattern*-1, pos=position, size=size, radialCycles=1, texRes=256, opacity=1) self._toggle_flag = False self.log_time = log_time diff --git a/examples/wiki-ecg-analysis.py b/examples/wiki-ecg-analysis.py new file mode 100644 index 00000000..b109101d --- /dev/null +++ b/examples/wiki-ecg-analysis.py @@ -0,0 +1,46 @@ +import neurokit2 as nk +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt +import pyedflib + +def edf_to_arr(edf_path): + f = pyedflib.EdfReader(edf_path) + n = f.signals_in_file + signal_labels = f.getSignalLabels() + sigbufs = np.zeros((n, f.getNSamples()[0])) + for i in np.arange(n): + sigbufs[i, :] = f.readSignal(i) + + return sigbufs + +data = edf_to_arr("../data/wiki-ECG-resting_ExG.bdf") # read in bdf file +data = data[1] # channel 1 holds ECG data +data *= 1e-3 # scale uV to mV for ECG analysis in neurokit +ecg_signals, info = nk.ecg_process(data[5000:25000], sampling_rate=250) # convert + +rpeaks = info["ECG_R_Peaks"] +cleaned_ecg = ecg_signals["ECG_Clean"] + +intervalrelated = nk.ecg_intervalrelated(ecg_signals) +intervalrelated.iloc[0,1:83] + +nk.ecg_plot(ecg_signals, info) +fig = plt.gcf() +fig.set_size_inches(20, 12, forward=True) +fig.savefig("../plots/resting_ecg.png") + +ecg_signals, info = nk.ecg_process(data[18000:20000] , sampling_rate=250) # take a subset from the middle of recording and apply neurokit +rpeaks = info["ECG_R_Peaks"] +cleaned_ecg = ecg_signals["ECG_Clean"] +plot = nk.events_plot(rpeaks, cleaned_ecg[0:cleaned_ecg.shape[0]]) +fig = plt.gcf() +fig.set_size_inches(20, 12, forward=True) +fig.savefig("../plots/resting_rpeaks.png") + +peaks, info = nk.ecg_peaks(data[5000:25000], sampling_rate=250) +hrv_time = nk.hrv_time(peaks, sampling_rate=250, show=True) +hrv_time +fig = plt.gcf() +fig.set_size_inches(20, 12, forward=True) +fig.savefig("../plots/sv_ecg_time.png") \ No newline at end of file diff --git a/examples/wiki-sleep-analysis.py b/examples/wiki-sleep-analysis.py new file mode 100644 index 00000000..04b286a0 --- /dev/null +++ b/examples/wiki-sleep-analysis.py @@ -0,0 +1,61 @@ +# Data processing +## Load packages +import mne +import yasa +import numpy as np +import matplotlib.pyplot as plt + +## Load data +ch_names = ['EOG-above', 'EOG-canthus', 'EMG-chin', 'Dry-O2', 'Dry-C2', 'Dry-F2', 'Sticky-C1', 'Wet-Fc1'] +data = np.loadtxt("wiki-sleep-analysis-data/Mentalab-sleep-analysis_ExG.csv", skiprows=1, delimiter=',').transpose()[1:9] +sf = 250 +ch_types = ["eog", "eog", "emg", "eeg", "eeg", "eeg", "eeg", "eeg"] +info = mne.create_info(ch_names = ch_names, sfreq = sf, ch_types = ch_types) +raw = mne.io.RawArray(data, info) +raw.apply_function(lambda x: x * 1e-6) # scale muV to V + +## Pre-processing +raw.filter(0.1, 40) # Apply a bandpass filter from 0.1 to 40 Hz + +# Detecting sleep stages +sls = yasa.SleepStaging(raw, eeg_name="Sticky-C1") + +## Computing predicted labels +y_pred = sls.predict() +y_pred[0:40] + +## Computing the hypnogram +hypno = yasa.Hypnogram(y_pred) +hypno_int = yasa.hypno_str_to_int(y_pred) +hypno_up = yasa.hypno_upsample_to_data(hypno=hypno_int, sf_hypno=(1/30), data=data, sf_data=sf) + +# Inspecting bandpower +data_filt = raw.get_data() * 1e6 +data_c1_uV = data_filt[6] +bandpower_stages = yasa.bandpower(data_c1_uV, sf = 100, win_sec=4, relative=True, hypno=hypno_up, include=(0, 1, 2, 3, 4)) +bandpower_avg = bandpower_stages.groupby('Stage')[['Delta', 'Theta', 'Alpha', 'Sigma', 'Beta', 'Gamma']].mean() +bandpower_avg.index = ['Wake', 'N1', 'N2', 'N3', 'REM'] + +print(bandpower_avg) + +# Spectrogram and hypnogram +print(hypno_up.shape, 'Unique values =', np.unique(hypno_up)) +fig = yasa.plot_spectrogram(data_c1_uV, sf, hypno=hypno_up, fmax=30, cmap='Spectral_r', trimperc=5) +fig.show() + +# Slow wave and sleep spindle detection +## Slow waves +sw = yasa.sw_detect(data_c1_uV, sf, hypno=hypno_up) +sw.summary() + +sw.plot_average(time_before=0.4, time_after=0.8, center="NegPeak"); +plt.legend(['C1']) +plt.show() + +## Sleep spindles +sp = yasa.spindles_detect(data_c1_uV, sf) +sp.summary() + +sp.plot_average(time_before=0.6, time_after=0.6); +plt.legend(['C1']) +plt.show() \ No newline at end of file diff --git a/src/explorepy/_exceptions.py b/src/explorepy/_exceptions.py index 2d8a0cff..9fdefdea 100644 --- a/src/explorepy/_exceptions.py +++ b/src/explorepy/_exceptions.py @@ -40,14 +40,12 @@ class ReconnectionFlowError(Exception): """ pass - class BleDisconnectionError(Exception): """ Reconnection flow error, only thrown when device is reconnecting """ pass - if sys.platform == "darwin": class BluetoothError(Exception): """ diff --git a/src/explorepy/explore.py b/src/explorepy/explore.py index 8757820d..41d11f9c 100644 --- a/src/explorepy/explore.py +++ b/src/explorepy/explore.py @@ -19,6 +19,7 @@ import time from threading import Timer +import explorepy import numpy as np from appdirs import user_cache_dir diff --git a/src/explorepy/exploresdk.py b/src/explorepy/exploresdk.py index 5451bf50..e13c341c 100644 --- a/src/explorepy/exploresdk.py +++ b/src/explorepy/exploresdk.py @@ -5,8 +5,6 @@ # the SWIG interface file instead. from sys import version_info as _swig_python_version_info - - if _swig_python_version_info < (2, 7, 0): raise RuntimeError("Python 2.7 or later required") @@ -64,8 +62,6 @@ class _SwigNonDynamicMeta(type): import collections.abc - - class SwigPyIterator(object): thisown = property(lambda x: x.this.own(), lambda x, v: x.this.own(v), doc="The membership flag") diff --git a/src/explorepy/parser.py b/src/explorepy/parser.py index 4fcda800..48d6a3c2 100644 --- a/src/explorepy/parser.py +++ b/src/explorepy/parser.py @@ -262,7 +262,6 @@ def _parse_packet(self, pid, timestamp, bin_data): raise FletcherError return packet - class FileHandler: """Binary file handler"""