import subprocess
import sys
import importlib
import os
import re
import utils as utl
# def is_package_installed(package_name):
# '''
# Checks if package installed: tries to import package, if exception thrown return False (signalizes for package to be installed)
# :param package_name: str, package name
# '''
# try:
# importlib.import_module(package_name)
# return True
# except ImportError:
# return False
# def install_requirements_if_needed(requirements_file="requirements.txt"):
# '''
# Checks requirements, installs packages missing.
# :param requirements_file: .txt file,
# '''
# requirements_file=utl.resource_path("requirements.txt")
# if not os.path.exists(requirements_file):
# print(f"{requirements_file} not found.")
# return
# with open(requirements_file, "r") as f:
# lines = [line.strip() for line in f if line.strip() and not line.startswith("#")]
# missing = []
# for line in lines:
# package = re.split(r'[<>=]', line.strip())[0]
# if not is_package_installed(package):
# missing.append(line)
# if missing:
# print("Installing missing packages:", missing)
# try:
# subprocess.check_call([sys.executable, "-m", "pip", "install", *missing])
# print("All missing requirements installed successfully.")
# except subprocess.CalledProcessError as e:
# print("Failed to install some requirements:", e)
# else:
# print("All required packages are already installed.")
# # --- Run before your imports ---
# install_requirements_if_needed()
from datetime import timedelta, datetime
import dearpygui.dearpygui as dpg
from dearpygui_ext import themes as themes
import numpy as np
import DataHandling as DH # type: ignore
import Preprocessing as pre
import Features as ft
import time
import csv
#----------------------------------------GLOBAL-----------------------------
global_bands = ft.bands
electrodes_names, all_event_names = {}, {}
exporting_tags = {'Plots': [], 'Signals': [], 'Tables': []}
cwh, cww = 110, 140
app_state = {"windows": {}}
directories = {
'rec': "C:/Users/OCD/Desktop/DATA/RN",
'events': "C:/Users/OCD/Desktop/LRodrigues/NeoDBS",
'save': ""
}
colors = [
(0, 0, 255, 255), # blue
(255, 0, 0, 255), # red
(0, 255, 0, 255), # green
(255, 165, 0, 255), # orange
(255, 255, 0, 255), # yellow
(211, 211, 211, 204), #light grey
(255, 51, 255, 255), #pink
(102, 0, 204, 255), #purple
(0, 255, 255, 255), #cian
(34, 139, 34, 255), #forest green
]
#-----------------------------------------UTILS----------------------------------------
[docs]def add_band(sender, app_data, user_data):
'''
Internal: Add user-defined band to internal app_state memory dictinary
'''
tab_tag = user_data
old = getBandsByTab(tab_tag)
min = dpg.get_value(f"{tab_tag}_band_min")
max = dpg.get_value(f"{tab_tag}_band_max")
new = {f'Band {min}-{max}': [min,max]}
t_bands = old | new
window_id = extract_window_id(tab_tag)
app_state['windows'][window_id]['Bands'] = t_bands
b = f"{tab_tag}_add_band"
text = 'Current Bands (Hz): \n ___________________\n\n'
for name, value in t_bands.items():
text = text + f"- {name}: {value}\n"
dpg.delete_item(f"{b}_toast",children_only=True)
dpg.add_text(text, parent=f"{b}_toast")
[docs]def reset_bands(sender,app_data,user_data):
'''
Internal: reset to default bands (D,T,A,B,G) to internal app_state memory dictinary
'''
tab_tag = user_data
window_id = extract_window_id(tab_tag)
b = f"{tab_tag}_add_band"
app_state['windows'][window_id]['Bands'] = ft.bands
text = 'Current Bands (Hz): \n ___________________\n\n'
for name, value in ft.bands.items():
text = text + f"- {name}: {value}\n"
dpg.delete_item(f"{b}_toast",children_only=True)
dpg.add_text(text, parent=f"{b}_toast")
[docs]def sanitize_filename(filename):
'''Internal: Removes or replaces invalid characters from filenames.'''
return re.sub(r'[<>:"/\\|?*]', '_', filename) # Replace invalid characters with '_'
[docs]def save_table_to_file(sender, app_data, user_data):
'''Internal: Saves DPG table data to a .csv or .txt file'''
table_id, tag = user_data # Unpack table ID and file type
file_type = 'csv'
# Get table columns (headers)
column_children = dpg.get_item_children(table_id, slot=0)
headers = [dpg.get_item_label(col) for col in column_children] if column_children else []
# Get table rows (data)
rows = []
row_children = dpg.get_item_children(table_id, slot=1)
if row_children:
for row in row_children:
cell_children = dpg.get_item_children(row, slot=1)
if cell_children:
row_data = [dpg.get_value(cell) for cell in cell_children]
rows.append(row_data)
new_title = dpg.get_value(f"{tag}_file_name")
if new_title is not None: filename=new_title
else: filename = dpg.get_item_label(tag)
filename = sanitize_filename(f"{filename}.{file_type}")
if isinstance(app_data,str):
last = app_data[app_data.find(' ')+1:]
if last != 'None':
last = dpg.get_item_label(f"window_{last}")
filename = f"Window {last}//{filename}"
with open(filename, mode="w", newline="") as file:
writer = csv.writer(file)
if headers:
writer.writerow(headers)
writer.writerows(rows)
if not dpg.does_item_exist(f"{sender}_toast"):
with dpg.tooltip(parent=sender,tag=f"{sender}_toast"):
dpg.add_text(f"Saved file: {filename}!")
[docs]def save_band_powers_to_txt(sender, app_data, user_data):
'''
Internal: saves band mean power over day signals
info[channel][band] = {
timestamp: [mean, std],
}
'''
info, filename=user_data
filename = filename + ".txt"
with open(filename, "w") as f:
for channel, band_data in info.items():
bands = list(band_data.keys())
f.write(f"{channel}:\n")
f.write("date," + ",".join(bands) + "\n")
timestamps = []
timestamps.extend([entry[0] for entry in band_data[bands[0]]])
for ts in timestamps:
row = [ts]
for band in bands:
mean_val = ""
for entry in band_data[band]:
if entry[0] == ts:
mean_val = str(entry[1][0]) # only mean power
break
row.append(mean_val)
f.write(",".join(row) + "\n")
f.write("\n") # space between channels
print(f"Saved file: {filename}")
[docs]def add_exporting_tag(window_id,plot=None,signal=None,table=None):
if window_id not in exporting_tags.keys():
exporting_tags[window_id] = {'Plots': [], 'Signals': [], 'Tables': []}
keys = []
if plot: keys.append(['Plots',plot])
if signal: keys.append(['Signals',signal])
if table: keys.append(['Tables',table])
for value in keys:
key, tag = value
exporting_tags[key].append(tag)
exporting_tags[window_id][key].append(tag)
[docs]def filename_window(sender,app_data,user_data):
label = dpg.get_item_label(sender)
if 'Table' in label:
#get collapsing header label
plot_tag = dpg.get_item_parent(sender)
else:
plot_tag= user_data[1]
title = dpg.get_item_label(plot_tag)
x, y = dpg.get_item_rect_min(sender)
window_pos = (x - 10, y + 10)
final_userdata = user_data
parent_sender = sender
if dpg.does_item_exist(f"{plot_tag}_file"):
dpg.delete_item(f"{plot_tag}_file")
def handle_submit(sender,app_data,user_data):
label = dpg.get_item_label(parent_sender)
if dpg.does_item_exist(f"{plot_tag}_file"):
dpg.configure_item(f"{plot_tag}_file",show=False)
if 'Plot' in label:
save_plot(parent_sender,app_data,final_userdata)
elif 'Save Table' == label:
other, _ = final_userdata
save_table_to_file(parent_sender,app_data,(other,plot_tag))
else:
file_name = dpg.get_value(f"{plot_tag}_file_name")
if not isinstance(file_name,str):
file_name = dpg.get_item_label(plot_tag)
ud = final_userdata + (file_name,)
save_signals(parent_sender,app_data,ud)
if app_data and 'All' in app_data:
handle_submit(None,app_data,None)
return
with dpg.window(label='File Name',tag=f"{plot_tag}_file", width = 200,pos=window_pos,no_collapse=True,modal=True):
dpg.add_text("Save file as: ")
dpg.add_input_text(default_value=title, width=150,tag=f"{plot_tag}_file_name")
if 'plot' in label.lower():
with dpg.group(horizontal=True):
dpg.add_text("DPI: ")
dpg.add_input_int(default_value=300,min_value=300,max_value=600,min_clamped=True,max_clamped=True,step=50,width=150,tag=f"{plot_tag}_dpi")
dpg.add_button(label="Submit", callback=handle_submit)
[docs]def save_plot(sender,app_data,user_data):
'''
Prepares data from selected plot to be exported into png file.
plot_type, plot_tag, y_axis, colormap_int = user_data
colomap_int:
- int: for regular plots
- rgba array: timeline (specific colors)
- string: 3D (name of colormap)
'''
plot_type, plot_tag, y_axis, colormap_int = user_data
tags = dpg.get_item_children(y_axis)[1]
mask = [dpg.get_item_configuration(tag)['show'] for tag in tags]
if isinstance(colormap_int,int): #follow colormap order
colors = [dpg.get_colormap_color(colormap_int,i) for i in range(len(tags))]
elif isinstance(colormap_int,str):
colors = colormap_int
elif sum(colormap_int[0])>4: #specified colors
colors = [tuple(np.round(np.array(c)/255,3)) for c in colormap_int]
else: colors = colormap_int
if not isinstance(colormap_int,str) and plot_type!='SMP':
colors = np.array(colors)[mask].tolist()
if plot_type not in ['3D','3DC','SMP']:
tags = np.array(tags)[mask].tolist()
title = dpg.get_item_label(plot_tag)
axis_tags = dpg.get_item_children(plot_tag)[1]
axis = [dpg.get_item_label(ax) for ax in axis_tags]
limits = [dpg.get_axis_limits(ax) for ax in axis_tags]
size = dpg.get_item_width(plot_tag), dpg.get_item_height(plot_tag)
line_type = [dpg.get_item_info(tag)['type'] for tag in tags]
labels = [dpg.get_item_label(tag) for tag in tags]
if plot_type in ['3D','3DC']:
# for '3D', '3DC' == coherogram
children = dpg.get_item_children(plot_tag)[1]
#if its in subplots
colormap, subplots = children[0], children[1]
title = dpg.get_item_label(subplots)
line_type = None
if plot_type == '3D':
subplots = dpg.get_item_children(subplots)[1]
else:
subplots=[subplots]
labels = [dpg.get_item_label(plot) for plot in subplots]
axs = [dpg.get_item_children(plot)[1] for plot in subplots]
axis = [dpg.get_item_label(ax) for ax in axs[-1]]
axis.append(dpg.get_item_label(colormap))
size = dpg.get_item_width(subplots[-1]), dpg.get_item_height(subplots[-1])
limits = [[dpg.get_axis_limits(ax) for ax in plot_axis] for plot_axis in axs]
tags = [dpg.get_item_children(ax[1])[1][0] for ax in axs]
signals = []
for _,tag in enumerate(tags):
values = dpg.get_value(tag)
config = dpg.get_item_configuration(tag)
density = [config.get('scale_min'),config.get('scale_max')]
limits[_].append(density)
rows = int(config.get('rows'))
cols = int(config.get('cols'))
sxx = values[0]
# sxx = np.flipud(np.array(sxx)).reshape(rows,cols)
sxx = np.flipud(np.array(sxx).reshape(rows,cols))
mins = config.get('bounds_min')
maxs = config.get('bounds_max')
xlim = [mins[0],maxs[0]]
ylim = [mins[1],maxs[1]]
freqs = np.linspace(*ylim,rows)
time = np.linspace(*xlim,cols)
signals.append([freqs,time,sxx])
elif plot_type == 'SMP':
extra_tags = []
title = dpg.get_item_label(plot_tag)
axis_tags = dpg.get_item_children(plot_tag)[1]
tags = np.array(tags)[mask].tolist()
axis = [dpg.get_item_label(tag) for tag in axis_tags]
limits = [dpg.get_axis_limits(tag) for tag in axis_tags]
size = dpg.get_item_width(plot_tag), dpg.get_item_height(plot_tag)
signals = {'Y1': [], 'Y2':[]}
labels = {'Y1': [], 'Y2':[]}
line_type = {'Y1': [], 'Y2':[]}
if axis[-1] != '':
extra_tags = dpg.get_item_children(axis_tags[-1])[1]
mask2 = [dpg.get_item_configuration(tag)['show'] for tag in extra_tags]
tags = np.array(tags).tolist()
mask = mask + mask2
colors = np.array(colors)[mask].tolist()
for _, tag in enumerate(tags):
x,y = dpg.get_value(tag)[:2]
labels['Y1'].append(dpg.get_item_label(tag))
line = dpg.get_item_info(tag)['type']
line_type['Y1'].append(line)
signals['Y1'].append({'X': x, 'Y': y})
for _, tag in enumerate(extra_tags):
x,y = dpg.get_value(tag)[:2]
labels['Y2'].append(dpg.get_item_label(tag))
line_type['Y2'].append(dpg.get_item_info(tag)['type'])
signals['Y2'].append({'X': x, 'Y': y[:len(x)]})
#when line series are added (ex.: y-bocs) axis tags are in following order: x1,y1,x2,y2
elif plot_type in ['2D','CC']:
# for '2D'
signals = []
for i,tag in enumerate(tags):
values = dpg.get_value(tag)
if 'Std. Dev' in labels[i]:
colors[i][-1]=0.5
x,y1,y2 = values[:3]
sign = {'X':x,'Y1':y1,'Y2':y2}
elif 'InfLine' in line_type[i]:
sign = {'X': values[0]}
else:
x,y = values[:2]
sign = {'X':x,'Y':y}
if any(keyword in title for keyword in ("Timeline", "Circadian", "Stimulation", "Event-locked")):
sign['X'] = [pre.get_date_from_ts(x) for x in sign['X']]
signals.append(sign)
elif plot_type in ['Bar','Events']:
if plot_type == 'Events':
labels = [dpg.get_item_label(tag) for tag in tags]
signals = []
for tag in tags:
x,y = dpg.get_value(tag)[:2]
x = [pre.get_date_from_ts(t) for t in x]
signals.append({'X':x,'Y':y})
else:
values = [dpg.get_value(tag)[:2] for tag in tags]
signals = [{'X': v[0], 'Y': v[1]} for v in values]
ticks = dpg.get_item_user_data(axis_tags[0])
if ticks is not None:
ticks = np.array(dpg.get_item_user_data(axis_tags[0]))
ticks_pos = [float(t) for t in ticks[:,1]]
ticks = ticks[:,0]
axis.append([ticks_pos, ticks.tolist()])
elif plot_type == 'Pie':
tag = tags[0]
labels = dpg.get_item_configuration(tag)['labels']
signals = dpg.get_value(tag)[0]
colors = [dpg.get_colormap_color(colormap_int,i) for i in range(len(signals))]
new_title = dpg.get_value(f"{plot_tag}_file_name")
if new_title is not None: title=new_title
if directories['save'] != "":
title = directories['save'] + f"\\{title}"
if isinstance(app_data,str):
last = app_data[app_data.find(' ')+1:]
if last != 'None':
last = dpg.get_item_label(f"window_{last}")
title = f"Window {last}//{title}"
info = signals,labels,colors,title,axis,limits
#signals, labels, colors, title, axis = info
dpi = dpg.get_value(f"{plot_tag}_dpi")
if dpi is None:
dpi = 600
ft.export_plot(info,plot_type,size=size,line_types=line_type,dpi=dpi)
dpg.bind_item_theme(sender,change_button_color(-1))
if not dpg.does_item_exist(f"{sender}_toast"):
with dpg.tooltip(parent=sender,tag=f"{sender}_toast"):
dpg.add_text(f"Saved plot: {title}.png!")
[docs]def save_signals(sender,app_data,user_data):
'''
Internal: Prepares data from selected plot to save signals into .csv file
parent, plot, plot_type = user_data
'''
print(user_data)
parent, plot, plot_type, file_name = user_data
children = dpg.get_item_children(parent)[1]
tfd = False
match plot_type:
case 'Pie':
tag = children[0]
labels = dpg.get_item_configuration(tag)['labels']
signals = dpg.get_value(tag)[0]
info = {labels[i]: [signals[i]] for i in range(len(signals))}
case 'Events':
labels = [dpg.get_item_label(tag) for tag in children]
signals = [dpg.get_value(tag)[:2] for tag in children]
info = {label: [] for label in labels}
for idx,signal in enumerate(signals):
x,y = signal
x = sorted([pre.get_date_from_ts(s) for s in x])
info[labels[idx]] = x
case 'Timeline':
children = [c for c in children if dpg.get_item_configuration(c)['show'] == True]
labels = [dpg.get_item_label(tag) for tag in children]
signals = [dpg.get_value(tag)[:2] for tag in children]
timeline = [t for s in signals for t in s[0]]
timeline = sorted(np.unique(timeline).tolist())
temp = ['']*len(timeline)
timeline_str = [pre.get_date_from_ts(t) for t in timeline]
info = {'X': timeline_str, 'Left': temp.copy(), 'Right': temp.copy(), 'Events': temp.copy()}
for idx,signal in enumerate(signals):
x,y = signal
if 'LineSeries' not in dpg.get_item_info(children[idx])['type']:
info['Events'][timeline.index(x[0])] = labels[idx]
continue
if x[-1]<x[0]:
x, y = x[::-1], y[::-1]
beg, end = timeline.index(x[0]), timeline.index(x[-1])
if 'left' in labels[idx].lower():
info['Left'][beg:end] = y
else: info['Right'][beg:end] = y
if info['Left'] == info['Right'] == temp:
info.pop('Left')
info.pop('Right')
case '2D':
#only save signals shown in plot
children = [c for c in children if dpg.get_item_configuration(c)['show'] == True]
labels = [dpg.get_item_label(tag) for tag in children]
if ('Timeline' not in file_name) and ('Circadian' not in file_name):
for i,l in enumerate(labels): #Specifically for Survey Peaks
if l in ['', ' ']: labels[i] = f"{labels[i-1]} Peak"
signals = [dpg.get_value(tag)[1] for tag in children]
if labels[-2] in ['Std. Dev','Std. Dev']: #get the actual standard deviation, not the min/max
signals[-2] = list(np.array(signals[-1]) - np.array(signals[-2]))
mini,maxi,maxX = get_limits(children,plot)
cut = maxX[mini:maxi]
if 'Timeline' in file_name:
cut = [pre.get_date_from_ts(c) for c in cut]
elif 'Circadian' in file_name:
cut = [str(utl.extract_time(pre.get_date_from_ts(c))) for c in cut]
info = {'X': cut}
more = {labels[i]: signals[i][mini:maxi] for i in range(len(signals))}
info = info | more
case 'CC':
children = [c for c in children if dpg.get_item_configuration(c)['show'] == True]
labels = [dpg.get_item_label(tag) for tag in children]
types = [dpg.get_item_info(tag)['type'] for tag in children]
signals = []
mini,maxi,maxX = get_limits(children,plot)
for i,tag in enumerate(children):
if 'Half Max' == labels[i]:
signals.append([dpg.get_value(tag)[0][0]]*len(maxX))
elif ('Scatter' in types[i]) or ('Half Max Width' == labels[i]):
x,_ = dpg.get_value(tag)[:2]
if 'Half Max Width' == labels[i]:
x = np.round(np.array(x),3).tolist()
new_y = [''] * len(maxX)
for xx in x:
if xx in maxX:
new_y[maxX.index(xx)] = 0
else:
temp = np.abs(np.array(maxX)-xx).tolist()
idx = min(temp)
if idx < 1/250:
new_y[temp.index(idx)] = 0
signals.append(new_y)
else:
signals.append(dpg.get_value(tag)[1])
info, more = {'X': maxX}, {label: signals[i] for i, label in enumerate(labels)}
info = info | more
case 'PAC':
signal = dpg.get_value(children[0])[:2]
axis = dpg.get_item_children(plot)[1]
labels = [dpg.get_item_label(ax) for ax in axis]
info = {labels[i]: signal[i] for i in range(len(axis))}
case 'Stimulation':
x,stim = dpg.get_value(children[0])[:2]
mini,maxi,_=get_limits(children,plot)
x,stim = x[mini:maxi],stim[mini:maxi]
new_x,new_s = [],[]
for i in range(1,len(x)):
if i==len(x): continue
if stim[i-1] ==stim[i]:continue
new_x.append(str(pre.get_date_from_ts(x[i-1])))
new_s.append(stim[i-1])
new_x.append(str(pre.get_date_from_ts(x[-1])))
new_s.append(stim[-1])
info = {'X':new_x,'Stimulation (mA)': new_s}
case '3D' | '3DC':
# group = plot
# subplots = parent
file_name = dpg.get_item_label(parent)
tfd = True
plots = children
if plot_type=='3DC':
plots = [parent]
y_axis = [dpg.get_item_children(plot)[1][1] for plot in plots]
signals = [dpg.get_item_children(y)[1][0] for y in y_axis]
plot_title = [dpg.get_item_label(plot) for plot in plots]
info = []
for i,signal in enumerate(signals):
values = dpg.get_value(signal)
config = dpg.get_item_configuration(signal)
rows = int(config.get('rows'))
cols = int(config.get('cols'))
sxx = values[0]
sxx = np.array(sxx).reshape(rows,cols)
mins = config.get('bounds_min')
maxs = config.get('bounds_max')
xlim = [mins[0],maxs[0]]
ylim = [mins[1],maxs[1]]
freqs = np.linspace(*ylim,rows).tolist()
time = np.linspace(*xlim,cols).tolist()
info.append({plot_title[i]: [freqs[::-1],time,sxx]})
if isinstance(app_data,str):
last = app_data[app_data.find(' ')+1:]
if last != 'None':
last = dpg.get_item_label(f"window_{last}")
file_name = f"Window {last}//{file_name}"
if directories['save'] != '':
file_name = directories['save'] + '\\' + file_name
ft.export_signals(info,file_name,tfd=tfd)
dpg.bind_item_theme(sender,change_button_color(-1))
if not dpg.does_item_exist(f"{sender}_toast"):
with dpg.tooltip(parent=sender,tag=f"{sender}_toast"):
dpg.add_text(f"Saved file: {file_name}.csv!")
[docs]def save_all(sender,app_data,user_data):
key, window_id = user_data
if window_id:
buttons = exporting_tags[window_id][key]
else:
buttons = exporting_tags[key]
for button in buttons:
if not dpg.does_item_exist(button): continue
callback = dpg.get_item_callback(button)
new_user_data = dpg.get_item_user_data(button)
if callback:
ad = 'All ' + str(window_id)
callback(button,ad,new_user_data)
[docs]def set_directory(sender,app_data,user_data):
if isinstance(app_data['selections'], dict): # Check if files were selected
try:
new = list(app_data['selections'].values())[0]
except Exception: return
repeated = str(new[::-1]).find("\\")
directories[user_data] = new[:-(repeated+1)]
[docs]def change_directory(sender,app_data,user_data):
label = dpg.get_item_label(sender)
dpg.add_file_dialog(directory_selector=True, show=True, width=700, height=400,callback=set_directory,label=label,user_data=user_data)
#--- Add common buttons
[docs]def add_combo_filters(sender, app_data, user_data):
'''
Internal: Callback to show custom filters
:param sender: sender id/alias
:param app_data: value of sender in moment of callback
:param user_data: data saved to callback
'''
tab_tag,signal_tags = user_data
filters = ['butter','cheby1','cheby2','ellip',
'bessel','iirnotch','iirpeak']
dpg.add_spacer(parent=f"{tab_tag}_filters")
with dpg.group(horizontal=True,parent=f"{tab_tag}_filters"):
dpg.add_combo(label='IIR Filters',callback = CustomFilter, user_data=(tab_tag,signal_tags),items = filters, parent=f"{tab_tag}_filters", tag = f"{tab_tag}_combo_filters",width=200)
[docs]def add_combo_features(tab_tag,signal_tags):
'''
Internal: adds custom features option to tab
:param tab_tag: (int or str) alias of tab
:param signal_tags: list of signals ploted in main plot of tab
'''
features = ['periodogram','welch','spectrogram', 'dpss', 'wavelet', 'coherence', 'multitaper coherence','cross-correlation','PAC']
dpg.add_spacer(parent=f"{tab_tag}_features")
with dpg.group(parent=f"{tab_tag}_features", tag = f"{tab_tag}_custom_features"):
dpg.add_combo(label='Customizable Signal Analysis',callback = CustomFeatures, user_data=(tab_tag,signal_tags),items = features, parent=f"{tab_tag}_custom_features", tag = f"{tab_tag}_combo_features",width=200)
[docs]def add_combo_df_features(tab_tag,signal_tags):
'''
Internal: adds default features option to tab
:param tab_tag: (int or str) alias of tab
:param signal_tags: list of signals ploted in main plot of tab
'''
features = ['periodogram','welch','spectrogram', 'dpss', 'wavelet', 'coherence', 'multitaper coherence', 'cross-correlation', 'PAC'] #,'multitaper'
dpg.add_spacer(parent=f"{tab_tag}_features")
with dpg.group(parent=f"{tab_tag}_features", tag = f"{tab_tag}_df_features"):
dpg.add_combo(label='Default Signal Analysis',callback = DefaultFeatures, user_data=(tab_tag,signal_tags),items = features, parent=f"{tab_tag}_df_features", tag = f"{tab_tag}_combo_df_features",width=200)
#--- Toggle buttons
[docs]def deviation(sender, app_data, user_data):
'''
Internal: Recalculates and plots user-defined data
:param sender: sender id/alias
:param app_data: choosen deviation, via radio button
:param user_data: before, tab_label (objects)
before: value of sender, before. if equals: just return
tab_label: id/alias of tab
'''
before, tab_label = user_data
if app_data==before: return
if not dpg.does_item_exist(f"{tab_label}_mean"): return
media = np.array(dpg.get_value(f"{tab_label}_mean")[1])
std_dev = np.array(dpg.get_value(f"{tab_label}_std")[1])
if app_data == '95% C.I.':
dev = (std_dev - media) * -2
else:
dev = (std_dev - media) * -0.5
minus = media - dev
plus = media + dev
dpg.configure_item(f"{tab_label}_std", y1=minus.tolist(), y2=plus.tolist())
dpg.configure_item(sender,user_data=(app_data,tab_label))
[docs]def toggleVisibility(signal_tags, parent, group, user_data):
'''
Internal: creates checkbox for each plotted tag (in signal_tags). Callback defines tag/plot visibility.
:param signal_tags: list of tags, identifiers of each added plot series
:param parent: parent of tags (y axis)
:param group: group that encapsulates plot
:param user_data: window_id
'''
# Create a group for visibility controls
window_id = user_data
tab_tag = parent
def toggle_all(sender, app_data):
for tag, checkbox in checkboxes.items():
dpg.set_value(checkbox, app_data)
dpg.configure_item(tag, show=app_data)
if dpg.does_item_exist(f"{tag}_peak"):
dpg.configure_item(f"{tag}_peak",show=app_data)
if dpg.does_item_exist(f"{tab_tag}_std"):
dpg.configure_item(f"{tab_tag}_std", show=app_data)
dpg.configure_item(f"{tab_tag}_mean", show=app_data)
if dpg.does_item_exist(f"{tab_tag}_mean") and app_data: #only in events for now
mean_power, minus, plus = getMeanActiveSeries(window_id,tab_tag,signal_tags)
dpg.configure_item(f"{tab_tag}_std", y1=minus,y2=plus)
current = dpg.get_value(f"{tab_tag}_mean")
dpg.set_value(f"{tab_tag}_mean", [current[0], mean_power])
def toggle_visibility(sender, app_data, user_data):
if dpg.does_item_exist(f"{tab_tag}_hide"):
if dpg.get_value(f"{tab_tag}_hide"):
hide(sender, False, (signal_tags, [None]))
dpg.set_value(f"{tab_tag}_hide",False)
dpg.configure_item(user_data, show=app_data)
item_id = f"{parent}_select_all"
if dpg.does_item_exist(item_id): # Check if the item exists
dpg.set_value(item_id, False)
else:
pass
mode = extract_mode(tab_tag)
if mode == 'Events':
mean_power, minus, plus = getMeanActiveSeries(window_id,tab_tag,signal_tags)
if isinstance(mean_power,(int,float,np.float64)):
dpg.configure_item(f"{tab_tag}_mean",show=False)
dpg.configure_item(f"{tab_tag}_std",show=False)
return
else:
dpg.configure_item(f"{tab_tag}_mean",show=True)
dpg.configure_item(f"{tab_tag}_std",show=True)
current = dpg.get_value(f"{tab_tag}_mean")
dpg.set_value(f"{tab_tag}_mean", [current[0], mean_power])
dpg.configure_item(f"{tab_tag}_std", y1=minus,y2=plus)
with dpg.group(parent=group, tag = f"{tab_tag}_tav"):
dpg.add_text("Choose signals:")
checkboxes = {}
dpg.add_checkbox(label="Select All", default_value=True, callback=toggle_all, tag = f"{tab_tag}_all")
with dpg.child_window(width=-1, height=cwh, border=True):
# Create checkboxes for each plot
for series_tag, visible in signal_tags.items():
series_config = dpg.get_item_configuration(series_tag)
try:
label = series_config.get('label', series_tag.split('_')[-1])
except:
label = series_config.get('label')
checkbox = dpg.add_checkbox(label=label, default_value=visible,
callback=toggle_visibility, user_data=series_tag)
checkboxes[series_tag] = checkbox
[docs]def toggleType(signal_tags, parent, user_data):
'''
Internal: toggles visibility according to the
:param signal_tags: Description
:param parent: Description
:param user_data: Description
'''
type_filter, window_id, tab_tag = user_data
def toggle_filtered_series(sender, app_data, user_data):
if dpg.does_item_exist(f"{tab_tag}_hide"):
if dpg.get_value(f"{tab_tag}_hide"):
hide(sender, False, (signal_tags, [None]))
dpg.set_value(f"{tab_tag}_hide",False)
window_id = extract_window_id(tab_tag)
key = 'Y'
toggle_label = dpg.get_item_label(sender)
for series_tag in signal_tags:
series_label = str(dpg.get_item_label(series_tag))
if toggle_label == 'CT': toggle_label = 'CT_A'
if toggle_label.lower() in series_label.lower(): # Check if the label contains the type
dpg.configure_item(series_tag, show=app_data)
if 'psd' in tab_tag:
key = 'LFPMagnitude'
if dpg.does_item_exist(f"{series_tag}_peak"): dpg.configure_item(f"{series_tag}_peak", show=app_data)
if dpg.does_item_exist(f"{tab_tag}_mean"):
mean_power, minus, plus = getMeanActiveSeries(window_id,tab_tag,signal_tags,key)
if isinstance(mean_power,(int,float,np.float64)):
dpg.configure_item(f"{tab_tag}_mean",show=False)
dpg.configure_item(f"{tab_tag}_std",show=False)
return
else:
dpg.configure_item(f"{tab_tag}_mean",show=True)
dpg.configure_item(f"{tab_tag}_std",show=True)
current = dpg.get_value(f"{tab_tag}_mean")
dpg.configure_item(f"{tab_tag}_std", y1=minus,y2=plus)
dpg.set_value(f"{tab_tag}_mean", [current[0], mean_power])
dpg.add_checkbox(label=type_filter, default_value=True, callback=toggle_filtered_series,tag = f"{tab_tag}_{type_filter}_checkbox", parent=parent)
[docs]def toggleDisplay(signals, signal_tags, window_id, tab_tag,plot_tag=None,tables=[],dates=None,colors=None):
'''
Documentation
'''
days, hours, artifacts = [], [], [] #by date
normal = True
mode = extract_mode(tab_tag)
if ('artifacts' not in tab_tag.lower()) and ('psd' not in tab_tag.lower()):
# signals = getActiveSignals(tab_tag,signal_tags)
for signal in signals:
date = signal['Date']
hour = f"{date.hour:02d}:{date.minute:02d}"
hours.append(hour)
day = f"{date.year}-{date.month:02d}-{date.day:02d}"
days.append(day)
elif 'artifacts' in tab_tag.lower():
artifacts = [extract_artifact_type(signal['Artifact']) for signal in signals]
else:
normal = False
#(['Hemisphere', 'SensingElectrodes', 'ArtifactStatus', 'LFPFrequency', 'LFPMagnitude'])
colormap = 0
if colors:
colormap = colors
channels = []
sides = ['Left','Right']
if normal:
days = sorted(np.unique(days).tolist())
hours = np.unique(hours).tolist()
for signal in signals: # by channel
# channels.append(extract_channel_from_series_tag(signal['Channel']).upper())
ch = signal['Channel']
if ch in electrodes_names.keys():
ch = electrodes_names[ch]
else:
electrodes_names[ch] = ch
channels.append(extract_channel_from_series_tag(ch))
channels = np.unique(channels).tolist()
channels = [ch.upper() for ch in channels]
types = [hours,days,channels,sides]
label_types=['By Recording:','By Day:','By Channel:','By Hemisphere:']
if len(artifacts)>0:
label_types.append('By Artifact:')
artifacts = np.unique(artifacts).tolist()
types.append(artifacts)
else:
types, label_types = [], []
if dates:
dates = sorted(np.unique(dates).tolist())
types = [dates]
label_types = ['By Day:']
for signal in signals:
channels.append(utl.after_point(signal['SensingElectrodes']))
channels = np.unique(channels).tolist()
types = types + [channels,sides]
label_types= label_types + ['By Channel:','By Hemisphere:']
all_toggles = {}
for l, array in zip(label_types,types):
if len(array)>1:
all_toggles[l] = array
with dpg.group(tag=f"{tab_tag}_buttons"):
# Ensure toggleVisibility is inside a vertical group
with dpg.group(tag = f"{tab_tag}_all_streams"):
toggleVisibility(signal_tags, tab_tag, group=f"{tab_tag}_all_streams", user_data=window_id)
with dpg.group(horizontal=True, parent=f"{tab_tag}_buttons", tag=f"{tab_tag}_types"):
no_groups = len(list(all_toggles.keys()))
if no_groups<4:
for text, values in all_toggles.items():
with dpg.child_window(tag=f"{tab_tag}_{text}_buttons",height=cwh,width=cww,horizontal_scrollbar=True):
dpg.add_text(text)
for v in values:
toggleType(signal_tags,parent=f"{tab_tag}_{text}_buttons", user_data=(v, window_id, tab_tag))
if ('channel' in text.lower()) or ('artifact' in text.lower()):
dpg.configure_item(f"{tab_tag}_{text}_buttons",width=cww,horizontal_scrollbar=True)
else:
g1 = dpg.add_group(parent=f"{tab_tag}_buttons")
g2 = dpg.add_group(parent=f"{tab_tag}_buttons")
for i in range(4):
if i<2: parent=g1
else: parent=g2
text, values = label_types[i], types[i]
with dpg.child_window(tag=f"{tab_tag}_{text}_buttons", height=cwh,width=cww,horizontal_scrollbar=True,parent=parent):
dpg.add_text(text)
for v in values:
toggleType(signal_tags,parent=f"{tab_tag}_{text}_buttons", user_data=(v, window_id, tab_tag))
if ('channel' in text.lower()) or ('artifact' in text.lower()):
dpg.configure_item(f"{tab_tag}_{text}_buttons",width=cww,horizontal_scrollbar=True)
if len(artifacts)==0 and normal:
segment_signal(tab_tag, signal_tags,plot_tag)
else:
with dpg.group(horizontal=True):
dpg.add_checkbox(label='Mean Visible', default_value=True, callback=show_mean, parent=f"{tab_tag}_buttons", tag=f"{tab_tag}_mean_visibility",user_data=tab_tag)
dpg.add_checkbox(label='Mean Only', default_value=False,callback=hide,parent=f"{tab_tag}_buttons",user_data=(signal_tags,[None]), tag=f"{tab_tag}_hide")
dpg.add_checkbox(label='Change to dB',callback=Volt2dB, user_data=(f"y_axis_{tab_tag}", tab_tag, signal_tags, tables),parent=f"{tab_tag}_buttons",default_value=False)
dpg.add_separator()
dpg.add_text('Shaded Area')
dpg.add_radio_button(['Std. Deviation', '95% C.I.'], horizontal=True, callback=deviation, tag = f"{tab_tag}_deviation",user_data=('Standard Deviation',tab_tag))
with dpg.group():
dpg.add_separator(label='Export')
b1 = dpg.add_button(label = 'Export Plot Figure',width=-1,callback = filename_window,user_data = ('2D', plot_tag, f"y_axis_{tab_tag}",colormap))
b2 = dpg.add_button(label='Export Signals',width=-1,callback=filename_window,user_data=(f"y_axis_{tab_tag}",f"plot_{tab_tag}",'2D'))
add_exporting_tag(window_id,plot=b1,signal=b2)
'''
with dpg.group(horizontal=True, parent=f"{tab_tag}_buttons", tag=f"{tab_tag}_types"):
with dpg.group():
if len(hours) > 1:
with dpg.child_window(tag=f"{tab_tag}_buttons_hours", width=120, height=120):
dpg.add_text('By recording:')
for day in hours:
toggleType(signal_tags, parent=f"{tab_tag}_buttons_hours", user_data=(day, window_id, tab_tag))
if len(days) > 1:
dpg.add_child_window(tag=f"{tab_tag}_buttons_day", height=120, width=120)
dpg.add_text('By day:', parent=f"{tab_tag}_buttons_day")
for day in days:
toggleType(signal_tags, parent=f"{tab_tag}_buttons_day", user_data=(day, window_id, tab_tag))
with dpg.group():
if len(channels) > 1:
with dpg.child_window(tag=f"{tab_tag}_buttons_channel", height=120, width=120):
dpg.add_text('By channel:')
for day in channels:
toggleType(signal_tags, parent=f"{tab_tag}_buttons_channel", user_data=(day, window_id, tab_tag))
if len(sides) > 1:
with dpg.child_window(tag=f"{tab_tag}_buttons_side", height=120, width=120):
dpg.add_text('By hemisphere:')
for day in sides:
toggleType(signal_tags, parent=f"{tab_tag}_buttons_side", user_data=(day, window_id, tab_tag))
'''
[docs]def show_mean(sender,app_data,user_data):
tab_tag = user_data
c = f"{tab_tag}_hide"
if dpg.get_value(c):
dpg.set_value(c,False)
callback = dpg.get_item_callback(c)
if callback:
callback(c, False, dpg.get_item_user_data(c))
dpg.set_value(sender,app_data)
dpg.configure_item(f"{tab_tag}_mean",show=app_data)
dpg.configure_item(f"{tab_tag}_std",show=app_data)
[docs]def toggleDisplay2(signals, signal_tags, window_id, tab_tag, tables=None,plot_tag=None):
events = [signal['EventName'] for signal in signals]
for i,e in enumerate(events):
events[i] = getUpdatedEventName(e)
# if e in all_event_names.keys():
# events[i] = all_event_names[e]
# else:
# all_event_names[e] = e
dates = []
for signal in signals:
date = utl.parse_datetime(signal['DateInitial'])
date = f"{date.day:02d}/{date.month:02d}"
dates.append(date)
dates = sorted(np.unique(dates).tolist())
events = np.unique(events).tolist()
sides = ['Left', 'Right']
with dpg.group(label = f"{tab_tag}_buttons"):
toggleVisibility(signal_tags, tab_tag, group = f"{tab_tag}_buttons",user_data=window_id)
dpg.add_spacer(parent=f"{tab_tag}_buttons")
with dpg.group(horizontal=True, parent=f"{tab_tag}_buttons"):
#check days
if len(dates)>1:
with dpg.child_window(height=cwh,width=cww,horizontal_scrollbar=True):
dpg.add_text('By days \n (Day/Month):')
for day in dates:
toggleType(signal_tags,parent=f"{tab_tag}_buttons_day", user_data=(day, window_id, tab_tag))
#check channels
if len(events)>1:
with dpg.child_window(label = f"{tab_tag}_buttons_events",height=cwh,width=cww,horizontal_scrollbar=True):
dpg.add_text('By events:')
for day in events:
toggleType(signal_tags,parent=f"{tab_tag}_buttons_events", user_data=(day, window_id, tab_tag))
#check sides
if len(sides)>1:
with dpg.child_window(label = f"{tab_tag}_buttons_side",height=cwh,width=cww,horizontal_scrollbar=True):
dpg.add_text('By hemisphere:')
for day in sides:
toggleType(signal_tags,parent=f"{tab_tag}_buttons_side", user_data=(day, window_id, tab_tag))
dpg.add_text('Shaded Area')
dpg.add_radio_button(['Std. Deviation', '95% C.I.'], horizontal=True, callback=deviation, tag = f"{tab_tag}_deviation",user_data=('Standard Deviation',tab_tag))
dpg.add_separator()
with dpg.group(horizontal=True):
dpg.add_checkbox(label='Mean Visible', default_value=True, callback=show_mean, parent=f"{tab_tag}_buttons", tag=f"{tab_tag}_mean_visibility",user_data=tab_tag)
dpg.add_checkbox(label='Mean Only', default_value=False,callback=hide,parent=f"{tab_tag}_buttons",user_data=(signal_tags,[None]), tag=f"{tab_tag}_hide")
dpg.add_checkbox(label='Change to dB',callback=Volt2dB, user_data=(f"y_axis_{tab_tag}", tab_tag, signal_tags, tables),parent=f"{tab_tag}_buttons",default_value=False,tag=f"{tab_tag}_2db")
dpg.add_separator(label='Export')
b1 = dpg.add_button(label = 'Export Plot Figure',width=-1,callback = filename_window,user_data =('2D', plot_tag, f"y_axis_{tab_tag}",0))
b2 = dpg.add_button(label='Export Signals',width=-1,callback=filename_window,user_data=(f"y_axis_{tab_tag}",f"plot_{tab_tag}",'2D'))
add_exporting_tag(window_id,plot=b1,signal=b2)
dpg.add_separator()
dpg.add_button(label='Recalculate Mean Metrics', callback=recalculateMeanMetrics, width=-1, user_data=tab_tag, parent=f"{tab_tag}_buttons")
[docs]def recalculateMeanMetrics(sender, app_data, user_data):
'''
Internal: recaulculates mean metrics of Events
:param sender: button id
:param app_data: -
:param user_data: tab_tag
'''
tab_tag = user_data
bands = getBandsByTab(tab_tag)
if dpg.does_item_exist(f"{tab_tag}_mean"):
x,y = dpg.get_value(f"{tab_tag}_mean")
mean_power = {'X': x, 'Y': y}
dpg.delete_item(f"{tab_tag}_mean_metric", children_only=True)
for band in bands.items():
info = ft.extract_psd_features(mean_power, band,bands)
create_table(f"{tab_tag}_mean_metric",f"{tab_tag}_mean_metric_table", info, trial_label=band[0])
[docs]def hide(sender,app_data,user_data):
'''
Hides streams in plot, except mean and standard deviation and updates checkbox calue
:param sender: button id
:param app_data: value of checkbox
:param user_data: streams, checkboxes
'''
streams, checkboxes = user_data
tab_tag = dpg.get_item_alias(sender)[:-5]
sender_name = dpg.get_item_label(sender)
if dpg.does_item_exist(f"{tab_tag}_mean_visibility") and not dpg.get_value(f"{tab_tag}_mean_visibility"):
c = f"{tab_tag}_mean_visibility"
dpg.set_value(c,True)
callback = dpg.get_item_callback(c)
if callback:
callback(c, True, dpg.get_item_user_data(c))
if checkboxes != [None]:
if app_data == False:
for c in checkboxes:
dpg.set_value(c,True)
callback = dpg.get_item_callback(c)
ud = dpg.get_item_user_data(c)
if sender_name == 'Hide Days': #what
ud = 'dont'
if callback:
callback(c, True, ud)
for s in streams:
bol = not app_data
dpg.configure_item(s, show=bol)
if 'psd' in tab_tag: #survey peaks
if dpg.does_item_exist(f"{s}_peak"): dpg.configure_item(f"{s}_peak",show=bol)
if dpg.does_item_exist(f"{tab_tag}_2db"):
if app_data:
dpg.configure_item(f"{tab_tag}_2db",enabled=False)
if not dpg.does_item_exist(f"{tab_tag}_2db_toast"):
with dpg.tooltip(parent=f"{tab_tag}_2db",tag=f"{tab_tag}_2db_toast"):
dpg.add_text(f"To enable unit conversion uncheck {sender_name}")
else:
dpg.configure_item(f"{tab_tag}_2db",enabled=True)
if dpg.does_item_exist(f"{tab_tag}_2db_toast"): dpg.delete_item(f"{tab_tag}_2db_toast")
#--- Visualization functions
[docs]def plot_td(signals, parent, tab_tag, signal_tags, files = None, events = False, count = []):
'''
Plot Time Domain signals (Streaming, Indefinite, Setup, Survey)
:param signals: array of signals dictionaries
:param parent: y axis, parent of streams
:param tab_tag: tab id
:param signal_tags: tags of signals
:param files: files id, for Combined window
:param events: events data
:param count: aiding object --> plots events according to their file
'''
window_id = extract_window_id(tab_tag)
if events:
event_types = [signal['Type'] for signal in signals]
event_types = np.unique(event_types).tolist()
if count != []: fdx = files
for idx, signal in enumerate(signals):
if count != []:
files = fdx[idx]
idx = count[idx]
series_tag = f"{tab_tag}_series_{idx}" #+extra
combined_tag = f"{tab_tag}_file_{files}_series_{idx}"
if events == False:
x = signal['X']
y = np.ascontiguousarray(signal['Y'])
date = signal['Date']
channel = signal['Channel']
if channel in electrodes_names.keys():
channel = electrodes_names[channel]
else:
electrodes_names[channel] = channel
if 'Data' in app_state['windows'][window_id].keys():
if not dpg.does_item_exist(series_tag):
dpg.add_line_series(x, y, label=f"{channel} ({date})", parent=parent, tag=series_tag)
signal_tags[series_tag] = True # Track visibility
app_state['windows'][window_id]['Data'][tab_tag]['Signals'].append(signal)
else:
print(f"Skipping duplicate series: {series_tag}")
elif not dpg.does_item_exist(combined_tag):
dpg.add_line_series(x, y, label=f"{channel} ({date})", parent=parent, tag=combined_tag)
signal_tags[combined_tag] = True
app_state['windows'][window_id]['Files'][files]['Data'][tab_tag]['Signals'].append(signal)
else:
print(f"Skipping duplicate series: {combined_tag}")
elif events == True:
old_channel = signal['Channel']
if old_channel in electrodes_names.keys():
old_channel = electrodes_names[old_channel]
else:
electrodes_names[old_channel] = old_channel
x, y, type_event, channel, serie_idx = signal['X'],signal['Y'], signal['Type'], old_channel, signal['Serie']
if extract_type_window(window_id) == 'Combined':
segment_tag = f"{tab_tag}_file_{files}_series_{serie_idx}_{type_event}_{idx}"
else:
segment_tag = f"{tab_tag}_series_{serie_idx}_{type_event}_{idx}"
if not dpg.does_item_exist(segment_tag):
dpg.add_line_series(x, y, label=f"{type_event} {signal['Date']} ({channel})", parent=parent, tag=segment_tag)
dpg.bind_item_theme(segment_tag,change_inf_line_color(event_types.index(type_event)))
signal_tags[segment_tag] = True
else:
print(f"Skipping duplicate series: {combined_tag}")
return signal_tags
[docs]def update_plot_td(tab_tag, signal_tags, title = None, signal_mode = None):
'''
Updates tab plot, after filtering and segmentation
:param tab_tag: tab id
:param signal_tags: array of plotted signals' id
:param title: updates title after performed task
:param signal_mode: default to None (normal), else considered segments (epoch)
'''
active_series = getActiveSeries(signal_tags)
window_id = extract_window_id(tab_tag)
mode = extract_mode(tab_tag)
plot_tag = f"plot_{tab_tag}"
previous = dpg.get_item_label(item=plot_tag)
if title != None: dpg.set_item_label(item=plot_tag,label=f"{previous} - {title}")
for i,serie in enumerate(active_series):
if mode == 'Events':
signal = getSignalbyIdxSide(window_id,tab_tag,serie)
elif signal_mode == None:
signal = getSignalbyIdx(window_id,tab_tag,serie)
else:
signal = getSignalbyIdx(window_id,tab_tag,serie, mode='segment')
x = dpg.get_value(serie)[0][:len(signal['X'])]
y = signal['Y']
if dpg.does_item_exist(serie):
dpg.set_value(serie, [x, y]) # Update existing series data
else:
#dpg.add_line_series(x, y, label=f"{series}", parent=f"y_axis_{tab_tag}", tag=serie)
print('No existing series: ', serie)
if isinstance(signal_tags, (list, np.ndarray)):
signal_tags[i] = True # Track visibility
if isinstance(signal_tags, dict):
signal_tags[serie] = True
dpg.set_axis_limits_auto(f"x_axis_{tab_tag}")
[docs]def create_table(tab_label,table_tag,features, trial_label = "New Trial"):
'''
Creates table, if it already existis adds row.
Adds only one row per use, so it need to be inside for-cycle to add several rows.
Includes 'Save Table' button.
:param tab_label: str/int, tab id
:param table_tag: table id
:param features: dictionary of information. Keys will be columns, features[key] will be cell
:param trial_label: Name of row, editable by user.
'''
def add_row():
with dpg.table_row(parent=table_tag):
dpg.add_input_text(default_value=trial_label, width=-1)
for _, value in features.items():
if isinstance(value, (list, np.ndarray, tuple)): # Check if value is a list or NumPy array
if len(value) == 1: # Single-element array
dpg.add_input_text(default_value=f"{float(value[0]):.2f}",readonly=True) # Convert to float for clean display
else: # Multiple values
rounded_values = [float(v) for v in np.round(value, 2)] # Convert each np.float64 to float
dpg.add_input_text(default_value=f"{rounded_values}",readonly=True) # Display clean list
elif isinstance(value, str):
dpg.add_input_text(default_value=value,readonly=True,width=200)
else: # Single number case
dpg.add_input_text(default_value=f"{float(value):.2f}",readonly=True) # Ensure it's a clean float
with dpg.group(parent=tab_label):
if not dpg.does_item_exist(table_tag):
with dpg.table(header_row=True, borders_innerH=True, borders_innerV=True, borders_outerH=True, borders_outerV=True, tag=table_tag):
dpg.add_table_column(label="Trial", width=100)
for band in features.keys():
width =100
if band == 'Electrodes':
width = -1
sender = dpg.add_table_column(label=band, width=width)
if 'std. dev' in band:
if not dpg.does_item_exist(f"{sender}_toast"):
with dpg.tooltip(parent=sender,tag=f"{sender}_toast"):
dpg.add_text(f"Standard Deviation negative values must be considered as positive. \nNegative values are maintained for convertional purpouses.")
add_row()
if not dpg.does_item_exist(f"{table_tag}_save"):
b1 = dpg.add_button(label='Save Table', tag = f"{table_tag}_save", callback=filename_window, user_data=(table_tag, 'csv'), parent=tab_label)
add_exporting_tag(extract_window_id(tab_label),table=b1)
[docs]def fill_params_features(app_data):
'''
Returns app_data function parameters, and respective default values, and documentation.
:param app_data: name of function
'''
match app_data:
case 'correlate':
params = pre.get_required_params(app_data,module='np')
functions = pre.get_functions(module='np')
doc = functions[app_data].__doc__ if functions[app_data].__doc__ else "No documentation available"
case 'dpss':
windows = pre.get_required_params(app_data, module = 'ssw')
welch = pre.get_required_params('welch')
params = windows | welch #join dictionaries
replace = {
'x': '---',
'M': '---',
'fs': 250,
'window': 'skip',
'NW': 3, #min1
'Kmax': 5
}
params.update(replace)
functions = pre.get_functions('ssw')
doc1 = functions[app_data].__doc__ if functions[app_data].__doc__ else "No documentation available"
functions = pre.get_functions()
doc2 = functions['welch'].__doc__ if functions['welch'].__doc__ else "No documentation available"
doc = doc1 + doc2
case 'wavelet':
app_data = 'tfr_array_morlet'
params = pre.get_required_params(app_data, module='mtf')
rep = {'freqs':100,'n_cycles': 2,'sfreq':250, 'output':'power'}
params.update(rep)
functions = pre.get_functions('mtf')
doc = functions['tfr_array_morlet'].__doc__ if functions['tfr_array_morlet'].__doc__ else "No documentation available"
case 'multitaper':
app_data = 'psd_array_multitaper'
params = pre.get_required_params(app_data, module='mtf')
# rep = {'fmax':100, adaptive = True}
functions = pre.get_functions('mtf')
doc = functions['psd_array_multitaper'].__doc__ if functions['psd_array_multitaper'].__doc__ else "No documentation available"
case _:
params = pre.get_required_params(app_data)
functions = pre.get_functions()
doc = functions[app_data].__doc__ if functions[app_data].__doc__ else "No documentation available"
return params, doc
[docs]def apply_feat(func, parameter_values, raw):
'''
Applies function (func), with defined parameter_values, to raw signal.
:param func: str, function name
:param parameter_values: dict, previously filled with default and user-defined
:param raw: list, signal to be aplied feature
'''
signal = None
for param in parameter_values.keys():
if param == 'fs' or param =='sfreq':
parameter_values[param] = 250
if param == 'x' or param =='data':
parameter_values[param] = np.array(raw)
match func:
case 'multitaper':
func = 'psd_array_multitaper'
signal = pre.call_function(func, parameter_values, module='mtf')
return signal
case 'dpss':
if not isinstance(raw,float):
parameter_values['M'] = len(np.array(raw))
try:
window_len = nfft = int(parameter_values['nfft'])
except Exception:
window_len = nfft = 512
step = window_len // 4 #75% overlap (since step = ¼ of window)
kmax = 2*int(parameter_values['NW']) - 1
if parameter_values['Kmax'] == ' ':
parameter_values['Kmax'] = kmax
parameter_values['M'] = window_len
windows = pre.call_function(func, parameter_values, module = 'ssw')
Sxx_sum = np.zeros(nfft//2+1)
n_windows = 0
for start in range(0, len(raw) - window_len + 1, step):
xw = raw[start:start+window_len]
Sxx_tapers = []
for taper in windows:
S1 = np.fft.rfft(xw * taper, n=nfft)
scale = 1 / (250 * np.sum(taper**2))
psd = (np.abs(S1)**2) * scale
Sxx_tapers.append(psd)
Sxx_sum += np.mean(Sxx_tapers, axis=0)
n_windows += 1
Sxx = Sxx_sum / n_windows
freq = np.fft.rfftfreq(nfft, d=1/250)
signal = freq, Sxx
return signal
case 'wavelet':
func = 'tfr_array_morlet'
a = np.array(raw)
try:
#older version was 'data' and not 'epoch_data' --> 10.06.2025 - changed again to prior name: this way it has acess to both values
parameter_values['data'] = a[np.newaxis, np.newaxis, :]
parameter_values['epoch_data'] = a[np.newaxis, np.newaxis, :]
# parameter_values['epoch_data'] = parameter_values['data']
if isinstance(parameter_values['freqs'], str):
parameter_values['freqs'] = int(parameter_values['freqs'])
if isinstance(parameter_values['n_cycles'], str):
parameter_values['n_cycles'] = float(parameter_values['n_cycles'])
if isinstance(parameter_values['n_cycles'],(int, float)):
parameter_values['freqs'] = np.arange(1,parameter_values['freqs']+1, 1)
parameter_values['n_cycles'] = parameter_values['freqs'] / parameter_values['n_cycles']
signal = pre.call_function(func, parameter_values, module = 'mtf')
return signal
except Exception as e: print(e)
case _:
try:
signal = pre.call_function(func, parameter_values)
except Exception as e:
return str(e)
return signal
[docs]def display_error(tab_tag,e):
'''
Displays error e in doc container of tab_tag
:param tab_tag: str/int, tab parent id
:param e: str, exception thrown
'''
doc_container = f"{tab_tag}_ft_doc"
if dpg.does_item_exist(doc_container):
v = dpg.get_value(f"{doc_container}_doc")
dpg.delete_item(doc_container,children_only=True)
ee = dpg.add_text(e, parent=doc_container)
dpg.add_separator(parent=doc_container)
dpg.add_text(v, parent=doc_container, tag = f"{doc_container}_doc")
dpg.bind_item_theme(ee, change_text_color(1))
[docs]def plot_featured_signal(signal_tags, tab_label, func, tab_tag, parameter_values, mode = None):
'''
Computes and plots method func (with parameter_values that are user-defined),
to signals identified by signal_tags, to be ploted in tab_label, child of tab_tag.
Resulting func must be 2D.
May be applied to epoch segments, or raw signals.
:param signal_tags: list, of signals' id
:param tab_label: str/int, tab id
:param func: str, function name
:param tab_tag: str/int, tab parent id
:param parameter_values: dict, keys: parameter names, items: values
:param mode: default None, if 'segment' gets signal data accordingly
'''
window_id = extract_window_id(tab_tag)
tags = getActiveSeries(signal_tags)
tables, events_tags, events_names, channels, ploted_tags, days = [], [], [], [], [], []
comparing = False
if dpg.does_item_exist(f"{tab_label}_bva"):
comparing = dpg.get_value(f"{tab_label}_bva")
if dpg.does_item_exist(f"{tab_label}_smp"): dpg.delete_item(f"{tab_label}_smp")
bands = getBandsByTab(tab_tag)
dpg.delete_item(f"{tab_label}_plot_group")
units = 'µV^2/Hz'
if 'scaling' in parameter_values and parameter_values['scaling'] == 'spectrum':
units = 'µV^2'
if not 'Data' in app_state['windows'][window_id]: #combined
for j in app_state['windows'][window_id]['Files']:
if tab_tag in j['Data'].keys():
if isinstance(j['Data'][tab_tag], dict) and 'Events' in j['Data'][tab_tag].keys():
if len(j['Data'][tab_tag]['Events']) > 1:
mode = 'segment'
break
else: mode = None
else: mode = None
deviation_type = 'Standard Deviation'
all_info={}
plot_tag = f"{tab_label}_plot"
with dpg.group(horizontal=True, parent = tab_label,tag=f"{tab_label}_plot_group"):
with dpg.plot(label=f"Spectral Analysis - {func}", height=400, width=600, tag = plot_tag):
dpg.bind_colormap(plot_tag, 0)
dpg.add_plot_legend(location=dpg.mvPlot_Location_NorthEast)
x_axis = dpg.add_plot_axis(dpg.mvXAxis, label="Frequency (Hz)") #tag=f"{tab_label}_x_axis"
y_axis = dpg.add_plot_axis(dpg.mvYAxis, label=f"LFP Power ({units})", tag= f"{tab_label}_y_axis")
def contiguous_array(signal, func):
if func == 'multitaper':
mask = signal[1] <= 100
psd = np.ascontiguousarray(signal[0][mask])
freqs = np.ascontiguousarray(signal[1][mask])
else:
mask = signal[0] <= 100
freqs = np.ascontiguousarray(signal[0][mask])
psd = np.ascontiguousarray(signal[1][mask])
p = {'X': freqs, 'Y': psd}
return p
power = []
max_p = 0
for i,tag in enumerate(tags):
stream = getSignalbyIdx(window_id,tab_tag,tag,mode)
days.append(stream['Date'])
series_label = dpg.get_item_configuration(tag)['label']
channel, side = extract_channel_hemisphere(stream['Channel'])
channels.append(channel)
full_channel = f"{channel}_{side}"
if full_channel not in all_info.keys():
all_info[full_channel] = {}
if mode == 'segment':
events_names.append(f"{stream['Type']}")
#BEFORE AND AFTER PSD
if comparing == True:
temp = []
zero = [i for i,x in enumerate(stream['X']) if x == 0.0][0]
pre_event = stream['Y'][:zero]
post_event = stream['Y'][zero:]
pre_event = apply_feat(func,parameter_values,pre_event)
post_event = apply_feat(func,parameter_values,post_event)
#BEFORE-------------------------------------------------
p = contiguous_array(pre_event, func)
b_label = 'Before ' + series_label
line = dpg.add_line_series(p['X'],p['Y'], label=b_label, parent=y_axis, tag = f"{tab_label}_{b_label}_{func}_pre")
events_tags.append(f"{tab_label}_{b_label}_{func}_pre")
power.append(p)
temp.append(p)
ploted_tags.append(line)
#AFTER-------------------------------------------------
p = contiguous_array(post_event, func)
a_label = 'After ' + series_label
line = dpg.add_line_series(p['X'],p['Y'], label=a_label, parent=y_axis, tag = f"{tab_label}_{a_label}_{func}_post")
events_tags.append(f"{tab_label}_{a_label}_{func}_post")
ploted_tags.append(line)
power.append(p)
temp.append(p)
max_p = max(max_p,np.max(p['Y']))
states = ['Before', 'After']
#TABLE CONTENT
full_power = np.sum(p['Y'])
mean_full_power = np.nanmean(p['Y'])
if not dpg.does_item_exist(f"{tab_label}_{series_label}"):
dpg.add_collapsing_header(label = f'{series_label}', tag =f"{tab_label}_{series_label}", parent = tab_label)
else:
dpg.delete_item(f"{tab_label}_{series_label}", children_only=True)
for i,p in enumerate(temp):
freqs, psd = p['X'], p['Y']
for b, v in bands.items():
band = {b: v}
#mean power per band
pc = ft.mean_power_all_bands(p, band)
std = ft.std_dev_streams(p, band)
mean = {'Mean Power (mean, std. dev)': [np.round(pc[b],2), np.round(std[b],2)]}
#normalized mean power
norm_mean = {'% Mean Power (mean, std. dev)': [np.round(abs(pc[b]*100)/mean_full_power,2), np.round(std[b],2)]}
#auc
params = pre.get_required_params('simpson', module='si')
param_auc = {}
for name, param in params.items(): param_auc[name] = param
mask = (freqs >= v[0]) & (freqs <= v[1])
param_auc['y'] = psd[mask]
simpson = pre.call_function('simpson', param_dict=param_auc, module='si')
auc = {'Sum Power (AUC)': f"{np.round(simpson,2)}"}
#normalized auc
norm_auc = {'% Sum Power (AUC)': f"{np.round(abs(simpson*100)/full_power,2)}"}
#peak power
peak = ft.peak_per_band(p, band)
picos = {'Peak Power (power, frequency)': p for p in peak.values()}
info = mean | norm_mean| auc | norm_auc| picos
if not dpg.does_item_exist(f"{tab_label}_{series_label}_{states[i]}"): dpg.add_text(f"{states[i]}", parent=f"{tab_label}_{series_label}", tag = f"{tab_label}_{series_label}_{states[i]}")
create_table(f"{tab_label}_{series_label}",f"{tab_label}_{series_label}_table_{states[i]}", info, trial_label=b)
tables.append(f"{tab_label}_{series_label}_table_{states[i]}")
#NORMAL PSD
if comparing == False:
y = stream['Y']
try:
signal = apply_feat(func,parameter_values,y)
if isinstance(signal, str):
display_error(tab_tag, signal)
return
p = contiguous_array(signal, func)
max_p = max(max_p,np.max(p['Y']))
dpg.add_line_series(p['X'],p['Y'], label=series_label, parent=y_axis, tag = f"{tab_label}_{series_label}_{func}")
except Exception as e:
display_error(tab_tag,e)
return
power.append(p)
freqs, psd = p['X'], p['Y']
ploted_tags.append(f"{tab_label}_{series_label}_{func}")
if not dpg.does_item_exist(f"{tab_label}_hemisphere"):
dpg.add_tree_node(tag=f"{tab_label}_hemisphere",
label="By Hemisphere",
parent=tab_label,
default_open=False
)
if not dpg.does_item_exist(f"{tab_label}_channel"):
dpg.add_tree_node(tag=f"{tab_label}_channel",
label="By Channel",
parent=tab_label,
default_open=False
)
if not dpg.does_item_exist(f"{tab_label}_individual"):
dpg.add_tree_node(tag=f"{tab_label}_individual",
label="Individual Streams",
parent=tab_label,
default_open=False
)
if not dpg.does_item_exist(f"{tab_label}_{side}"):
dpg.add_collapsing_header(label = f'{side} HEMISPHERE', tag =f"{tab_label}_{side}", parent = f"{tab_label}_hemisphere")
if not dpg.does_item_exist(f"{tab_label}_{stream['Channel']}"):
dpg.add_collapsing_header(label = f"{stream['Channel']}", tag =f"{tab_label}_{stream['Channel']}", parent = f"{tab_label}_channel")
if not dpg.does_item_exist(f"{tab_label}_{series_label}"):
dpg.add_collapsing_header(label = f'{series_label}', tag =f"{tab_label}_{series_label}", parent = f"{tab_label}_individual")
else:
dpg.delete_item(f"{tab_label}_{series_label}", children_only=True)
#TABLE CONTENT
for band in bands.items():
info = ft.extract_psd_features(p, band,bands=bands)
key = list(info.keys())[0]
if band[0] not in all_info[full_channel].keys():
all_info[full_channel][band[0]] = []
all_info[full_channel][band[0]].append([str(stream['Date']),info[key]])
create_table(f"{tab_label}_{series_label}",f"{tab_label}_{series_label}_table", info, trial_label=band[0])
create_table(f"{tab_label}_{stream['Channel']}",f"{tab_label}_{stream['Channel']}_table", info, trial_label=f"{band[0]} | {series_label}")
create_table(f"{tab_label}_{side}",f"{tab_label}_{side}_table", info, trial_label=f"{band[0]} | {series_label}")
tables.append(f"{tab_label}_{series_label}_table")
tables.append(f"{tab_label}_{stream['Channel']}_table")
tables.append(f"{tab_label}_{side}_table")
updateSignalbyIdx(p,tab_tag,tag,func,mode=mode)
freqs, psd = p['X'], p['Y']
if len(power)>1:
flag = False
for i in range(len(power)-1):
if len(power[i]['Y']) != len(power[i+1]['Y']):
flag = True
if flag == True: #checks if they DON'T have the same len
# Define 1-Hz bin edges from 0 to 125 Hz
bin_edges = np.arange(0, 100, 1) # 0–1, 1–2, ..., 124–125
bin_centers = bin_edges[:-1] + 0.5
# Collect all binned PSDs
binned_psds = []
for p in power:
freqs = p['X']
psd = p['Y']
binned = []
for i in range(len(bin_edges) - 1):
fmin, fmax = bin_edges[i], bin_edges[i+1]
mask = (freqs >= fmin) & (freqs < fmax)
if np.any(mask):
binned.append(np.mean(psd[mask]))
else:
binned.append(np.nan) # No data in this bin
binned_psds.append(binned)
# Convert to NumPy array
binned_psds = np.array(binned_psds)
# Compute mean PSD across trials (ignoring NaNs)
all_mean = np.nanmean(binned_psds, axis=0)
all_std = np.nanstd(binned_psds, axis=0)
freqs = np.arange(0,len(all_mean),1)
else:
all_mean = np.nanmean([p['Y'] for p in power], axis=0)
all_std = np.nanstd([p['Y'] for p in power], axis=0)
minus = all_mean - all_std
plus = all_mean + all_std
mean_power = {
'Y': all_mean,
'X': freqs
}
if not dpg.does_item_exist(f"{tab_label}_mean_metric"):
dpg.add_collapsing_header(label = 'MEAN POWER', tag =f"{tab_label}_mean_metric", parent = tab_label)
else:
dpg.delete_item(f"{tab_label}_mean_metric", children_only=True)
for band in bands.items():
info = ft.extract_psd_features(mean_power, band,bands=bands)
create_table(f"{tab_label}_mean_metric",f"{tab_label}_mean_metric_table", info, trial_label=band[0])
tables.append(f"{tab_label}_mean_metric_table")
freqs = np.asarray(freqs, dtype=np.float32)
all_mean = np.asarray(all_mean, dtype=np.float32)
s = dpg.add_shade_series(freqs.tolist(),y1=minus.tolist(),y2=plus.tolist(),label = 'Std. Dev', tag=f"{tab_label}_std", parent=y_axis)
dpg.bind_item_theme(s, create_alpha_theme())
m = dpg.add_line_series(freqs.tolist(), all_mean.tolist(), label="Mean Power", parent=y_axis, tag=f"{tab_label}_mean")
dpg.bind_item_theme(m,change_line_thickness(3))
dpg.set_axis_limits(x_axis,min(freqs),max(freqs))
dpg.set_axis_limits(y_axis,min(psd),max(psd))
def choose_toggle(sender, app_data, user_data):
if dpg.does_item_exist(f"{tab_label}_bva"):
comparing = dpg.get_value(f"{tab_label}_bva")
if comparing: toggle(sender,app_data,user_data)
else: toggle2(sender,app_data,user_data)
else: toggle2(sender,app_data,user_data)
def toggle(sender,app_data,user_data):
axis = user_data
power_temp = []
if dpg.get_value(f"{tab_label}_hide") == True:
callback = dpg.get_item_callback(f"{tab_label}_hide")
dpg.set_value(f"{tab_label}_hide",False)
if callback:
callback(f"{tab_label}_hide", None, dpg.get_item_user_data(f"{tab_label}_hide"))
for i, tag in enumerate(events_tags):
key = str(dpg.get_item_label(sender))
if axis!=None:
key = key.split(' ')[axis]
if key in tag:
dpg.configure_item(tag,show=app_data)
visibility = dpg.get_item_configuration(tag)['show']
if visibility==True:
power_temp.append(power[i])
if len(power_temp) == 0:
dpg.configure_item(f"{tab_label}_mean",show=False)
dpg.configure_item(f"{tab_label}_std",show=False)
return
mean_temp = np.mean([p['Y'] for p in power_temp], axis=0)
std = np.std([p['Y'] for p in power_temp], axis=0)
times = dpg.get_value(f"{tab_label}_deviation")
if times == '95% Confidence Interval':
times = 2
else: times = 1
minus = mean_temp - times*std
plus = mean_temp + times*std
dpg.configure_item(f"{tab_label}_mean",y=mean_temp,show=True)
dpg.configure_item(f"{tab_label}_std",y1=minus, y2=plus,show=True)
def toggle2(sender,app_data,user_data):
if dpg.get_value(f"{tab_label}_hide") == True:
callback = dpg.get_item_callback(f"{tab_label}_hide")
dpg.set_value(f"{tab_label}_hide",False)
if callback:
callback(f"{tab_label}_hide", None, dpg.get_item_user_data(f"{tab_label}_hide"))
_ = []
ch = dpg.get_item_label(sender)
for tag in ploted_tags:
if ch in tag:
dpg.configure_item(tag, show=app_data)
flag = dpg.get_item_configuration(tag)['show']
if flag:
_.append(tag)
if len(_) == 0:
dpg.configure_item(f"{tab_label}_mean",show=False)
dpg.configure_item(f"{tab_label}_std",show=False)
return
array = [dpg.get_value(t)[1] for t in _]
mean = np.mean(array, axis=0)
dpg.configure_item(f"{tab_label}_mean",y=mean)
std = np.std(array, axis=0)
times = dpg.get_value(f"{tab_label}_deviation")
if times == '95% Confidence Interval':
times = 2
else: times = 1
minus = mean - times*std
plus = mean + times*std
dpg.configure_item(f"{tab_label}_mean",y=mean,show=True)
dpg.configure_item(f"{tab_label}_std",y1=minus, y2=plus,show=True)
def toggle_over_time(sender,app_data,user_data):
y_axis = user_data
temp_tags = dpg.get_item_children(y_axis,slot=1)
labels = [dpg.get_item_label(tag) for tag in temp_tags]
name = dpg.get_item_label(sender)
for idx,label in enumerate(labels):
tag = temp_tags[idx]
if name in label:
dpg.configure_item(tag,show=app_data)
elif (label == '') or (label == ' '): #for scatter series, so it doesnt show legend repeated, but still can toggle visibility
tag = dpg.get_item_alias(tag)
if name in tag:
dpg.configure_item(tag,show=app_data)
def show_power_over_time(sender,app_data,user_data):
all_info=user_data
color_int =0
filename = f"MeanPower_Time_{tab_tag}"
# save_band_powers_to_txt(all_info,filename)
ch,colors = [], []
if dpg.does_item_exist(f"{tab_label}_smp"): dpg.delete_item(f"{tab_label}_smp")
with dpg.group(parent=tab_label, horizontal=True,tag=f"{tab_label}_smp"):
with dpg.plot(label='Band Mean Power over Time',height=400, width=700,tag=f"{tab_label}_smp_plot"):
dpg.add_plot_legend(horizontal=True)
xx = dpg.add_plot_axis(dpg.mvXAxis,label='Session Days')
yy = dpg.add_plot_axis(dpg.mvYAxis,label='Band Mean Power (uV/Hz)') #y axis always links to the x axis above!
x2 = dpg.add_plot_axis(dpg.mvXAxis2,label='Inserted Values',show=False)
y2 = dpg.add_plot_axis(dpg.mvYAxis2,label='',show=False,opposite=True)
for channel, info in all_info.items():
ch.append(channel)
for band, power in info.items():
dates, values = [utl.parse_datetime(p[0]).timestamp() for p in power], [p[1][0] for p in power]
dates_array = np.arange(0,len(dates),1)
if band=='Beta': label=f"{band} | {channel}" # so they all have the same spacing
else: label=f"{band} | {channel}"
dpg.add_line_series(dates_array.tolist(),values,parent=yy,label=label)
s = dpg.add_scatter_series(dates_array.tolist(),values,parent=yy,label='',tag=f"{tab_tag}_smp_{channel}_{band}_{color_int}")
color = dpg.get_colormap_color(0,color_int)
dpg.bind_item_theme(s,change_plot_color(color))
color_int=color_int+ 1
colors.append(color)
colors.append(color) #one for line series, other for scatter series
ch = np.unique(ch).tolist()
bds = list(all_info[channel].keys())
with dpg.group():
with dpg.group(horizontal=True):
for i, array in enumerate([ch,bds]):
if len(array)>1:
if i==0:
title='By Channel:'
width = cww
else:
title='By Band:'
width = 80
with dpg.child_window(height=cwh,width=width):
dpg.add_text(title)
for c in array: dpg.add_checkbox(label=c,callback=toggle_over_time,user_data=yy,default_value=True)
dpg.add_separator()
dpg.add_button(label='Insert Line Series',width=-1,callback=insert_line_series,user_data=(x2,y2,yy,colors))
dpg.add_separator(label='Export')
b1 = dpg.add_button(label = 'Export Plot Figure',width=-1,tag=f"{tab_label}_save_plot",callback = filename_window,user_data =('SMP', f"{tab_label}_smp_plot",yy,colors))
b2 = dpg.add_button(label='Export Signals',width=-1,callback=save_band_powers_to_txt, user_data=(all_info,filename))
add_exporting_tag(window_id,plot=b1,signal=b2)
exporting_tags['Signals'].append(b2)
with dpg.child_window(label="X Axis Dates",width=-1, height=140):
dpg.add_text("Index: Date")
dpg.add_separator()
for i, date in enumerate(dates):
dpg.add_text(f"{i}: {pre.get_date_from_ts(date)}")
def insert_line_series(sender, app_data, user_data):
def read_txt(sender, app_data, user_data):
x2,y2,yy,colors = user_data
streaming_file = app_data['file_path_name']
info =utl.read_lines(streaming_file)
old = list(dpg.get_item_user_data(f"{tab_label}_save_plot"))
last_x = dpg.get_value(dpg.get_item_children(yy,1)[0])[0]
label = list(info.keys())[0]
values = [float(v) for v in info[label]]
y_title = dpg.get_item_label(y2)
if y_title == '':
y_title=label
else:
y_title=y_title+ ' & '+label
dpg.configure_item(y2,show=True,label=y_title)
dpg.configure_item(x2,show=True)
line = dpg.add_line_series(last_x,values,label=label,parent=y2)
# s = dpg.add_scatter_series(last_x,values,parent=yy)
# dpg.bind_item_theme(s,change_plot_color(dpg.get_colormap_color(0,color_int)))
dpg.bind_item_theme(line,change_line_thickness(4))
idx = len(np.unique(colors).tolist())
color = dpg.get_colormap_color(0,idx)
colors.append(color)
old[-1] = colors
dpg.set_item_user_data(f"{tab_label}_save_plot",tuple(old))
def create_file_dialog(user_data):
with dpg.file_dialog(
directory_selector=False,
show=True,
callback=read_txt,
width=700,
height=400,
modal=True,
user_data=user_data,
default_path=directories['events'],
) as dialog_id:
dpg.add_file_extension(".txt", color=(0, 255, 0, 255), custom_text="[TEXT File]")
create_file_dialog(user_data)
checkboxes = []
with dpg.group(tag=f"{tab_label}_buttons"):
if mode == 'segment':
dpg.add_checkbox(label='Before vs. After',callback=before_vs_after, user_data=(signal_tags,tab_label,func,tab_tag,parameter_values), tag = f"{tab_label}_bva", default_value=comparing)
if comparing== True:
dpg.add_separator()
dpg.add_text('Toggle Streams')
with dpg.group(parent=f"{tab_label}_buttons", horizontal=True,tag=f"{tab_label}_bvabuttons"):
with dpg.child_window(height=cwh,width=cww,horizontal_scrollbar=True): #Before and After
dpg.add_text('When:')
cc = dpg.add_checkbox(label='Before', default_value=True, callback=toggle, user_data=-1)
c = dpg.add_checkbox(label='After', default_value=True, callback=toggle, user_data=-1)
checkboxes.append(cc)
checkboxes.append(c)
if dpg.does_item_exist(f"{tab_label}_hide"): dpg.set_item_user_data(f"{tab_label}_hide",(ploted_tags, checkboxes))
channels = np.unique(channels)
days = np.unique(days)
if mode == 'segment':
if dpg.does_item_exist(f"{tab_label}_bvabuttons"):
this_parent = f"{tab_label}_bvabuttons"
else:
this_parent = f"{tab_label}_buttons"
with dpg.child_window(height=cwh,width=cww,horizontal_scrollbar=True, parent=this_parent): #Per Type of Event
events_type = np.unique(events_names)
dpg.add_text('Events:')
for i, etype in enumerate(events_type):
c = dpg.add_checkbox(label=etype, callback=choose_toggle, default_value=True)
checkboxes.append(c)
with dpg.group(horizontal=True):
if len(channels)>1:
with dpg.child_window(height=cwh,width=cww,horizontal_scrollbar=True): #channels
dpg.add_text('Channels: ')
for ch in channels:
c = dpg.add_checkbox(label=ch, callback=choose_toggle, default_value=True)
checkboxes.append(c)
if len(days)>1:
with dpg.child_window(height=cwh,width=cww,horizontal_scrollbar=True): #days
dpg.add_text('Recordings: ')
for d in days:
c = dpg.add_checkbox(label=d, callback=choose_toggle, default_value=True)
checkboxes.append(c)
with dpg.child_window(height=cwh,width=cww,horizontal_scrollbar=True): #side
dpg.add_text('Hemisphere: ')
sides = ['LEFT', 'RIGHT']
for d in sides:
c = dpg.add_checkbox(label=d, callback=choose_toggle, default_value=True)
checkboxes.append(c)
dpg.add_separator()
with dpg.group(horizontal=True):
dpg.add_checkbox(label='Change to dB',callback=Volt2dB, user_data=(y_axis, tab_label, ploted_tags, tables), tag = f"{tab_label}_2db")
dpg.add_checkbox(label='Show Mean Only', callback=hide, tag = f"{tab_label}_hide", user_data=(ploted_tags, checkboxes))
dpg.add_separator()
dpg.add_text('Shaded Area')
dpg.add_radio_button(['Std. Deviation', '95% C.I.'], horizontal=True, callback=deviation, tag = f"{tab_label}_deviation",user_data=(deviation_type,tab_label))
dpg.add_separator()
if len(days)>1:
dpg.add_button(label='Show Mean Power Across Time',width=-1,callback=show_power_over_time,user_data=all_info)
b1 = dpg.add_button(label = 'Export Plot Figure',callback = filename_window,user_data =('2D', plot_tag, y_axis,0),width=-1)
b2 = dpg.add_button(label='Export Signals',width=-1,callback=filename_window,user_data=(y_axis,plot_tag,'2D'))
add_exporting_tag(window_id,plot=b1,signal=b2)
time.sleep(0.2)
dpg.set_axis_limits_auto(x_axis)
dpg.set_axis_limits_auto(y_axis)
[docs]def plot_spectrogram_joined(tags, tab_label, func, tab_tag, parameter_values, mode = None):
'''
Computes and plots spectrogram of joined recordings, per channel.
:param tags: list, signals' id
:param tab_label: str/id, tab id
:param func: str, function name
:param tab_tag: str/id, parent tab id
:param parameter_values: dict,
:param mode: Description
'''
window_id = extract_window_id(tab_tag)
wt = extract_type_window(window_id)
new_signal, identifiers, event_names = {}, {}, []
for tag in tags:
signal = getSignalbyIdx(window_id,tab_tag,tag,mode)
ch = signal['Channel']
ch = getUpdatedElectrodeName(ch)
if func in signal.keys(): signal.pop(func)
temp = {key: np.array([]) for key in signal.keys()}
temp['dX'] = []
if ch not in new_signal.keys(): new_signal[ch] = temp
else: new_signal[ch] = new_signal[ch] | temp
current_start, prev_end_time = {}, {}
# breakpoint()
for tag in tags:
signal = getSignalbyIdx(window_id,tab_tag,tag,mode)
'''X = signal['X']
arr = np.array(X)
if np.issubdtype(arr.dtype, np.number):
valid = arr[~np.isnan(arr)]
if len(valid) > 0:# and np.round(valid[0]) == 0:
# 'X' is in seconds
signal['dX'] = [signal['Date'] + timedelta(seconds=val) for val in X]
else:
# 'X' is in Unix timestamp
signal['dX'] = [pre.get_date_from_ts(val) for val in X]
ch = signal['Channel']
for key in signal.keys():
if key == 'dX' or key == 'Y':
new_signal[ch][key] = np.append(new_signal[ch][key], np.array(signal[key]))
identifiers[ch] = len(signal[key])
else:
new_signal[ch][key] = np.append(new_signal[ch][key], signal[key])'''
X = signal['X']
arr = np.array(X)
if np.issubdtype(arr.dtype, np.number):
valid = arr[~np.isnan(arr)]
if len(valid) > 0:# and np.round(valid[0]) == 0:
try:
signal['dX'] = [signal['Date'] + timedelta(seconds=val) for val in X]
except Exception:
signal['dX'] = [datetime.fromtimestamp(val) for val in X]
else:
# 'X' is in Unix timestamp
signal['dX'] = [pre.get_date_from_ts(val) for val in X]
ch = getUpdatedElectrodeName(signal['Channel'])
prev = len(new_signal[ch]['X'])
current_start[ch] = signal['dX'][0]
# Insert zero gap if there's a time difference
if ch in prev_end_time:
gap = current_start[ch] - prev_end_time[ch]
if gap.total_seconds() > 0:
# Limit the gap to 10 min
gap = min(gap, timedelta(seconds=15))
# Determine how many points to fill (based on sampling rate)
sample_rate = 250
n_fill = int(gap.total_seconds() * sample_rate)
if n_fill > 0:
# Generate filler time and zero signal
filler_dX = [prev_end_time[ch] + timedelta(seconds=i / sample_rate) for i in range(n_fill)]
filler_Y = np.zeros(n_fill)
# Append the filler
new_signal[ch]['dX'] = np.append(new_signal[ch]['dX'], filler_dX)
new_signal[ch]['Y'] = np.append(new_signal[ch]['Y'], filler_Y)
else: prev_end_time[ch] = None
for key in signal.keys():
if key == 'dX' or key == 'Y':
new_signal[ch][key] = np.append(new_signal[ch][key], np.array(signal[key]))
identifiers[ch] = len(signal[key])
elif key == 'Events':
for e in signal[key]:
event_names.append(e['Type'])
e['sIdx'] = e['Idx'] + prev
new_signal[ch][key] = np.append(new_signal[ch][key], signal[key])
else:
new_signal[ch][key] = np.append(new_signal[ch][key], signal[key])
prev_end_time[ch] = signal['dX'][-1]
event_names = np.unique(event_names).tolist()
num_plots = len(new_signal.keys())
num_cols = 1
num_rows = num_plots
global_min, global_max = 0,0
per_tag, spectrogram_tags = {}, []
group_tag = f"{tab_label}_plot_group"
with dpg.group(horizontal=True, parent=f"{tab_label}", tag=group_tag):
dpg.add_colormap_scale(tag=f"{group_tag}_colormap_scale", label="Density (dB)", colormap=dpg.mvPlotColormap_Jet,
min_scale = global_min, max_scale=global_max, height=400*num_rows)
with dpg.subplots(rows=num_rows, columns=num_cols, label="Spectrograms", width=800, height=400*num_rows, tag = f"{tab_label}_subplots"):
for channel in new_signal:
title =f"Joined Spectrogram - {channel}"
subplot_tag = f"{tab_label}_joined_{channel}"
with dpg.plot(label=title, width=800, height=400, tag=subplot_tag, parent=f"{tab_label}_subplots"):
x_axis = dpg.add_plot_axis(dpg.mvXAxis, label="Time (s)", scale= dpg.mvPlotScale_Time)
y_axis = dpg.add_plot_axis(dpg.mvYAxis, label="Frequency (Hz)")
y = new_signal[channel]['Y']
if current_start == {}:
x = new_signal[channel]['dX']
if isinstance(x[0], (int, float)): x = [pre.get_date_from_ts(val) for val in x]
x = [(xx + timedelta(hours=1)).timestamp() for xx in x]
else:
x = np.arange(0,len(y)/250,1)
dpg.configure_item(x_axis, scale = dpg.mvPlotScale_Linear)
try:
events = new_signal[channel]['Events']
except Exception as e:
events = 'None'
parameter_values['x'] = np.array(y)
signal = pre.call_function(func, parameter_values)
frequencies, times, Sxx = signal
Sxx_log = 10 * np.log10(Sxx + 1e-10) # Convert to dB scale
Sxx_log = np.flipud(Sxx_log)
# channels.append(Sxx_log)
per_tag[channel] = [Sxx_log,subplot_tag, title, events,x]
global_min = min(global_min, Sxx_log.min())
global_max = max(global_max, Sxx_log.max())
tag = dpg.add_heat_series(Sxx_log.flatten(), rows=Sxx_log.shape[0], cols=Sxx_log.shape[1],
scale_min=global_min, scale_max=global_max,
parent=y_axis, format="",bounds_min=[x[0],0], bounds_max=[x[-1],125])
spectrogram_tags.append(tag)
dpg.bind_colormap(subplot_tag, dpg.mvPlotColormap_Jet)
if not isinstance(events,str):
dpg.add_plot_legend()
for event in events:
stamp = event['sIdx']/250 #axis is seconds, not datetime
type = event['Type']
type = getUpdatedEventName(type)
if stamp<=x[-1]:
ev = dpg.add_inf_line_series(x=[stamp],label=f'{type}',parent=y_axis)
dpg.bind_item_theme(ev,change_inf_line_color(event_names.index(type)))
dpg.configure_item(f"{group_tag}_colormap_scale", min_scale=global_min,max_scale=global_max)
def link(sender, app_data):
dpg.configure_item(f"{tab_label}_subplots", link_all_x=app_data, link_all_y=app_data)
'''def mean(sender,app_data):
mean_Sxx = np.mean(np.stack(channels), axis=0)
with dpg.plot(label='Mean Spectrogram', width=-1, height=400, tag=f"{tab_label}_join_mean", parent=f"{tab_label}_merged"):
x_axis = dpg.add_plot_axis(dpg.mvXAxis, label="Time (s)")
y_axis = dpg.add_plot_axis(dpg.mvYAxis, label="Frequency (Hz)")
if mode and wt=='Combined':
emin = -int(dpg.get_value(f"{tab_tag}_emin"))
emax = int(dpg.get_value(f"{tab_tag}_emax"))
else:
emin, emax = 0, len(times)
dpg.add_heat_series(mean_Sxx.flatten(), rows=mean_Sxx.shape[0], cols=mean_Sxx.shape[1],
scale_min=global_min, scale_max=global_max,
parent=y_axis, format="", bounds_min=[emin,0], bounds_max=[emax,125])
dpg.bind_colormap(f"{tab_label}_join_mean", dpg.mvPlotColormap_Jet)
if not isinstance(events,str):
for event in events:
stamp = event['Idx']/250 #spectrogram axis is in reality from 0 to 1, and the columns are time based, not sample
type = event['Type']
dpg.add_plot_legend()
dpg.add_inf_line_series(x=[stamp],label=f'{type}',parent=y_axis)
'''
def spectrograms_power_over_time(sender,app_data,user_data):
bands = getBandsByTab(tab_tag)
if dpg.does_item_exist(f"{tab_label}_merged"): dpg.delete_item(f"{tab_label}_merged")
with dpg.group(parent=tab_label, tag = f"{tab_label}_merged"):
alpha_theme = create_alpha_theme()
dpg.add_text('Band Power over Time', parent=f"{tab_label}_merged")
dpg.add_separator(parent=f"{tab_label}_merged")
for _, value in per_tag.items():
sxx, plot_tag, plot_label, stream_events, time = value
header_tag = f"{plot_tag}_header"
new_plot_tag = f"{plot_tag}_overtime"
if dpg.does_item_exist(header_tag): dpg.delete_item(header_tag, children_only=True)
with dpg.collapsing_header(tag=header_tag, parent=f"{tab_label}_merged", label=plot_label):
with dpg.plot(label=plot_label, width=-1,tag= f"{plot_tag}_overtime"):
dpg.add_plot_legend()
x_axis = dpg.add_plot_axis(dpg.mvXAxis, label="Time (s)")
y_axis = dpg.add_plot_axis(dpg.mvYAxis, label="Power (dB)")
for b,v in bands.items():
mask = (frequencies*125 >= v[0]) & (frequencies*125 < v[1])
if np.any(mask):
power = np.mean(sxx[mask, :],axis=0) # mean across freqs -> shape: (time,)
# power = sxx[mask, :]
std = np.std(sxx[mask, :], axis=0)
min_std = power-std
max_std = power+std
x_time = np.linspace(0,max(time),len(power))
dpg.add_shade_series(x_time,min_std,y2=max_std,label = f"{b} Std. Deviation", tag=f"{plot_tag}_{b}_std", parent=y_axis)
dpg.bind_item_theme(f"{plot_tag}_{b}_std", alpha_theme)
dpg.add_line_series(x_time,power,label = f"{b} Power", tag=f"{plot_tag}_{b}_mean", parent=y_axis)
if not isinstance(stream_events,str) and wt == 'Individual':
for event in stream_events:
# stamp = [event['Idx']/(len(times)*250)] #spectrogram axis is in reality from 0 to 1, and the columns are time based, not sample
stamp = [event['Idx']/250]
type = event['Type']
if stamp[0] <= time[-1]:
dpg.add_plot_legend()
dpg.add_inf_line_series(x=stamp,label=f'{type}',parent=y_axis)
b1 = dpg.add_button(label = 'Export Plot Figure',callback = filename_window,user_data =('2D', new_plot_tag, y_axis,0),width=-1)
b2 = dpg.add_button(label='Export Signals',width=-1,callback=filename_window,user_data=(y_axis,new_plot_tag,'2D'))
add_exporting_tag(window_id,plot=b1,signal=b2)
with dpg.group(horizontal=False, tag = f"{group_tag}_buttons"):
dpg.add_checkbox(label='Synchronize Axis', callback=link)
dpg.add_button(label='Band Power over Time', callback=spectrograms_power_over_time)
dpg.add_separator()
colormap_buttons(group_tag, (global_min, global_max), subplot_tag, spectrogram_tags, group_tag=f"{group_tag}_buttons")
[docs]def plot_df_coherence(parameter_values,tab_tag,tags):
'''
Plots default coherence (welch method), computed in plot_coherence
:param parameter_values: Description
:param tab_tag: Description
:param tags: Description
'''
labels = [dpg.get_item_label(tag) for tag in tags]
signals = [dpg.get_value(tag)[:2] for tag in tags]
tab_label = f"{tab_tag}_df_coherence"
window_id = extract_window_id(tab_tag)
def plot():
labs = []
for no in ['0','1']:
tag = f"{tab_label}_s{no}"
lab = dpg.get_value(tag)
idx = labels.index(lab)
values = signals[idx]
signal = {}
signal['X'], signal['Y'] = values
signal['Label'] = lab
signal['Channel'] = lab[:lab.find(' ')]
# signal = getSignalbyIdx(window_id,tab_tag,tags[idx])
if no =='0': parameter_values['x'] = signal
else: parameter_values['y'] = signal
labs.append(lab)
if labs[0]==labs[1]:return
plot_coherence(parameter_values,tab_label)
combos = {}
with dpg.group(horizontal=True,parent=tab_label):
for i,s in enumerate(['Signal 1','Signal 2']):
input_id = dpg.add_combo(
label=s,
items=labels, # <-- customize your options
default_value='---',
callback=different_combo,
user_data=(tab_label, combos),
tag = f"{tab_label}_s{i}",
width=200
)
combos[s] = input_id
dpg.add_button(label='Plot coherence', callback=plot,width=200)
[docs]def plot_coherence(parameter_values, tab_label):
'''
Computes default coherence (welch)
:param parameter_values: Description
:param tab_label: Description
'''
if not dpg.does_item_exist(f"{tab_label}_clean"):
dpg.add_checkbox(label = 'Clean Previous Plot', parent=tab_label,default_value=True,tag=f"{tab_label}_clean")
signal1 = parameter_values['x']
signal2 = parameter_values['y']
parameter_values['x'] = signal1['Y']
parameter_values['y'] = signal2['Y']
parameter_values['fs'] = 250
clean = dpg.get_value(f"{tab_label}_clean")
if clean==True:
dpg.delete_item(f"{tab_label}_plot_group_text",children_only=True)
freq, coh = pre.call_function('coherence', parameter_values)
mask = freq <= 100
freq, coh = freq[mask], coh[mask]
# parameter_values['nfft'] = 512
p2 = parameter_values.copy()
_, pxy = pre.call_function('csd',parameter_values)
_, pxx = pre.call_function('welch',parameter_values)
p2['x'] = parameter_values['y']
_, pyy = pre.call_function('welch', p2)
im = pxy/np.sqrt(pxx*pyy)
ab = np.abs(im)[mask]
IMcoh = np.abs(np.imag(im)[mask])
if not dpg.does_item_exist(f"{tab_label}_plot_group"):
dpg.add_group(parent=tab_label, tag=f"{tab_label}_plot_group")
dpg.add_group(parent=f"{tab_label}_plot_group", tag=f"{tab_label}_plot_group_text")
# with dpg.group(parent=f"{tab_label}_plot_group", tag = f"{tab_label}_plot_group_text"):
dpg.add_text(f"Signal 1: {signal1['Label']}", parent=f"{tab_label}_plot_group_text")
dpg.add_text(f"Signal 2: {signal2['Label']}",parent=f"{tab_label}_plot_group_text")
plot_tag = f"{tab_label}_plot"
if clean:
with dpg.plot(label = 'Coherence', width=-1, height=400, tag=plot_tag, parent=f"{tab_label}_plot_group_text"):
dpg.bind_colormap(plot_tag, 0)
dpg.add_plot_legend(location=dpg.mvPlot_Location_NorthEast)
x_axis = dpg.add_plot_axis(dpg.mvXAxis, label="Frequency (Hz)") #tag=f"{tab_label}_x_axis"
y_axis = dpg.add_plot_axis(dpg.mvYAxis, label="Coherence", tag= f"{tab_label}_y_axis")
dpg.add_line_series(freq.tolist(), coh.tolist(), label=f"Coherence | {signal1['Channel']}-{signal2['Channel']}", parent=f"{tab_label}_y_axis")
dpg.add_line_series(freq.tolist(), IMcoh.tolist(), label=f"Imaginary Coherence | {signal1['Channel']}-{signal2['Channel']}", parent=f"{tab_label}_y_axis")
bands = getBandsByTab(tab_label)
if not dpg.does_item_exist(f"{tab_label}_header"):
dpg.add_collapsing_header(tag=f"{tab_label}_header",parent=tab_label,label='Extracted Features',default_open=False)
for b,band in bands.items():
p = {'X': freq, 'Y':coh}
info = ft.extract_psd_features(p,[b,band],bands=bands)
create_table(f"{tab_label}_header",f"{tab_label}_table", info, trial_label=f"{b} | {signal1['Channel']}-{signal2['Channel']}")
if not dpg.does_item_exist(f"{tab_label}_exp"):
with dpg.group(tag=f"{tab_label}_exp", parent=tab_label):
dpg.add_separator(label='Export')
b1 = dpg.add_button(label = 'Export Plot Figure',callback = filename_window,user_data =('2D', plot_tag, y_axis,0),width=-1)
b2 = dpg.add_button(label='Export Signals',width=-1,callback=filename_window,user_data=(y_axis,plot_tag,'2D'))
add_exporting_tag(extract_window_id(tab_label),plot=b1,signal=b2)
# time.sleep(0.2)
# dpg.set_axis_limits_auto(f"{tab_label}_y_axis")
[docs]def plot_multitaper_coherence(parameter_values,tab_tag,tags,flag=None):
'''
Computes and Plots multitaper coherence
:param parameter_values: Description
:param tab_tag: Description
:param tags: Description
:param flag: Description
'''
labels = [dpg.get_item_label(tag) for tag in tags]
if flag == None: #flag is None if default feature
tab_label = f"{tab_tag}_df_multitaper coherence"
else: tab_label = f"{tab_tag}_multitaper coherence"
window_id = extract_window_id(tab_tag)
bands = getBandsByTab(tab_tag)
def calculate():
if dpg.does_item_exist(f"{tab_label}_plot_group"):
dpg.delete_item(f"{tab_label}_plot_group",children_only=True)
if dpg.does_item_exist(f"{tab_label}_spec_group"):
dpg.delete_item(f"{tab_label}_spec_group")
dpg.delete_item(f"{tab_label}_progress_bar")
if flag==None:
for no in ['0','1']:
tag = f"{tab_label}_s{no}"
lab = dpg.get_value(tag)
idx = labels.index(lab)
signal = getSignalbyIdx(window_id,tab_tag,tags[idx])
if no =='0': signal1 = signal
else: signal2 = signal
else:
for s in ['x','y']:
idx = labels.index(parameter_values[s])
signal = getSignalbyIdx(window_id,tab_tag,tags[idx])
if s == 'x':
signal1 = signal
else: signal2= signal
if len(signal1['Y'])>len(signal2['Y']):
raw=signal2['Y']
else: raw = signal1['Y']
window_len = 512
if flag==None:
step = window_len // 8
nfft = window_len
kmax = 2*parameter_values['NW'] - 1
parameter_values['Kmax'] = kmax
parameter_values['M'] = window_len
else:
step = parameter_values['step']
nfft = parameter_values['nfft']
if step in [' ','']:
step =8
parameter_values['step'] = window_len//step
else: step = int(step)
if nfft in [' ','']: nfft = 256
else: nfft = int(nfft)
parameter_values['nfft'] = max(window_len,int(nfft))
parameter_values['M'] = window_len
tapers = pre.call_function('dpss', parameter_values, module = 'ssw')
Sxx_sum = np.zeros(nfft//2+1, dtype=np.complex128)
Syy_sum = np.zeros(nfft//2+1, dtype=np.complex128)
Sxy_sum = np.zeros(nfft//2+1, dtype=np.complex128)
n_windows = 0
coh_segments,imcoh_segments = [], []
for start in range(0, len(raw) - window_len + 1, step):
xw = signal1['Y'][start:start+window_len]
yw = signal2['Y'][start:start+window_len]
Sxx_tapers, Syy_tapers, Sxy_tapers = [], [], []
for taper in tapers:
S1 = np.fft.rfft(xw * taper, n=nfft)
S2 = np.fft.rfft(yw * taper, n=nfft)
# Sxx_tapers.append(np.abs(S1)**2)
# Syy_tapers.append(np.abs(S2)**2)
Sxx_tapers.append(S1*np.conj(S1))
Syy_tapers.append(S2*np.conj(S2))
Sxy_tapers.append(S1 * np.conj(S2))
Sxx_sum += np.mean(Sxx_tapers, axis=0)
Syy_sum += np.mean(Syy_tapers, axis=0)
Sxy_sum += np.mean(Sxy_tapers, axis=0)
n_windows += 1
coh_seg = np.abs(Sxy_sum)**2 / (Sxx_sum.real * Syy_sum.real)
imcoh_seg = np.abs(np.imag(Sxy_sum) / np.sqrt(Sxx_sum.real * Syy_sum.real))
coh_segments.append(coh_seg)
imcoh_segments.append(imcoh_seg)
#3rd
coh = np.mean(coh_segments, axis=0)
freq = np.fft.rfftfreq(nfft, d=1/250)
mask = freq <= 100
freq, coh = freq[mask], coh[mask]
#3rd
imcoh = np.mean(imcoh_segments, axis=0)
imcoh = imcoh[mask]
label=f"{signal1['Channel']}-{signal2['Channel']}"
plot(signal1,signal2,freq,coh,imcoh, label)
if not dpg.does_item_exist(f"{tab_label}_coh_spec"):
dpg.add_group(tag=f"{tab_label}_space",parent=tab_label)
dpg.add_separator(parent=f"{tab_label}_space")
dpg.add_button(label = 'Plot Coherogram',width=-1,tag=f"{tab_label}_coh_spec", callback=spectrogram, user_data = (signal1,signal2, tapers, freq, mask), parent=f"{tab_label}_space")
dpg.bind_item_theme(f"{tab_label}_coh_spec",change_button_color(3))
else:
dpg.set_item_user_data(f"{tab_label}_coh_spec", user_data=(signal1,signal2, tapers, freq, mask))
def plot(signal1,signal2,freq,coh,imcoh,label):
plot_tag = f"{tab_label}_plot"
if not dpg.does_item_exist(f"{tab_label}_plot_group"):
dpg.add_group(parent=tab_label, tag=f"{tab_label}_plot_group")
dpg.add_text(f"Signal 1: {signal1['Channel']} - {signal1['Date']}", parent=f"{tab_label}_plot_group")
dpg.add_text(f"Signal 2: {signal2['Channel']} - {signal2['Date']}", parent=f"{tab_label}_plot_group")
with dpg.plot(label = 'Multitaper Coherence', width=-1, height=400, tag=plot_tag, parent=f"{tab_label}_plot_group"):
dpg.bind_colormap(plot_tag, 0)
dpg.add_plot_legend(location=dpg.mvPlot_Location_NorthEast)
x_axis = dpg.add_plot_axis(dpg.mvXAxis, label="Frequency (Hz)") #tag=f"{tab_label}_x_axis"
y_axis = dpg.add_plot_axis(dpg.mvYAxis, label="Coherence", tag= f"{tab_label}_y_axis")
# freq, coh = freq.tolist(), coh.tolist()
dpg.add_line_series(freq.tolist(), coh.tolist(), label=f'Multitaper Coherence | {label}', parent=y_axis)
dpg.add_line_series(freq.tolist(), imcoh.tolist(), label=f'Imaginary Multitaper Coherence | {label}', parent=y_axis)
if not dpg.does_item_exist(f"{tab_label}_header"):
dpg.add_collapsing_header(tag=f"{tab_label}_header",label='Extracted Feautres',parent=f"{tab_label}_plot_group",default_open=False)
for b,band in bands.items():
p = {'X': freq, 'Y':coh}
info = ft.extract_psd_features(p,[b,band],bands=bands)
create_table(f"{tab_label}_header",f"{tab_label}_table", info, trial_label=f"{b} | {signal1['Channel']}-{signal2['Channel']}")
if not dpg.does_item_exist(f"{tab_label}_exp"):
with dpg.group(tag=f"{tab_label}_exp", parent=tab_label):
dpg.add_separator(label='Export')
b1 = dpg.add_button(label = 'Export Plot Figure',callback = filename_window,user_data =('2D', plot_tag, y_axis,0),width=-1)
b2 = dpg.add_button(label='Export Signals',width=-1,callback=filename_window,user_data=(y_axis,plot_tag,'2D'))
add_exporting_tag(extract_window_id(tab_tag),plot=b1,signal=b2)
def spectrogram(sender,app_data,user_data):
signal1,signal2, tapers, freqs, mask = user_data
if dpg.does_item_exist(f"{tab_label}_progress_bar"):
dpg.set_value(f"{tab_label}_progress_bar",0.0)
else: dpg.add_progress_bar(parent=tab_label, default_value=0.0, overlay='Coherogram Loading', tag = f"{tab_label}_progress_bar",width=-1)
if len(signal1['Y'])>len(signal2['Y']):
raw=signal2['Y']
else: raw = signal1['Y']
window_len = parameter_values['M']
if 'step' not in parameter_values.keys():
step = 8
nfft = max(window_len, 512)
step = window_len // step
else:
step = int(parameter_values['step'])
nfft = int(parameter_values['nfft'])
freqs = np.fft.rfftfreq(nfft, d=1/250)
mask = freqs <= 100
freqs = freqs[mask]
K = int(parameter_values['Kmax'])
starts = np.arange(0, len(raw)-window_len+1, step)
matrix = np.zeros((len(freqs), len(starts)))
for idx, s in enumerate(starts):
signal1w, signal2w = signal1['Y'][s:s+window_len], signal2['Y'][s:s+window_len]
Sxx = Syy = Sxy = 0
for tap in tapers:
X = np.fft.rfft(signal1w * tap, n=nfft)
Y = np.fft.rfft(signal2w * tap, n=nfft)
Sxx += np.abs(X)**2
Syy += np.abs(Y)**2
Sxy += X * np.conj(Y)
total = len(starts)
progress = (idx+1)/total
dpg.set_value(f"{tab_label}_progress_bar",np.round(progress,2))
Sxx /= K
Syy /= K
Sxy /= K
coh = np.abs(Sxy)**2 / (Sxx * Syy)
matrix[:, idx] = coh[mask]
matrix = np.flipud(np.array(matrix))
times = (starts + window_len/2) / 250
header_tag = f"{tab_label}_cot"
group_tag = f"{tab_label}_spec_group"
with dpg.group(parent=tab_label, tag=group_tag, horizontal=True):
dpg.add_colormap_scale(tag=f"{tab_label}_colormap_scale", label="Density (dB)", colormap=dpg.mvPlotColormap_Viridis,
min_scale = np.min(matrix), max_scale=np.max(matrix), height=400)
subplot_tag = f"{tab_label}_spec"
with dpg.plot(label='Coherogram', width=600, height=400, tag=subplot_tag):
x_axis = dpg.add_plot_axis(dpg.mvXAxis, label="Time (s)")
y_axis = dpg.add_plot_axis(dpg.mvYAxis, label="Frequency (Hz)")
heat = dpg.add_heat_series(matrix.flatten(), rows=len(freqs), cols=len(times),
scale_min=np.min(matrix), scale_max=np.max(matrix),
parent=y_axis, format="",bounds_min=[0,0], bounds_max=[int(len(signal1['Y'])/250),100])
dpg.bind_colormap(subplot_tag, dpg.mvPlotColormap_Viridis)
if dpg.does_item_exist(header_tag): dpg.delete_item(header_tag, children_only=True)
colormap_buttons(group_tag, (np.min(matrix), np.max(matrix)), subplot_tag, [heat], not_coherogram=False, group_tag=tab_label)
dpg.add_separator(label='Export',parent=tab_label)
b1 = dpg.add_button(label = 'Export Plot Figure',width=-1,parent=tab_label,callback = filename_window,user_data =('3DC', group_tag, y_axis,'viridis'))
b2 = dpg.add_button(label='Export Signals',width=-1,parent=tab_label,callback=filename_window,user_data=(f"{tab_label}_spec",group_tag,'3DC'))
add_exporting_tag(window_id,plot=b1,signal=b2)
with dpg.collapsing_header(tag=header_tag, parent=tab_label, label='Coherence over time'):
alpha_theme = create_alpha_theme()
with dpg.plot(label='Coherence over time',height=400, width=-1,tag=f"{header_tag}_plot"):
dpg.add_plot_legend()
x_axis = dpg.add_plot_axis(dpg.mvXAxis, label="Time (s)")
y_axis = dpg.add_plot_axis(dpg.mvYAxis, label="Power (dB)")
for b,v in bands.items():
mask = (freqs >= v[0]) & (freqs < v[1])
if np.any(mask):
power = np.flipud(matrix)[mask, :]
std = np.std(power, axis=0)
power = np.mean(power,axis=0)
min_std = power-std
max_std = power+std
c = dpg.add_shade_series(times,min_std,y2=max_std,label = f"{b} Std. Dev", parent=y_axis)
dpg.bind_item_theme(c, alpha_theme)
l = dpg.add_line_series(times,power,label = f"{b} Power", parent=y_axis)
dpg.bind_item_theme(l, change_line_thickness(3))
dpg.add_separator(label='Export',parent=header_tag)
b1 = dpg.add_button(label = 'Export Plot Figure',width=-1,parent=header_tag,callback = filename_window,user_data =('2D', f"{header_tag}_plot", y_axis,0))
b2 = dpg.add_button(label='Export Signals',width=-1,parent=header_tag,callback=filename_window,user_data=(y_axis,f"{header_tag}_plot",'2D'))
add_exporting_tag(window_id,plot=b1,signal=b2)
combos = {}
if flag == None: #Default
with dpg.group(horizontal=True,parent=tab_label):
for i,s in enumerate(['Signal 1','Signal 2']):
input_id = dpg.add_combo(
label=s,
items=labels, # <-- customize your options
default_value='---',
callback=different_combo,
user_data=(tab_label, combos),
tag = f"{tab_label}_s{i}",
width=200
)
combos[s] = input_id
dpg.add_button(label='Plot Coherence', callback=calculate,width=200,tag=f"{tab_label}_coh")
else:
calculate()
[docs]def plot_cross_correlation(parameter_values,tab_tag,tags, mode = None):
'''
Plots and computes cross-correlation between two signals, user-choosen, saved into keys 'a' and 'v' in parameter values.
:param parameter_values: Description
:param tab_tag: Description
:param tags: Description
:param mode: Description
'''
if mode:
tab_label = f"{tab_tag}_df_cross-correlation"
else: tab_label = f"{tab_tag}_cross-correlation"
labels = [dpg.get_item_label(tag) for tag in tags]
window_id = extract_window_id(tab_tag)
toggle_tags = []
def toggle(sender,app_data,user_data):
labs = [dpg.get_item_label(tag) for tag in toggle_tags]
name_band = dpg.get_item_label(sender)
for idx,l in enumerate(labs):
if name_band in l:
dpg.configure_item(toggle_tags[idx], show=app_data)
def plot():
nonlocal toggle_tags
toggle_tags.clear()
plot_mode = dpg.get_value(f"{tab_label}_band")
signal1, signal2 = None, None
if mode:
for i,key in enumerate(['a','v']):
lab = dpg.get_value(f"{tab_label}_s{i}")
try:
index = labels.index(lab)
parameter_values[f'label_{key}'] = lab
except Exception:
index = labels.index(parameter_values[f'label_{key}'])
signal = getSignalbyIdx(window_id,tab_tag,tags[index])
parameter_values[key] = signal['Y']
if key == 'a': signal1 = signal
else: signal2 = signal
else:
for i,key in enumerate(['a','v']):
lab = parameter_values[key]
try:
index = labels.index(lab)
parameter_values[f'label_{key}'] = lab
except Exception:
index = labels.index(parameter_values[f'label_{key}'])
signal = getSignalbyIdx(window_id,tab_tag,tags[index])
parameter_values[key] = signal['Y']
if key == 'a': signal1 = signal
else: signal2 = signal
if dpg.does_item_exist(f"{tab_label}_size_warning"):
dpg.delete_item(f"{tab_label}_size_warning")
# parameter_values['method'] = 'direct'
parameter_values['a'], parameter_values['v'] = np.array(parameter_values['a']).astype(np.float64), np.array(parameter_values['v']).astype(np.float64)
s1, s2 = parameter_values['a'], parameter_values['v']
if len(s1) == len(s2) and parameter_values['mode'] == 'valid':
parameter_values['mode'] = 'full'
dpg.add_text("Mode changed to 'full' because signals have the same length!", parent=tab_label, tag = f"{tab_label}_size_warning")
if dpg.does_item_exist(f"{tab_label}_cc"):
dpg.delete_item(f"{tab_label}_text",children_only=True)
dpg.delete_item(f"{tab_label}_cc",children_only=True)
else:
dpg.add_group(tag=f"{tab_label}_cc", parent=tab_label, horizontal=True)
dpg.add_group(tag=f"{tab_label}_text", parent=tab_label)
dpg.add_text(f"Signal 1: {signal1['Channel']} - {signal1['Date']}",parent=f"{tab_label}_text")
dpg.add_text(f"Signal 2: {signal2['Channel']} - {signal2['Date']}",parent=f"{tab_label}_text")
plot_tag = f"{tab_label}_ccplot"
with dpg.plot(label="Cross-correlation", height=300, width=800, tag = plot_tag, parent=f"{tab_label}_cc"):
dpg.bind_colormap(plot_tag, 0)
dpg.add_plot_legend()
cross_x = dpg.add_plot_axis(dpg.mvXAxis, label="Time (s)")
dpg.add_plot_axis(dpg.mvYAxis, label="Cross-Correlation Ratio", tag = f"{tab_label}_cc_y")
cross_y = f"{tab_label}_cc_y"
if dpg.does_item_exist(f"{tab_label}_cc_pb"):
dpg.set_value(f"{tab_label}_cc_pb", 0)
else:
dpg.add_progress_bar(parent=tab_label, overlay='Loading',width=-1, tag = f"{tab_label}_cc_pb")
if plot_mode:
bands = getBandsByTab(tab_tag)
else:
bands = {'Bands': 'b'}
d = parameter_values['dislocation']
if d == ' ' or d == '': d = 1
else: d = int(d)
shape = parameter_values['mode']
normalize = parameter_values['normalization']
if normalize == ' ' or normalize == '': normalize = 'False'
progress_value = 0
for b, band in bands.items():
for key in ['a','v']:
if not isinstance(band,str):
param_filter = {'N': '4', 'Wn': band,'btype':'band','fs': '250'}
args = pre.call_function('butter', param_filter)
parameter_values[key] = pre.call_function2('filtfilt', *args, parameter_values[key])
correlation =pre.call_function('correlate', parameter_values, module='np')
N, D = len(signal1['Y']), len(signal2['Y'])
match shape:
case 'full':
dislocation = np.arange(-(D-1), N) / 250
# dislocation = np.arange((-len(signal1['Y'])+d)/250,(len(signal1['Y'])/250),1/250)
case 'same': dislocation = (np.arange(N) - D//2) / 250
case 'valid': dislocation = np.arange(N-D+1) / 250
if normalize == 'True':
norm = np.linalg.norm(parameter_values['a']) * np.linalg.norm(parameter_values['v'])
correlation = correlation/norm
if plot_mode:
label = f"{b} Cross-Correlation"
info_mode = b
if not dpg.does_item_exist(f"{tab_label}_cc_buttons"):
dpg.add_group(tag=f"{tab_label}_cc_buttons", parent = f"{tab_label}_cc", horizontal=False)
dpg.add_checkbox(label=b,parent=f"{tab_label}_cc_buttons",default_value=True,callback=toggle)
else:
label = "Cross-correlation"
info_mode = None
step = 1/(len(bands.keys())*2)
dpg.add_line_series(dislocation.tolist(), correlation.tolist(),label=label, parent=cross_y,tag=f"{tab_label}_{label}")
progress_value = progress_value + step
dpg.set_value(f"{tab_label}_cc_pb", progress_value)
toggle_tags.append(f"{tab_label}_{label}")
p = {'Y': correlation.tolist(),'X':dislocation.tolist()}
info, line_tags = extract_cc_features(p,cross_y,tab_label,info_mode)
progress_value = progress_value + step
dpg.set_value(f"{tab_label}_cc_pb", progress_value)
label = f"{signal1['Channel']} - {signal2['Channel']}"
toggle_tags = toggle_tags + line_tags
if not dpg.does_item_exist(f"{tab_label}_header"):
dpg.add_collapsing_header(tag = f"{tab_label}_header", parent=tab_label, label='Features')
create_table(f"{tab_label}_header",f"{tab_label}_table",info,f"{b} | {label}")
if not dpg.does_item_exist(f"{tab_label}_export_group"):
with dpg.group(tag=f"{tab_label}_export_group",parent=tab_label):
dpg.add_separator(label='Export',parent=tab_label)
b1 = dpg.add_button(label = 'Export Plot Figure',parent=tab_label,width=-1,callback = filename_window,user_data =('CC', plot_tag, cross_y,0))
b2 = dpg.add_button(label='Export Signals',width=-1,callback=filename_window,parent=tab_label,user_data=(cross_y,plot_tag,'CC'))
add_exporting_tag(extract_window_id(tab_tag),plot=b1,signal=b2)
if not dpg.does_item_exist(f"{tab_label}_band"):
dpg.add_checkbox(label='Band Cross-Correlation', parent=tab_label, tag = f"{tab_label}_band") #, callback=plot)
if mode==None:
plot()
else: #Default
combos = {}
with dpg.group(horizontal=True,parent=tab_label):
parameter_values['mode'] = 'full'
parameter_values['dislocation'] = '1'
parameter_values['normalization'] = 'True'
for i,s in enumerate(['Signal 1','Signal 2']):
input_id = dpg.add_combo(
label=s,
items=labels, # <-- customize your options
default_value='---',
tag = f"{tab_label}_s{i}",
width=200
# callback=different_combo,
# user_data=(tab_label, combos),
)
combos[s] = input_id
dpg.add_button(label='Plot Cross-Correlation', callback=plot,width=200)
[docs]def set_colormap_range(sender,app_data,user_data):
'''
Resets colormap range of spectrogram, of colormap and reconfigures spectrograms to same interval.
tab_label: str/int, tab id
spec_tags: list, spectrogram's series' id
colormap_tag: str/int, colormap id
:param sender: Description
:param app_data: Description
:param user_data: tab_label, spec_tags, colormap_tag
'''
tab_label, spec_tags, colormap_tag = user_data
min_value = dpg.get_value(f"{tab_label}_emin")
max_value = dpg.get_value(f"{tab_label}_emax")
dpg.configure_item(colormap_tag,min_scale=min_value,max_scale=max_value)
for spec in spec_tags:
dpg.configure_item(spec,scale_max=max_value,scale_min=min_value)
[docs]def reset_colormap(sender,app_data,user_data):
mother, spec_tags, colormap_scale = user_data
e_min, e_max = f"{mother}_emin", f"{mother}_emax"
mother, min_value, max_value = dpg.get_item_user_data(e_min)
dpg.set_value(e_min, min_value)
dpg.set_value(e_max, max_value)
dpg.configure_item(colormap_scale,min_scale=min_value,max_scale=max_value)
for spec in spec_tags:
dpg.configure_item(spec,scale_max=max_value,scale_min=min_value)
[docs]def plot_spectrogram_og(tags, tab_label, func, tab_tag, parameter_values, mode = 'segment'):
'''
Computes and plots spectrograms individually, into subplots, under one colormap.
:param tags: Description
:param tab_label: Description
:param func: Description
:param tab_tag: Description
:param parameter_values: Description
:param mode: Description
'''
window_id = extract_window_id(tab_tag)
wt = extract_type_window(window_id)
if wt == 'Individual': mode = None
num_plots = len(tags)
num_cols = 2
num_rows = (num_plots + 1) // num_cols
global_min = 0
global_max = 1
Merge = []
per_file = {}
per_tag = {}
parameter_values['fs'] = 250
spectrogram_tags = []
for i, tag in enumerate(tags):
stream = getSignalbyIdx(window_id, tab_tag, tag, mode=mode)
file = extract_file_from_series_tag(tag)
parameter_values['x'] = np.array(stream['Y'])
signal = pre.call_function(func, parameter_values)
frequencies, times, Sxx = signal
Sxx_log = 10 * np.log10(Sxx + 1e-10) # Convert to dB scale
Sxx_log = np.flipud(Sxx_log)
global_min = min(global_min, Sxx_log.min())
global_max = max(global_max, Sxx_log.max())
signal = frequencies, times, Sxx_log
if file in per_file.keys():
per_file[file][tag] = signal
else:
per_file[file] = {tag: signal}
group_tag = f"{tab_label}_plot_group"
with dpg.group(horizontal=True, parent=tab_label, tag=group_tag):
dpg.add_colormap_scale(tag=f"{group_tag}_colormap_scale", label="Density (dB)", colormap=dpg.mvPlotColormap_Jet,
min_scale = global_min, max_scale=global_max, height=400*num_rows)
subplot_tag = f"{tab_label}_subplots"
with dpg.subplots(rows=num_rows, columns=num_cols, label="Spectrograms", width=800, height=400*num_rows, tag=subplot_tag):
for i,tag in enumerate(tags):
stream = getSignalbyIdx(window_id, tab_tag, tag, mode=mode)
stream_events = getEventsbyTag(tab_tag,tag)
file = extract_file_from_series_tag(tag)
y = stream['Y']
channel = getUpdatedElectrodeName(stream['Channel'])
date = stream['Date']
signal = per_file[file][tag]
Merge.append(signal)
frequencies, times, Sxx_log = signal
# Create a new subplot for each heat series
if mode and wt == 'Combined':
plot_label = f"{channel} | {stream['Type']} - {date}"
plot_tag = f"{tab_label}_{channel}_{stream['Type']}_{i}"
else:
plot_label = f"{channel} | {date}"
plot_tag = f"{tab_label}_{channel}_{date}"
with dpg.plot(label=plot_label, width=-1, height=-1, tag = plot_tag):
dpg.add_plot_legend()
x_axis = dpg.add_plot_axis(dpg.mvXAxis, label="Time (s)")
y_axis = dpg.add_plot_axis(dpg.mvYAxis, label="Frequency (Hz)")
if mode and wt== 'Combined':
emin = -int(dpg.get_value(f"{tab_tag}_emin"))
emax = int(dpg.get_value(f"{tab_tag}_emax"))
else:
emin = 0
emax = len(y)/250
per_tag[tag] = [Sxx_log,plot_tag, plot_label, stream_events, (emin, emax)]
t = dpg.add_heat_series(Sxx_log.flatten(), rows=Sxx_log.shape[0], cols=Sxx_log.shape[1],
scale_min=global_min, scale_max=global_max,
parent=y_axis, format="", bounds_min=[emin,0], bounds_max=[emax,125])
spectrogram_tags.append(t)
dpg.bind_colormap(plot_tag, dpg.mvPlotColormap_Jet)
if stream_events != None and wt == 'Individual':
mode = 'segment' #why??
for colorIdx, event in enumerate(stream_events):
# stamp = [event['Idx']/(len(times)*250)] #spectrogram axis is in reality from 0 to 1, and the columns are time based, not sample
stamp = [event['Idx']/250]
type = event['Type']
if stamp[0] <= Sxx_log.shape[1]:
dpg.add_plot_legend()
inf = dpg.add_inf_line_series(x=stamp,label=f'{type}',parent=y_axis)
color = change_inf_line_color(colorIdx)
dpg.bind_item_theme(inf, color)
dpg.configure_item(item=f"{group_tag}_colormap_scale", min_scale=global_min, max_scale=global_max)
def link(sender, app_data):
dpg.configure_item(f"{tab_label}_subplots", link_all_x=app_data, link_all_y=app_data)
def spectrograms_power_over_time(sender,app_data,user_data):
bands = getBandsByTab(tab_tag)
if dpg.does_item_exist(f"{tab_label}_merged"): dpg.delete_item(f"{tab_label}_merged")
with dpg.group(parent=tab_label, tag = f"{tab_label}_merged"):
alpha_theme = create_alpha_theme()
dpg.add_text('Band Power over Time', parent=f"{tab_label}_merged")
dpg.add_separator(parent=f"{tab_label}_merged")
for _, value in per_tag.items():
sxx, plot_tag, plot_label, stream_events, time = value
header_tag = f"{plot_tag}_header"
new_plot_tag = f"{plot_tag}_overtime"
if dpg.does_item_exist(header_tag): dpg.delete_item(header_tag, children_only=True)
with dpg.collapsing_header(tag=header_tag, parent=f"{tab_label}_merged", label=plot_label):
with dpg.plot(label=plot_label, width=-1,tag=new_plot_tag):
dpg.add_plot_legend()
x_axis = dpg.add_plot_axis(dpg.mvXAxis, label="Time (s)")
y_axis = dpg.add_plot_axis(dpg.mvYAxis, label="Power (dB)")
for b,v in bands.items():
mask = (frequencies >= v[0]) & (frequencies < v[1])
if np.any(mask):
power = np.flipud(sxx)[mask, :]
power = np.mean(power,axis=0)
std = np.std(sxx[mask, :], axis=0)
min_std = power-std
max_std = power+std
emin, emax = time
# time_array = np.arange(emin,emax,1/len(power))
# time_array = np.linspace(emin,emax,int(-emin+emax+1))
time_array = np.linspace(emin,emax,len(power))
dpg.add_shade_series(time_array,min_std,y2=max_std,label = f"{b} Std. Deviation", tag=f"{plot_tag}_{b}_std", parent=y_axis)
dpg.bind_item_theme(f"{plot_tag}_{b}_std", alpha_theme)
l = dpg.add_line_series(time_array,power,label = f"{b} Power", tag=f"{plot_tag}_{b}_mean", parent=y_axis)
dpg.bind_item_theme(l, change_line_thickness(3))
if stream_events != None and wt == 'Individual':
for event in stream_events:
# stamp = [event['Idx']/(len(times)*250)] #spectrogram axis is in reality from 0 to 1, and the columns are time based, not sample
stamp = [event['Idx']/250]
type = event['Type']
if stamp[0] <= Sxx_log.shape[1]:
dpg.add_plot_legend()
dpg.add_inf_line_series(x=stamp,label=f'{type}',parent=y_axis)
b1 = dpg.add_button(label = 'Export Plot Figure',callback = filename_window,user_data =('2D', new_plot_tag, y_axis,0),width=-1)
b2 = dpg.add_button(label='Export Signals',width=-1,callback=filename_window,user_data=(y_axis,new_plot_tag,'2D'))
add_exporting_tag(window_id,plot=b1,signal=b2)
def mean_spectrogram(sender,app_data,user_data):
if dpg.does_item_exist(f"{tab_label}_mean_spec"): dpg.delete_item(f"{tab_label}_mean_spec")
all_Sxx = user_data
all_Sxx = [s[2] for s in all_Sxx]
shapes = [s.shape for s in all_Sxx]
if not all(shape == shapes[0] for shape in shapes):
e = "All spectrograms must have the same time length to compute the mean."
t = dpg.add_text(e, parent=tab_label)
dpg.bind_item_theme(t,change_text_color(1))
return
# Compute the mean spectrogram (linear scale)
mean_Sxx_log = np.nanmean(np.stack(all_Sxx), axis=0)
title = "Mean Spectrogram"
subplot_tag = f"{tab_label}_mean"
mother = f"{tab_label}_mean_group"
with dpg.collapsing_header(label='Mean Spectrogram', parent=tab_label, tag = f"{tab_label}_mean_spec"):
with dpg.group(horizontal=True,tag=mother):
dpg.add_colormap_scale(tag=f"{mother}_colormap_scale", label="Density (dB)", colormap=dpg.mvPlotColormap_Jet,
min_scale = global_min, max_scale=global_max, height=400)
with dpg.plot(label=title, width=500, height=400, tag=subplot_tag):
x_axis = dpg.add_plot_axis(dpg.mvXAxis, label="Time (s)")
y_axis = dpg.add_plot_axis(dpg.mvYAxis, label="Frequency (Hz)")
if mode and wt== 'Combined':
emin = -int(dpg.get_value(f"{tab_tag}_emin"))
emax = int(dpg.get_value(f"{tab_tag}_emax"))
else:
emin = 0
emax = len(y)/250
mean_s = dpg.add_heat_series(mean_Sxx_log.flatten(), rows=mean_Sxx_log.shape[0], cols=mean_Sxx_log.shape[1],
scale_min=global_min, scale_max=global_max,
parent=y_axis, format="", bounds_min=[emin,0], bounds_max=[emax,125])
dpg.bind_colormap(subplot_tag, dpg.mvPlotColormap_Jet)
colormap_buttons(mother,(global_min,global_max),subplot_tag, [mean_s])
with dpg.group(horizontal=False, tag = f"{group_tag}_buttons"):
dpg.add_checkbox(label='Synchronize Axis', callback=link)
dpg.add_button(label='Band Power over Time', callback=spectrograms_power_over_time,width=-1)
dpg.add_button(label='Mean Spectrogram', callback=mean_spectrogram,width=-1, user_data=Merge)
colormap_buttons(group_tag, (global_min, global_max), subplot_tag, spectrogram_tags, group_tag=f"{group_tag}_buttons")
[docs]def choose(sender,app_data,user_data):
'''
Buttons to choose between individual or joined spectrogram, or wavelet.
Only appears in the case of there being more than 1 recording from the same channel
tags: list, signals' id
tab_label: str/id, tab id
func: str, function name
tab_tab: str/id, tab parent id
parameter_values: dict, key: name of parameter, item: value
:param sender: Description
:param app_data: Description
:param user_data: tags, tab_label, func, tab_tag, parameter_values, mode
'''
tags, tab_label, func, tab_tag, parameter_values, mode = user_data
window_id = extract_window_id(tab_tag)
wt = extract_type_window(window_id)
continue_flag = True
#Mode to get signal - if event or full signal
if wt == 'Individual': mode = None
elif mode == None:
# with dpg.child_window(parent=tab_label):
# dpg.add_text(f"{func} only available in Combined Analysis for Event-lock based signals", show= True)
continue_flag = True
if continue_flag == True:
events = [getEventsbyTag(tab_tag,serie) for serie in tags]
if all(e is None for e in events): mode = None
else: mode = 'segment'
#Mode to plot: joined or separate
plot_mode = app_data
if plot_mode == 'Join Recordings': plot_mode = 'join'
else: plot_mode = 'separate'
#erase group if it already exists
if dpg.does_item_exist(f"{tab_label}_plot_group"): dpg.delete_item(f"{tab_label}_plot_group")
if dpg.does_item_exist(f"{tab_label}_merged"): dpg.delete_item(f"{tab_label}_merged")
if plot_mode == 'join':
#plot per recording + possibility of merge all
if func == 'spectrogram': plot_spectrogram_joined(tags, tab_label, func, tab_tag, parameter_values, mode)
else: plot_wavelet_joined(tags, tab_label, func, tab_tag, parameter_values, mode)
else:
#plot per channel
if func == 'spectrogram': plot_spectrogram_og(tags, tab_label, func, tab_tag, parameter_values, mode)
else: plot_wavelet(tags, tab_label, func, tab_tag, parameter_values, mode)
[docs]def plot_wavelet_joined(tags, tab_label, func, tab_tag, parameter_values, mode = None):
'''
Plots wavelet, computed for all recordings joined by channel.
:param tags: Description
:param tab_label: Description
:param func: Description
:param tab_tag: Description
:param parameter_values: Description
:param mode: default to None
'''
window_id = extract_window_id(tab_tag)
wt = extract_type_window(window_id)
new_signal, identifiers, event_names = {}, {}, []
'''Initializing new signal dictionary'''
for tag in tags:
signal = getSignalbyIdx(window_id,tab_tag,tag,mode)
ch = signal['Channel']
if ch in electrodes_names.keys():
ch = electrodes_names[ch]
else:
electrodes_names[ch] = ch
if func in signal.keys(): signal.pop(func)
temp = {key: np.array([]) for key in signal.keys()}
temp['dX'] = []
if ch not in new_signal.keys(): new_signal[ch] = temp
else: new_signal[ch] = new_signal[ch] | temp
'''Filling new signal dictionary'''
prev_end_time = {}
current_start = {}
for tag in tags:
signal = getSignalbyIdx(window_id,tab_tag,tag,mode)
X = signal['X']
arr = np.array(X)
if np.issubdtype(arr.dtype, np.number):
valid = arr[~np.isnan(arr)]
if len(valid) > 0:# and np.round(valid[0]) == 0:
signal['dX'] = [datetime.fromtimestamp(val) for val in X]
else:
signal['dX'] = [pre.get_date_from_ts(val) for val in X]
ch = getUpdatedElectrodeName(signal['Channel'])
prev = len(new_signal[ch]['X'])
current_start[ch] = signal['dX'][0]
if ch in prev_end_time:
gap = current_start[ch] - prev_end_time[ch]
if gap.total_seconds() > 0:
# Limit the gap to 10 min
gap = min(gap, timedelta(seconds=15))
# Determine how many points to fill (based on sampling rate)
sample_rate = 250
n_fill = int(gap.total_seconds() * sample_rate)
if n_fill > 0:
# Generate filler time and zero signal
filler_dX = [prev_end_time[ch] + timedelta(seconds=i / sample_rate) for i in range(n_fill)]
filler_Y = np.zeros(n_fill)
# Append the filler
new_signal[ch]['dX'] = np.append(new_signal[ch]['dX'], filler_dX)
new_signal[ch]['Y'] = np.append(new_signal[ch]['Y'], filler_Y)
else: prev_end_time[ch] = None
for key in signal.keys():
if key == 'dX' or key == 'Y':
new_signal[ch][key] = np.append(new_signal[ch][key], np.array(signal[key]))
identifiers[ch] = len(signal[key])
elif key == 'Events':
for e in signal[key]:
event_names.append(e['Type'])
e['sIdx'] = e['Idx'] + prev
new_signal[ch][key] = np.append(new_signal[ch][key], signal[key])
else:
new_signal[ch][key] = np.append(new_signal[ch][key], signal[key])
prev_end_time[ch] = signal['dX'][-1]
num_plots = len(new_signal.keys())
num_cols = 1
num_rows = num_plots
global_min, global_max = 0,0
per_tag, wavelet_tags = {}, []
bands = getBandsByTab(tab_tag)
event_names = np.unique(event_names).tolist()
group_tag = f"{tab_label}_plot_group"
with dpg.group(horizontal=True, parent=tab_label, tag=group_tag):
dpg.add_colormap_scale(tag=f"{group_tag}_colormap_scale", label="Density (dB)", colormap=dpg.mvPlotColormap_Jet,
min_scale = global_min, max_scale=global_max, height=400*num_rows)
with dpg.subplots(rows=num_rows, columns=num_cols, label="Wavelet", width=800, height=400*num_rows, tag = f"{tab_label}_subplots"):
for channel in new_signal:
title =f"Joined Wavelet - {channel}"
subplot_tag = f"{tab_label}_joined_{channel}"
with dpg.plot(label=title, width=800, height=400, tag=subplot_tag, parent=f"{tab_label}_subplots"):
x_axis = dpg.add_plot_axis(dpg.mvXAxis, label="Time (DateTime)", scale= dpg.mvPlotScale_Time)
y_axis = dpg.add_plot_axis(dpg.mvYAxis, label="Frequency (Hz)")
y = new_signal[channel]['Y']
if current_start == {}:
x = new_signal[channel]['dX']
if isinstance(x[0], (int, float)): x = [pre.get_date_from_ts(val) for val in x]
x = [(xx + timedelta(hours=1)).timestamp() for xx in x]
else:
x = np.arange(0,len(y)/250,1)
dpg.configure_item(x_axis, scale = dpg.mvPlotScale_Linear)
try: events = new_signal[channel]['Events']
except Exception as e: events = 'None'
signal = None
try:
signal = apply_feat(func,parameter_values,y)
updateSignalbyIdx(signal,tab_tag,tag,func)
except Exception as e: print(e)
tfr = 10 * np.log10(signal + 1e-8)
tfr = np.flipud(tfr[0, 0, :, :])
downsampled_tfr = tfr[:, ::5]
heatmap_data = downsampled_tfr.flatten().tolist()
per_tag[channel] = [downsampled_tfr,subplot_tag, title, events, x]
global_min = min(global_min, np.min(downsampled_tfr))
global_max = max(global_max, np.max(downsampled_tfr))
tag = dpg.add_heat_series(heatmap_data, rows=downsampled_tfr.shape[0], cols=downsampled_tfr.shape[1],
scale_min=global_min, scale_max=global_max,
parent=y_axis, format="",bounds_min=[x[0],0], bounds_max=[x[-1],100])
wavelet_tags.append(tag)
dpg.bind_colormap(subplot_tag, dpg.mvPlotColormap_Jet)
if not isinstance(events,str):
dpg.add_plot_legend()
for event in events:
stamp = event['sIdx']/250 #spectrogram axis is in reality from 0 to 1, and the columns are time based, not sample
name = event['Type']
if stamp <= x[-1]:
ev = dpg.add_inf_line_series(x=[stamp],parent=y_axis,label = getUpdatedEventName(name))
dpg.bind_item_theme(ev,change_inf_line_color(event_names.index(name)))
dpg.configure_item(f"{group_tag}_colormap_scale", min_scale = global_min, max_scale=global_max)
def link(sender, app_data):
dpg.configure_item(f"{tab_label}_subplots", link_all_x=app_data, link_all_y=app_data)
def mean_wavelet(channels):
# mean_Sxx = np.mean(np.stack(channels), axis=0)
max_time = max(ch.shape[1] for ch in channels)
num_freqs = channels[0].shape[0]
for i,ch in enumerate(channels):
pad_width = max_time - ch.shape[1]
if pad_width > 0:
pad_array = np.full((num_freqs, pad_width), np.nan)
channels[i] = np.hstack([ch, pad_array])
stacked = np.stack(channels, axis=0) # shape: (channels, freqs, time)
mean_Sxx = np.nanmean(stacked, axis=0)
with dpg.plot(label='Mean Wavelet', width=-1, height=400, tag=f"{tab_label}_join_mean", parent=f"{tab_label}_merged"):
x_axis = dpg.add_plot_axis(dpg.mvXAxis, label="Time (s)")
y_axis = dpg.add_plot_axis(dpg.mvYAxis, label="Frequency (Hz)")
if mode and wt=='Combined':
emin = -int(dpg.get_value(f"{tab_tag}_emin"))
emax = int(dpg.get_value(f"{tab_tag}_emax"))
else:
emin, emax = 0, len(x)*5/250 #5 is downsampling factor, 250 is fs
dpg.add_heat_series(mean_Sxx.flatten(), rows=mean_Sxx.shape[0], cols=mean_Sxx.shape[1],
scale_min=global_min, scale_max=global_max,
parent=y_axis, format="", bounds_min=[emin,0], bounds_max=[emax,num_freqs])
dpg.bind_colormap(f"{tab_label}_join_mean", dpg.mvPlotColormap_Jet)
if not isinstance(events,str):
for event in events:
stamp = event['Idx']/250 #spectrogram axis is in reality from 0 to 1, and the columns are time based, not sample
if stamp <= emax:
type = event['Type']
dpg.add_plot_legend()
dpg.add_inf_line_series(x=[stamp],label=f'{type}',parent=y_axis)
def merge_wavelet():
if dpg.does_item_exist(f"{tab_label}_merged"): dpg.delete_item(f"{tab_label}_merged")
with dpg.group(parent=tab_label, tag = f"{tab_label}_merged"):
for _, value in per_tag.items():
sxx, plot_tag, plot_label, events, time = value
header_tag = f"{plot_tag}_header"
if dpg.does_item_exist(header_tag): dpg.delete_item(header_tag, children_only=True)
with dpg.collapsing_header(tag=header_tag, parent=f"{tab_label}_merged", label=plot_label):
with dpg.plot(label= f"Bands Power over Time - {_}",width=-1,tag=f"{header_tag}_plot"):
dpg.add_plot_legend()
x_axis = dpg.add_plot_axis(dpg.mvXAxis, label="Time (s)")
y_axis = dpg.add_plot_axis(dpg.mvYAxis, label="Power (dB)")
alpha_theme = create_alpha_theme()
for b,v in bands.items():
frequencies = parameter_values['freqs']
mask = (frequencies >= v[0]) & (frequencies < v[1])
if np.any(mask):
power = np.mean(sxx[mask, :],axis=0) # mean across freqs -> shape: (time,)std = np.std(sxx[mask, :], axis=0)
std = np.std(sxx[mask, :],axis=0)
min_std = power-std
max_std = power+std
x_time = np.linspace(0,max(time),len(power))
dpg.add_shade_series(x_time,min_std,y2=max_std,label = f"{b} Std. Deviation", tag=f"{plot_tag}_{b}_std", parent=y_axis)
dpg.bind_item_theme(f"{plot_tag}_{b}_std", alpha_theme)
dpg.add_line_series(x_time,power,label = f"{b} Mean", tag=f"{plot_tag}_{b}_mean", parent=y_axis)
if not isinstance(events,str):
dpg.add_plot_legend()
for event in events:
stamp = event['sIdx']/250 #spectrogram axis is in reality from 0 to 1, and the columns are time based, not sample
name = event['Type']
if stamp <= time[-1]:
ev = dpg.add_inf_line_series(x=[stamp],parent=y_axis,label = getUpdatedEventName(name))
dpg.bind_item_theme(ev,change_inf_line_color(event_names.index(name)))
b1 = dpg.add_button(label = 'Export Plot Figure',callback = filename_window,user_data =('2D', f"{header_tag}_plot", y_axis,0),width=-1,parent=header_tag)
b2 = dpg.add_button(label='Export Signals',width=-1,callback=filename_window,user_data=(y_axis,f"{header_tag}_plot",'2D'),parent=header_tag)
add_exporting_tag(window_id,plot=b1,signal=b2)
with dpg.group(horizontal=False, tag = f"{group_tag}_buttons"):
dpg.add_checkbox(label='Synchronize Axis', callback=link)
dpg.add_button(label='Band Power over Time', callback=merge_wavelet)
dpg.add_separator()
colormap_buttons(group_tag, (global_min, global_max), subplot_tag, wavelet_tags, group_tag=f"{group_tag}_buttons")
[docs]def plot_wavelet(tags, tab_label, func, tab_tag, parameter_values, mode = None):
'''
Plots wavelet, computed for all recordings individually.
:param tags: Description
:param tab_label: Description
:param func: Description
:param tab_tag: Description
:param parameter_values: Description
:param mode: default to None
'''
window_id = extract_window_id(tab_tag)
wt = extract_type_window(window_id)
num_plots = len(tags)
num_cols = 2
num_rows = (num_plots + 1) // num_cols
events = None
per_tag, wavelet_tags, event_names = {}, [], []
bands = getBandsByTab(tab_tag)
global_min,global_max = 0,1
group_tag = f"{tab_label}_plot_group"
with dpg.group(horizontal=True,tag = group_tag, parent=tab_label):
dpg.add_colormap_scale(tag=f"{group_tag}_colormap_scale", label="Density (dB)", colormap=dpg.mvPlotColormap_Jet,
min_scale=global_min, max_scale=global_max, height=400*num_rows)
subplot_tag = f"{tab_label}_subplots"
with dpg.subplots(rows=num_rows, columns=num_cols, label=f"Wavelet", width=800, height=400*num_rows, tag=subplot_tag):
for idx, tag in enumerate(tags):
stream = getSignalbyIdx(window_id,tab_tag,tag,mode=mode)
if mode == None: #else the stream is the event
try:
events = getEventsbyTag(tab_tag, tag)
except Exception as e: events = 'None'
y = stream['Y']
channel = stream['Channel']
date = stream['Date']
signal = None
try:
signal = apply_feat(func,parameter_values,y)
except Exception as e: print(e,': ',parameter_values['epoch_data'])
if mode and wt=='Combined': #Plotting the events
date = date + timedelta(seconds=int(stream['Idx']/250))
plot_label = f"{channel} - {stream['Type']} - {date}"
plot_tag = f"{tab_label}_{channel}_{stream['Type']}_{idx}"
else:
plot_label = f"{channel} | {date}"
plot_tag = f"{tab_label}_{channel}_{date}_{idx}"
with dpg.plot(label = plot_label, width=-1, height=-1, tag =plot_tag):
dpg.add_plot_legend()
x_axis = dpg.add_plot_axis(dpg.mvXAxis, label="Time (s)")
y_axis = dpg.add_plot_axis(dpg.mvYAxis, label="Frequency (Hz)")
freqs = parameter_values['freqs']
tfr = 10 * np.log10(signal)
tfr = np.flipud(tfr[0, 0, :, :])
if mode and wt== 'Combined':
emin = -int(dpg.get_value(f"{tab_tag}_emin"))
emax = int(dpg.get_value(f"{tab_tag}_emax"))
else:
emin = 0
emax = len(y)/250
downsample = 2*int(emax/13)
downsampled_tfr = tfr[:, ::downsample]
per_tag[tag] = [downsampled_tfr,plot_tag, plot_label, events, (emin,emax)]
heatmap_data = downsampled_tfr.flatten().tolist()
global_min = min(global_min,np.min(downsampled_tfr))
global_max = max(global_max,np.max(downsampled_tfr))
# Add downsampled heatmap series
tag = dpg.add_heat_series(heatmap_data, rows=downsampled_tfr.shape[0], cols=downsampled_tfr.shape[1],
parent=y_axis, scale_min=global_min, scale_max=global_max, format="", bounds_min=[emin,freqs[0]], bounds_max=[emax,freqs[-1]])
wavelet_tags.append(tag)
dpg.bind_colormap(plot_tag, dpg.mvPlotColormap_Jet)
try: events = stream['Events']
except Exception as e: events = 'None'
if not isinstance(events,str):
for event in events:
stamp = event['Idx']/250 #spectrogram axis is in reality from 0 to 1, and the columns are time based, not sample
name = event['Type']
if name not in event_names: event_names.append(name)
if stamp <= stream['X'][-1]:
ev = dpg.add_inf_line_series(x=[stamp],parent=y_axis,label = getUpdatedEventName(name))
dpg.bind_item_theme(ev,change_inf_line_color(event_names.index(name)))
dpg.configure_item(f"{group_tag}_colormap_scale",min_scale=global_min,max_scale=global_max)
def link(sender, app_data):
dpg.configure_item(f"{tab_label}_subplots", link_all_x=app_data, link_all_y=app_data)
def merge_wavelet():
if dpg.does_item_exist(f"{tab_label}_merged"): dpg.delete_item(f"{tab_label}_merged")
with dpg.group(parent=tab_label, tag = f"{tab_label}_merged"):
for _, value in per_tag.items():
sxx, plot_tag, plot_label, events, time = value
header_tag = f"{plot_tag}_header"
if dpg.does_item_exist(header_tag): dpg.delete_item(header_tag, children_only=True)
with dpg.collapsing_header(tag=header_tag, parent=f"{tab_label}_merged", label=plot_label):
with dpg.plot(label= f"Bands Power over Time - {_}",width=-1,tag=f"{header_tag}_plot"):
dpg.add_plot_legend()
x_axis = dpg.add_plot_axis(dpg.mvXAxis, label="Time (s)")
y_axis = dpg.add_plot_axis(dpg.mvYAxis, label="Power (dB)")
alpha_theme = create_alpha_theme()
sxx = np.flipud(sxx)
for b,v in bands.items():
frequencies = parameter_values['freqs']
mask = (frequencies >= v[0]) & (frequencies < v[1])
if np.any(mask):
power = np.mean(sxx[mask, :],axis=0) # mean across freqs -> shape: (time,)std = np.std(sxx[mask, :], axis=0)
std = np.std(sxx[mask, :],axis=0)
min_std = power-std
max_std = power+std
emin, emax = time
time_array = np.linspace(emin,emax,len(power))
dpg.add_shade_series(time_array,min_std,y2=max_std,label = f"{b} Std. Deviation", tag=f"{plot_tag}_{b}_std", parent=y_axis)
dpg.bind_item_theme(f"{plot_tag}_{b}_std", alpha_theme)
dpg.add_line_series(time_array,power,label = f"{b} Mean", tag=f"{plot_tag}_{b}_mean", parent=y_axis)
if isinstance(events,list):
for event in events:
stamp = event['Idx']/250 #spectrogram axis is in reality from 0 to 1, and the columns are time based, not sample
# type = event['Type'] #--> for label
if stamp <= emax: dpg.add_inf_line_series(x=[stamp],parent=y_axis)
b1 = dpg.add_button(label = 'Export Plot Figure',callback = filename_window,user_data =('2D', f"{header_tag}_plot", y_axis,0),width=-1,parent=header_tag)
b2 = dpg.add_button(label='Export Signals',width=-1,callback=filename_window,user_data=(y_axis,f"{header_tag}_plot",'2D'),parent=header_tag)
add_exporting_tag(window_id,plot=b1,signal=b2)
with dpg.group(horizontal=False,tag=f"{group_tag}_buttons"):
dpg.add_checkbox(label='Synchronize Axis', callback=link)
dpg.add_button(label='Band Power over Time', callback=merge_wavelet)
dpg.add_separator()
colormap_buttons(group_tag, (global_min, global_max), subplot_tag, wavelet_tags, group_tag=f"{group_tag}_buttons")
[docs]def plot_pac(tags,tab_tag,parameter_values,mode=None):
'''
Computes and plots Phase-Amplitude Coupling, for the selected streams and bands.
:param tags: Description
:param tab_tag: Description
:param parameter_values: Description
:param mode: Description
'''
window_id = extract_window_id(tab_tag)
labels = [dpg.get_item_label(tag) for tag in tags]
signals = [dpg.get_value(tag)[:2] for tag in tags]
bands = getBandsByTab(tab_tag)
if mode: #if default
tab_label = f"{tab_tag}_df_PAC"
else:
tab_label = f"{tab_tag}_PAC"
def plot():
for b in ['s','b']:
for s in ['1','2']:
label = dpg.get_value(f"{tab_label}_{b}{s}")
signal = {}
if b == 's':
idx = labels.index(label)
signal = getSignalbyIdx(window_id,tab_tag,tags[idx])
temp_signal = signals[idx]
signal['X'] , signal['Y'] = temp_signal
parameter_values[f'{b}{s}'] = signal['Y']
if s == '1':
signal1 = signal
else:
signal2 = signal
else:
parameter_values[f'{b}{s}'] = bands[label]
#1st: Filter signals
if len(parameter_values['s1']) != len(parameter_values['s2']):
t = dpg.add_text('Signals with different lengths!',parent=tab_label)
dpg.bind_item_theme(t,change_text_color(1))
return
for s in ['1','2']:
band = parameter_values[f'b{s}']
param_filter = {'N': '4', 'Wn': band,'btype':'band','fs': '250'}
args = pre.call_function('butter', param_filter)
filtered = pre.call_function2('filtfilt', *args, parameter_values[f's{s}'])
param_hilbert = {'x': filtered}
hilbert = pre.call_function('hilbert',param_hilbert)
if s == '1':
phase_data = np.angle(hilbert)
else: amp_data = np.abs(hilbert)
#4th: create bins
if 'n_bins' in parameter_values.keys():
n_bins = parameter_values['n_bins']
if n_bins =='' or n_bins==' ': n_bins = 18
else: n_bins = int(n_bins)
else: n_bins = 18
edges = np.linspace(-np.pi, np.pi, n_bins+1)
centers = (edges[:-1] + edges[1:]) / 2
digitized = np.digitize(phase_data, edges) - 1 #assign each time point to each bin (phase)
amp_means = np.zeros(n_bins)
for i in range(n_bins):
sel = amp_data[digitized==i]
amp_means[i] = np.mean(sel) if sel.size>0 else 0 #get the mean amp from the binned values
# Normaliza
amp_dist = amp_means / np.sum(amp_means + 1e-16)
entropy = -np.sum(amp_dist * np.log(amp_dist + 1e-16)) #shannon entropy
mi = np.round((np.log(n_bins) - entropy) / np.log(n_bins),5) #modulation index
if dpg.does_item_exist(f"{tab_label}_pac"): dpg.delete_item(f"{tab_label}_pac")
with dpg.group(tag=f"{tab_label}_pac", parent=tab_label):
dpg.add_separator()
dpg.add_text(f"Phase Signal: {dpg.get_value(f'{tab_label}_s1')} | Phase Band: {parameter_values['b1']}")
dpg.add_text(f"Amplitude Signal: {dpg.get_value(f'{tab_label}_s2')} | Phase Band: {parameter_values['b2']}")
dpg.add_text('Modulation Index (MI): 0 = NO coupling | 1 = STRONG coupling ')
plot_tag = f"{tab_label}_plot"
with dpg.plot(label=f"Phase-Amplitude Coupling (MI = {mi})", height=300, width=-1,tag=plot_tag):
dpg.add_plot_legend()
x_axis = dpg.add_plot_axis(dpg.mvXAxis, label="Phase (rad)")
y_axis = dpg.add_plot_axis(dpg.mvYAxis, label="Amplitude (microV)")
phase = centers.tolist()
amp = amp_means.tolist()
dpg.add_bar_series(phase,amp,parent=y_axis)
dpg.add_separator(label='Export')
b1 = dpg.add_button(label = 'Export Plot Figure', callback = filename_window,user_data =('Bar', plot_tag, y_axis,0),width=-1)
b2 = dpg.add_button(label='Export Signals',width=-1,callback=filename_window,user_data=(y_axis,plot_tag,'PAC'))
add_exporting_tag(extract_window_id(tab_tag),plot=b1,signal=b2)
peak_a = max(amp)
peak_p = phase[amp.index(peak_a)]
bandwidth = 360/n_bins
peak_d = peak_p*180/np.pi
peak_d = [np.round(peak_d-bandwidth/2,2),np.round(peak_d+bandwidth/2,2)]
info = {'Modulation of Index': str(mi), 'Peak Phase (Rad)':peak_p, 'Peak Band (Degrees)': peak_d ,'Peak Amplitude': peak_a}
if not dpg.does_item_exist(f"{tab_label}_header"): dpg.add_collapsing_header(label='Metrics', parent=tab_label, tag=f"{tab_label}_header")
create_table(f"{tab_label}_header",f"{tab_label}_table",info,trial_label=f"{signal1['Channel']} - {signal2['Channel']}")
if mode:
combos = {}
with dpg.group(horizontal=True,parent=tab_label):
for i,s in enumerate(['Signal 1','Signal 2']):
input_id = dpg.add_combo(
label=s,
items=labels, # <-- customize your options
default_value='---',
# callback=different_combo,
# user_data=(tab_label, combos),
tag = f"{tab_label}_s{i+1}",
width=200
)
combos[s] = input_id
with dpg.group(horizontal=True,parent=tab_label):
bands_labels = list(getBandsByTab(tab_tag).keys())
for i,s in enumerate(['Band 1','Band 2']):
input_id = dpg.add_combo(
label=s,
items=bands_labels, # <-- customize your options
default_value='---',
callback=different_combo,
user_data=(tab_label, combos),
tag = f"{tab_label}_b{i+1}",
width=200
)
combos[s] = input_id
dpg.add_button(label='Plot Phase-Amplitude Coupling', callback=plot,width=-1, parent=tab_label)
else: plot()
[docs]def update_table(table_tag, unit):
'''Update the table by replacing values with their dB equivalents.
:param table_tag:
:param type: str, 'dB' or else
'''
if not dpg.does_item_exist(table_tag):
print(f"Table {table_tag} does not exist!")
return
children = dpg.get_item_children(table_tag, slot=1) # Get table rows
columns = [dpg.get_item_label(c) for c in dpg.get_item_children(table_tag)[0]]
if not children:
print(f"No rows found in {table_tag}.")
return
for row_id in children: # Iterate through rows
row_children = dpg.get_item_children(row_id, slot=1) # Get row items
for i in range(1, len(row_children)): # Skip first column (Trial)
flag = False
if '%' in columns[i]: continue
elif 'freq' in columns[i]: flag=True
tabled = dpg.get_value(row_children[i])
value = eval(tabled)
if unit == 'dB':
db_value = pre.convert_to_db(value)
else: db_value = pre.convert_from_db(value)
if isinstance(db_value, list) and not flag: # List case
# db_value = [db_value[0], abs(db_value[1])]
dpg.set_value(row_children[i], str(db_value))
elif isinstance(db_value, tuple) or flag: # Peak case (power, frequency)
dpg.set_value(row_children[i], f"({db_value[0]}, {value[1]})")
else: # Single number case
dpg.set_value(row_children[i], f"{db_value:.2f}")
[docs]def before_vs_after(sender,app_data,user_data):
'''
Callback plot_featured_signal, for previosly segmented recordings
'''
signal_tags,tab_label,func,tab_tag,parameter_values = user_data
plot_featured_signal(signal_tags,tab_label,func,tab_tag,parameter_values)
[docs]def plot_streaming_stimulation(sender,app_data,user_data):
#toggle will get the child of the second y axis, if it exists
tab_tag, stimulation_list, plot_tag = user_data
axis = dpg.get_item_children(plot_tag)[1]
#create arrays
if app_data:
mint, maxt = 0,0
x_axis, y_axis = axis[0], axis[1]
# Groups = list(np.array([stim[0] for stim in stimulation_list if len(stim)>1]).flatten())
Right = {day: value for stim in stimulation_list if len(stim)>1 for day, value in stim[1].items()}
Left = {day: value for stim in stimulation_list if len(stim)>1 for day, value in stim[2].items()}
Time = {day: value for stim in stimulation_list if len(stim)>1 for day, value in stim[3].items()}
days = [utl.extract_time(utl.parse_datetime(day)) for day in Right.keys()]
if len(np.unique(days))>1:
# convert to UTC
for day, t in Time.items():
Time[day] = [(timedelta(seconds=timestamp) + utl.parse_datetime(day)).timestamp() for timestamp in t]
dpg.configure_item(x_axis,scale=dpg.mvPlotScale_Time,label='Time (UTC)')
for sdx, series in enumerate(dpg.get_item_children(y_axis)[1]):
x = dpg.get_value(series)[0]
label = dpg.get_item_label(series)
dt = utl.parse_datetime(label[label.find('(')+1:label.find(')')])
x = [(timedelta(seconds=xx) + dt).timestamp() for xx in x]
dpg.configure_item(series,x=x)
if sdx==0: mint = min(x)
else: mint = min(mint,min(x))
maxt = max(maxt,max(x))
if not dpg.does_item_exist(f"{tab_tag}_stim_axis"):
dpg.add_plot_axis(dpg.mvYAxis2,opposite=True,parent=plot_tag,label='Stimulation Amplitude (mA)',tag=f"{tab_tag}_stim_axis")
for rec, stim in Right.items():
r = dpg.add_line_series(Time[rec],stim,parent=f"{tab_tag}_stim_axis",tag=f"{tab_tag}_{rec}_right")
l = dpg.add_line_series(Time[rec],Left[rec],parent=f"{tab_tag}_stim_axis",tag=f"{tab_tag}_{rec}_left")
dpg.bind_item_theme(r,change_line_color_and_thickness(-1,4))
dpg.bind_item_theme(l,change_line_color_and_thickness(-3,4))
else:
dpg.configure_item(f"{tab_tag}_stim_axis",show=True)
elif not app_data and dpg.does_item_exist(f"{tab_tag}_stim_axis"):
mint, maxt = 0,0
dpg.configure_item(f"{tab_tag}_stim_axis",show=False)
x_axis, y_axis = axis[0], axis[1]
for sdx, series in enumerate(dpg.get_item_children(y_axis)[1]):
v = dpg.get_value(series)
x,y=v[0],v[1]
label = dpg.get_item_label(series)
new_x = [xx-x[0] for xx in x]
dpg.configure_item(series,x=new_x)
if sdx==0: mint = min(x)
else: mint = min(mint,min(x))
maxt = max(maxt,max(x))
dpg.configure_item(x_axis,scale=dpg.mvPlotScale_Linear,label='Time (s)')
dpg.set_axis_limits(x_axis, mint, maxt)
def reset_axis():
dpg.set_axis_limits_auto(x_axis)
dpg.set_axis_limits_auto(y_axis)
dpg.set_frame_callback(1, reset_axis)
#------------GUI PRETTY-----------------
[docs]def design():
'''
Defines theme of GUI
'''
def add(parent):
dpg.add_theme_style(dpg.mvStyleVar_FrameRounding, 8, parent=parent)
dpg.add_theme_style(dpg.mvStyleVar_WindowRounding,10, parent=parent)
dpg.add_theme_style(dpg.mvStyleVar_WindowTitleAlign,0.5, parent=parent)
with dpg.theme() as theme:
add(dpg.add_theme_component(dpg.mvAll))
with dpg.theme_component(dpg.mvCheckbox):
dpg.add_theme_style(dpg.mvStyleVar_FrameRounding, 0)
return theme
[docs]def create_alpha_theme(alpha=0.5,color=None):
'''
Create and return a DearPyGui plot alpha theme, to be binded to line series.
:param alpha (float): Opacity value between 0.0 and 1.0
:param color (list): rbga values of line (for cases of dpg.add_line_series, that are not filled)
Returns:
int: The theme ID
'''
if color:
color[-1] = alpha
color = [int(c*255) for c in color]
with dpg.theme() as alpha_theme:
with dpg.theme_component(dpg.mvAll):
if color:
dpg.add_theme_color(dpg.mvPlotCol_Line,color,category=dpg.mvThemeCat_Plots )
else: dpg.add_theme_style(dpg.mvPlotStyleVar_FillAlpha, alpha, category=dpg.mvThemeCat_Plots)
return alpha_theme
[docs]def switch_theme(sender, app_data, user_data):
'''
Binds selected theme to GUI
:param user_data: theme
'''
label = dpg.get_item_label(sender)
if label=='Classic':
dpg.bind_theme(design())
else:
dpg.bind_theme(user_data)
[docs]def change_electrodes_names(sender, app_data, user_data):
'''
Adds user-defined name into dictionary, so its used without app_state memory dictinary loss.
Channels are to be renamed are hardcoded.
'''
input_counter = [0] # Use a list to allow mutable reference
this = "Change Channel Names Window"
def handle_submit(sender, app_data, user_data):
for i in range(user_data[0]):
old_name = dpg.get_value(f"old_name_{this}_{i}")
new_name = dpg.get_value(f"new_name_{this}_{i}")
if new_name == '': new_name = old_name
electrodes_names[old_name] = new_name
for side in ['LEFT','RIGHT']:
electrodes_names[f'{old_name}_{side}'] = f'{new_name}_{side}'
dpg.delete_item(this)
def reset(sender, app_data, user_data):
electrodes_names.clear()
if dpg.does_item_exist(this):
dpg.delete_item(this)
with dpg.window(tag=this, width=400, height=300,on_close=dpg.delete_item(this)):
dpg.add_text("Change Channel Names")
dpg.add_separator()
channels = ['ONE_THREE','ZERO_THREE','ZERO_TWO']
for row_id, ch in enumerate(channels):
input_counter[0]+=1
with dpg.group(tag=f"input_group_{row_id}", parent="input_container", horizontal=True):
dpg.add_input_text(tag=f"old_name_{this}_{row_id}",default_value=ch, width=120)
dpg.add_input_text(tag=f"new_name_{this}_{row_id}", label="New Name", width=120)
with dpg.group(horizontal=True):
dpg.add_button(label="Submit", callback=handle_submit, user_data=input_counter)
dpg.add_button(label="Reset", callback=reset)
# dpg.configure_item(this,on_close=dpg.configure_item(this,show=False))
[docs]def change_window_name(sender,app_data,user_data):
'''Renames window.
window_id = user_data '''
window_id = user_data
old_name = dpg.get_item_label(window_id)
if dpg.does_item_exist(f"{window_id}_name_change"):
dpg.delete_item(f"{window_id}_name_change")
def handle_submit(sender,app_data,user_data):
name = dpg.get_value(f"{window_id}_name")
dpg.set_item_label(window_id,name)
dpg.delete_item(f"{window_id}_name_change")
with dpg.window(tag=f"{window_id}_name_change", autosize=True,no_collapse=True):
dpg.add_text("Change Window Name")
with dpg.group(horizontal=True):
dpg.add_text('New name: ')
dpg.add_input_text(default_value=old_name, width=120,tag=f"{window_id}_name")
with dpg.group(horizontal=True):
dpg.add_button(label="Submit", callback=handle_submit)
[docs]def change_event_name(sender,app_data,user_data):
'''
Renames events. User must know the original events names. Only replaced in future created items.
'''
this = "event_name_change"
row_id = 0
if dpg.does_item_exist(this):
dpg.delete_item(this)
def add(sender,app_data,user_data):
row_id=dpg.get_item_user_data('submit event name') +1
dpg.set_item_user_data('submit event name',row_id)
with dpg.group(horizontal=True, tag=f"{this}_{row_id}",parent=f"{this}_group"):
dpg.add_input_text(tag=f"old_name_{this}_{row_id}",default_value='Old Name Event', width=120)
dpg.add_input_text(tag=f"new_name_{this}_{row_id}",default_value='New Name Event', width=120)
def reset(sender,app_data,user_data):
oldies = list(all_event_names.keys())
for o in oldies: all_event_names[o] = o
dpg.delete_item(this)
def handle_submit(sender,app_data,user_data):
row_id=dpg.get_item_user_data(sender)
for i in range(row_id+1):
old_name = dpg.get_value(f"old_name_{this}_{i}")
new_name = dpg.get_value(f"new_name_{this}_{i}")
if new_name == '': new_name = old_name
all_event_names[old_name] = new_name
dpg.delete_item(this)
with dpg.window(tag=this, autosize=True,no_collapse=True,on_close=dpg.delete_item(this)):
dpg.add_text("Change Marked Events Name")
with dpg.group(tag=f"{this}_group"):
with dpg.group(horizontal=True, tag=f"{this}_{row_id}"):
dpg.add_input_text(tag=f"old_name_{this}_{row_id}",default_value='Old Name Event', width=120)
dpg.add_input_text(tag=f"new_name_{this}_{row_id}",default_value='New Name Event', width=120)
dpg.add_button(label = '+',small=True,callback=add,tag='add')
with dpg.group():
dpg.add_button(label='Submit',callback=handle_submit,user_data=row_id,tag='submit event name',small=True)
dpg.add_button(label='Reset',callback=reset)
[docs]def change_inf_line_color(color_id):
'''
Theme for mvInfLine
:param color_id: int, index of color
'''
with dpg.theme() as theme_id:
with dpg.theme_component(dpg.mvInfLineSeries):
dpg.add_theme_color(dpg.mvPlotCol_Line, colors[color_id], category=dpg.mvThemeCat_Plots)
return theme_id
[docs]def change_line_color(color):
'''
Theme for mvLineSeries
:param color_id: int or list, index of color or rgba code
'''
if isinstance(color,int):
color = colors[color]
elif isinstance(color, list):
if sum(color)<3:
color = [int(c*255) for c in color]
with dpg.theme() as theme_id:
with dpg.theme_component(dpg.mvLineSeries):
dpg.add_theme_color(dpg.mvPlotCol_Line, color, category=dpg.mvThemeCat_Plots)
return theme_id
[docs]def change_line_color_and_thickness(color_id,thickness):
with dpg.theme() as theme_id:
with dpg.theme_component(dpg.mvLineSeries):
dpg.add_theme_color(dpg.mvPlotCol_Line, colors[color_id], category=dpg.mvThemeCat_Plots)
dpg.add_theme_style(dpg.mvPlotStyleVar_LineWeight, thickness, category=dpg.mvThemeCat_Plots)
return theme_id
[docs]def change_plot_color(color_id):
'''
Theme for mvScatterSeries
:param color_id: int, index of color
'''
if isinstance(color_id,int):
color = colors[color_id]
else:
color = tuple([np.round(c*255) for c in color_id])
with dpg.theme() as theme_id:
with dpg.theme_component(dpg.mvScatterSeries):
dpg.add_theme_color(dpg.mvPlotCol_MarkerFill, color, category=dpg.mvThemeCat_Plots)
dpg.add_theme_color(dpg.mvPlotCol_MarkerOutline, color, category=dpg.mvThemeCat_Plots)
return theme_id
[docs]def change_line_thickness(thickness, component = dpg.mvLineSeries):
'''
Theme for mvLineSeries thickness
:param thickness: int, width of line
'''
with dpg.theme() as theme_id:
with dpg.theme_component(component):
dpg.add_theme_style(dpg.mvPlotStyleVar_LineWeight, thickness, category=dpg.mvThemeCat_Plots)
return theme_id
[docs]def change_text_color(color_id=5):
'''
Theme for text color
:param color_id: int, index of color
'''
with dpg.theme() as theme_id:
with dpg.theme_component(dpg.mvText):
dpg.add_theme_color(dpg.mvThemeCol_Text, colors[color_id], category=dpg.mvThemeCat_Core)
return theme_id
[docs]def different_combo(sender,app_data,user_data):
'''
Checks if selected recordings are different
'''
if dpg.does_item_exist(f"{sender}_pac_w"):dpg.delete_item(f"{sender}_pac_w")
doc_container, combos = user_data
atual = dpg.get_value(sender)
for key in combos.keys():
if sender != combos[key]:
other = dpg.get_value(combos[key])
if atual == other:
dpg.add_separator(parent=doc_container)
e = dpg.add_text('Recordings cannot be the same!', parent=doc_container, show=True, tag = f"{sender}_pac_w")
dpg.bind_item_theme(e,change_text_color(1))
return
[docs]def resize_font(sender,app_data,user_data):
'''
Resizes font of all items
'''
window_id = user_data
this = f"{window_id}_font_size"
if dpg.does_item_exist(this):
dpg.delete_item(this)
def set_size(sender,app_data,user_data):
value = dpg.get_value(sender)
set_font(size=value)
def reset(sender,app_data,user_data):
set_font()
dpg.set_value(user_data,16)
with dpg.window(label="Resize Font",tag=this, autosize=True,no_collapse=True):
dpg.add_text("Resize Font")
with dpg.group(horizontal=True,width=90):
dv = int(dpg.get_item_configuration("CURRENT_FONT").get('size'))
slider=dpg.add_slider_int(default_value=dv,min_value=8,max_value=25,callback=set_size,width=80)
dpg.add_button(label='Reset',callback= reset,small=True,user_data=slider)
[docs]def set_font(size=16,just_theme=False):
'''
Redefines actual theme to support function resize_font
:param size: int
'''
# delete previous font if it exists
if dpg.does_item_exist("CURRENT_FONT"):
dpg.delete_item("CURRENT_FONT")
font_path = utl.resource_path("fonts", "SF-Pro-Display-Semibold.otf")
exist = os.path.exists(font_path)
if not exist:
return
# add new font to existing registry
default_font = dpg.add_font(
font_path,
size,
parent="FONT_REGISTRY",
tag="CURRENT_FONT"
)
if just_theme:
default_font = dpg.add_font(
font_path,
size,
parent="FONT_REGISTRY",
)
return default_font
try:
dpg.bind_font(default_font)
except Exception as e: print(e)
#--- Extract signal/identifiers
[docs]def getActiveSeries(signal_tags):
'''
From signal's id, returns only a list of the visible tags.
:param signal_tags: dict/list, if dict the tags are keys
'''
active_tags = []
if type(signal_tags) == dict:
tags = signal_tags.keys()
elif type(signal_tags) == list:
tags = signal_tags.copy()
for tag in tags:
info = dpg.get_item_configuration(tag)
if info['show'] == True:
active_tags.append(tag)
keywords = ['series', 'file', 'OFF', 'ON', 'ARTIFACTS', 'psd']
active_tags = [tag for tag in active_tags if extract_event_from_series_tag(tag) in keywords]
return active_tags
[docs]def getActiveSignals(tab_tag,signal_tags):
'''
From Active Series, returns the data of the series in app_state memory dictinary.
:param tab_tag: Description
:param signal_tags: Description
'''
window_id = extract_window_id(tab_tag)
active_series = getActiveSeries(signal_tags)
signals = []
for serie in active_series:
try:
signal = getSignalbyIdx(window_id, tab_tag, serie)
if signal is None: # If `getSignalbyIdx` returns None
signal = getSignalbyIdxSide(window_id, tab_tag, serie)
except KeyError as e: # Catch missing key issues
signal = getSignalbyIdxSide(window_id, tab_tag, serie)
except Exception as e: # Catch any other unexpected error
print(f"Unexpected error for {tab_tag}, {serie}: {e}")
signal = None # Handle gracefully
signals.append(signal)
return signals
[docs]def getSignalbyIdx(window_id,tab_tag,series_tag,mode=None):
'''
Gets signal data, from app_state memory dictinary, from series_tag
:param window_id: Description
:param tab_tag: Description
:param series_tag: Description
:param mode: Description
'''
idx = extract_idx_from_series_tag(series_tag)
file = extract_file_from_series_tag(series_tag)
edx = int(extract_edx_from_series_tag(series_tag))
# edx = extract_idx_from_series_tag(series_tag)
wt = extract_type_window(window_id)
if wt == 'Combined': #Combined window
if mode == None: #signal and not events
return app_state['windows'][window_id]['Files'][file]['Data'][tab_tag]['Signals'][idx]
else: #events
return app_state['windows'][window_id]['Files'][file]['Data'][tab_tag]['Events'][edx]
else: #Individual - always signal
return app_state['windows'][window_id]['Data'][tab_tag]['Signals'][idx]
[docs]def getSignalbyIdxSide(window_id,tab_tag,series_tag):
'''
Gets signal data, for Events, from series_tag.
:param window_id: Description
:param tab_tag: Description
:param series_tag: Description
'''
idx = extract_idx_from_series_tag(series_tag)
side = extract_side_from_series_tag(series_tag)
wt = extract_type_window(window_id)
file = extract_file_from_series_tag(series_tag)
if wt == 'Individual': return app_state['windows'][window_id]['Data'][tab_tag]['Signals'][side][idx]
else: return app_state['windows'][window_id]['Files'][file]['Data'][tab_tag]['Signals'][idx][side]
[docs]def getEventsbyIdx(tab_tag,idx):
'''
Gets Events in signal idx, by id of signal
:param tab_tag: Description
:param idx: int, signal id in app_state memory dictinary
'''
window_id = extract_window_id(tab_tag)
dictionary = {}
if extract_type_window(window_id) == 'Combined':
dictionary = app_state['windows'][window_id]['Data'][tab_tag]['Signals'][idx]
if 'Events' in dictionary.keys():
return dictionary['Events']
else: return None
[docs]def getEventsbyTag(tab_tag,series_tag):
'''
Gets Events in signal idx, by series tag
:param tab_tag: Description
:param series_tag: Description
'''
window_id = extract_window_id(tab_tag)
idx = extract_idx_from_series_tag(series_tag)
file = extract_file_from_series_tag(series_tag)
if extract_type_window(window_id) == 'Combined':
dictionary= app_state['windows'][window_id]['Files'][file]['Data'][tab_tag]
else:
dictionary = app_state['windows'][window_id]['Data'][tab_tag]['Signals'][idx]
if 'Events' in dictionary.keys():
if dictionary['Events'] == []: return None
return dictionary['Events']
else: return None
[docs]def getBandsByTab(tab_tag):
'''
Gets frequency bands (used for spectral analysis), associated to window or tab.
:param tab_tag: Description
'''
window_id = extract_window_id(tab_tag)
return app_state['windows'][window_id]['Bands']
[docs]def getUpdatedEventName(event):
'''
Gets user-defined event name, from original name defined in json file.
:param event: str, original name
'''
if event not in all_event_names.keys():
all_event_names[event] = event
return all_event_names[event]
[docs]def getUpdatedElectrodeName(electrode):
'''
Gets user-defined electrode name, from original electrode name defined in json file.
:param electrode: str, original name
'''
electrode = electrode.upper().replace('_AND_','_')
if electrode not in electrodes_names.keys():
electrodes_names[electrode] = electrode
return electrodes_names[electrode]
[docs]def getDatabyTag(series_tag):
'''
Return Data dicitionary (from json file), from series tag
:param series_tag: Description
'''
window_id = extract_window_id(series_tag)
wt = extract_type_window(window_id)
if wt == 'Combined':
file = extract_file_from_series_tag(series_tag)
return app_state['windows'][window_id]['Files'][file]['Data']
else:
return app_state['windows'][window_id]['Data']
[docs]def getDeviation(tab_tag):
dev = 1
if dpg.does_item_exist(f"{tab_tag}_deviation"):
name = dpg.get_value(f"{tab_tag}_deviation")
if '95%' in name: dev = 2
return dev
[docs]def getDatabyFile(window_id, file):
'''
Return Data dicitionary (from json file), from file id in app_state memory dictinary
:param window_id: Description
:param file: Description
'''
wt = extract_type_window(window_id)
if wt == 'Combined':
return app_state['windows'][window_id]['Files'][file]['Data']
else:
return app_state['windows'][window_id]['Data']
[docs]def get_limits(children,plot_tag):
'''
Returns absolute axis limits, and indexes to create mask on signals and return only the visible parts of the signals.
:param children: list, signals tags id (children of y axis)
:param plot_tag: str/id, plot id (parent of axis)
'''
axis = dpg.get_item_children(plot_tag)[1][0]
limits = dpg.get_axis_limits(axis)
xx = [dpg.get_value(tag)[0] for tag in children]
lengths = [len(x) for x in xx]
maxX = xx[lengths.index(max(lengths))] #get the longest array
mini = np.abs(np.array(maxX) - limits[0]).argmin() # closest index
maxi = np.abs(np.array(maxX) - limits[1]).argmin()
tmin = min(mini,maxi)
tmax = max(mini,maxi)
return tmin,tmax,maxX
[docs]def updateKeySignalbyIdx(value,tab_tag,series_tag, key): #check need of this
'''
Updates app_state memory dictionary with processed signal (cut or new events)
:param value: list, new signal
:param tab_tag: Description
:param series_tag: Description
:param key: str, key onto the value will be updated, must exist
'''
window_id = extract_window_id(tab_tag)
idx = extract_idx_from_series_tag(series_tag)
file = extract_file_from_series_tag(series_tag)
if file == None:
app_state['windows'][window_id]['Data'][tab_tag]['Signals'][idx][key] = value
else: app_state['windows'][window_id]['Files'][file]['Data'][tab_tag]['Signals'][idx][key] = value
[docs]def updateSignalbyIdx(value,tab_tag,series_tag, func = None, mode = None, new_tab_tag = None): #check need of this
'''
Updates signal after filtering and saves wavelet into app_state memory.
:param value: list, new signal
:param tab_tag: Description
:param series_tag: Description
:param func: Description
:param mode: Description
:param new_tab_tag: Description
'''
window_id = extract_window_id(tab_tag)
idx = extract_idx_from_series_tag(series_tag)
file = extract_file_from_series_tag(series_tag)
edx = int(extract_edx_from_series_tag(series_tag))
# edx = extract_idx_from_series_tag(series_tag)
wt = extract_type_window(window_id)
if new_tab_tag: tab_tag=new_tab_tag
if mode == None:
if file == None:
if func != None:
app_state['windows'][window_id]['Data'][tab_tag]['Signals'][idx][func] = value
else: app_state['windows'][window_id]['Data'][tab_tag]['Signals'][idx]['Y'] = value
else:
if func != None: app_state['windows'][window_id]['Files'][file]['Data'][tab_tag]['Signals'][idx][func] = value
else: app_state['windows'][window_id]['Files'][file]['Data'][tab_tag]['Signals'][idx]['Y'] = value
else:
if wt == 'Combined':
if func != None: app_state['windows'][window_id]['Files'][file]['Data'][tab_tag]['Events'][edx][func] = value
else:
app_state['windows'][window_id]['Files'][file]['Data'][tab_tag]['Events'][edx]['Y'] = value
else:
if func != None: app_state['windows'][window_id]['Data'][tab_tag]['Signals'][idx]['Events'][edx][func] = value
else: app_state['windows'][window_id]['Data'][tab_tag]['Signals'][idx]['Events'][edx]['Y'] = value
[docs]def enforce_min_max(sender, app_data, user_data):
'''Ensure max is always at least diff units greater than min, and min is never below 0.
sender: min_tag or max_tag
min_tag, max_tag, diff = user_data
'''
min_tag, max_tag, diff = user_data # Extract tag references
min_value = max(0, dpg.get_value(min_tag)) # Ensure min is at least 0
max_value = absolute_max = dpg.get_value(max_tag)
if sender == min_tag:
if min_value >= max_value - diff: # If min is too close to max
max_value = min_value + diff
dpg.set_value(max_tag, max_value) # Ensure max stays 10+ units above
dpg.set_value(min_tag, min_value) # Set min (ensuring it's 0 or higher)
elif sender == max_tag:
if max_value < diff: # Prevent max from being below 10
min_value = 0
max_value = diff
elif max_value <= min_value + diff: # Ensure min stays 10 below max
min_value = max_value - diff
elif max_value > absolute_max:
max_value = absolute_max
dpg.set_value(min_tag, min_value)
dpg.set_value(max_tag, max_value)
[docs]def enforce_range(sender, app_data, user_data):
'''
Enforces range of colormap, diff of 5 units.
:param sender: Description
:param app_data: Description
:param user_data: tab_label (parent of sender), absolute_min (int/float), absolute_max (int/float)
'''
tab_label, absolute_min, absolute_max = user_data
min_tag, max_tag = f"{tab_label}_emin", f"{tab_label}_emax"
diff = 5
min_value = dpg.get_value(min_tag)
max_value = dpg.get_value(max_tag)
min_value = max(min_value, absolute_min)
max_value = min(max_value, absolute_max)
if max_value - min_value < diff:
if sender == min_tag:
max_value = min(min_value + diff, absolute_max)
min_value = max(max_value - diff, absolute_min)
else: # sender == max_tag
min_value = max(max_value - diff, absolute_min)
max_value = min(min_value + diff, absolute_max)
# Set adjusted values back
dpg.set_value(min_tag, min_value)
dpg.set_value(max_tag, max_value)
#--- Process with signal
[docs]def Volt2dB(sender,app_data,user_data):
'''
Callback of checkbox - changes PSD plot between original units to dB.
y_axis: str/id, parent of plots (axis y)
plot_tag: str/id, parent of axis
signal_tags: list, list of tags to be converted, if they're shown
tables: list, list of tags of tables to be converted
:param sender: Description
:param app_data: boolean, type of units (original/dB)
:param user_data: y_axis, plot_tag, signal_tags, tables
'''
unit = app_data
if unit == True:
unit = 'dB'
label = unit
else:
unit = 'Y'
label = 'µV^2/Hz'
y_axis, plot_tag, signal_tags, tables = user_data
dpg.set_item_label(y_axis,label=label)
all_values, visible = [], 0
for tag in signal_tags:
label = dpg.get_item_label(tag)
if dpg.get_item_configuration(tag)['show']==True:
visible = +1
_,y = dpg.get_value(tag)[:2]
if app_data:
y = pre.convert_to_db(y)
else:
y = pre.convert_from_db(y)
dpg.configure_item(tag,y=y)
all_values.append(y)
if visible == 0:
std_tag,mean_tag = dpg.get_item_children(y_axis)[1][-2:]
mean = dpg.get_value(mean_tag)[1]
std = dpg.get_value(std_tag)[1]
std = np.array(std) + np.array(mean)
if app_data:
mean = pre.convert_to_db(mean)
std = pre.convert_to_db(std.tolist())
else:
mean = pre.convert_from_db(mean)
std = pre.convert_from_db(std.tolist())
mean = np.array(mean)
std = np.array(std)
else:
mean = np.nanmean(all_values,axis=0)
std = np.nanstd(all_values,axis=0)
dev = getDeviation(plot_tag)
minus, plus = mean-dev*std, mean+dev*std
dpg.configure_item(f"{plot_tag}_std", y1=minus,y2=plus)
dpg.bind_item_theme(f"{plot_tag}_std",create_alpha_theme())
dpg.configure_item(f"{plot_tag}_mean", y=mean)
dpg.bind_item_theme(f"{plot_tag}_mean", change_line_thickness(5))
time.sleep(0.1)
for table in np.unique(tables).tolist():
update_table(table,unit)
[docs]def getMeanActiveSeries(window_id,tab_tag,signal_tags,key='Y'): #try to use it in more places
'''
Calculates mean and standard deviation from the active signals in plot.
:param window_id: str/id
:param tab_tag: Description
:param signal_tags: Description
:param dev: default to None, standar deviation*1, else gets value of radio button to get type of standard deviation
'''
if 'psd' in tab_tag: key = 'LFPMagnitude'
active_tags = getActiveSeries(signal_tags)
active_signals = []
mode = extract_mode(tab_tag)
for tag in active_tags:
if mode =='Events':
active = getSignalbyIdxSide(window_id,tab_tag,tag)
else:
active = getSignalbyIdx(window_id,tab_tag,tag)
active_signals.append(active)
if len(active_signals)>0:
mean_power = pre.MeanSignal(active_signals,key)
std_power = pre.StdDevSignal(active_signals,key)
dev = getDeviation(tab_tag)
std_power = dev*std_power
minus, plus = mean_power-std_power, mean_power+std_power
if dpg.does_item_exist(f"{tab_tag}_2db") and dpg.get_value(f"{tab_tag}_2db"):
mean_power = np.array([pre.convert_to_db(m) for m in mean_power])
minus = np.array([pre.convert_to_db(m) for m in minus])
plus = np.array([pre.convert_to_db(m) for m in plus])
else:
return 0,0
return mean_power, minus, plus
[docs]def segment_signal(tab_tag, signal_tags,plot_tag):
'''
Adds buttons to perform segmentation of time-domain signals
:param tab_tag: Description
:param signal_tags: Description
:param plot_tag: Description
'''
with dpg.theme() as theme:
with dpg.theme_component(dpg.mvAll):
dpg.add_theme_style(dpg.mvStyleVar_SeparatorTextAlign,1)
with dpg.group(tag = f"{tab_tag}_segmentation"):
time.sleep(0.1)
s = dpg.add_separator(label='SEGMENTATION: minimum of 10s')
dpg.bind_item_theme(s,theme)
mmax = max([dpg.get_value(tag)[0][-1] for tag in signal_tags])
with dpg.group(horizontal=True):
dpg.add_input_int(label='Start', tag=f"{tab_tag}_min", width=90, min_value=0,default_value=0, callback=enforce_min_max, user_data=(f"{tab_tag}_min", f"{tab_tag}_max", 10))
dpg.add_input_int(label='End', tag=f"{tab_tag}_max", width=90,max_value=mmax, default_value=mmax, callback=enforce_min_max, user_data=(f"{tab_tag}_min", f"{tab_tag}_max", 10))
dpg.add_button(label='Segment Signal', show=True, callback=cut_signals,user_data=(tab_tag, signal_tags))
[docs]def cut_signals(sender,app_data,user_data):
'''
Cuts and saves time-domain signals.
tab_tag: Description
signal_tags: Description
:param sender: Description
:param app_data: Description
:param user_data: tab_tag, signal_tags
'''
tab_tag, signal_tags = user_data
window_id = extract_window_id(tab_tag)
active_series = getActiveSeries(signal_tags)
min_value = dpg.get_value(f"{tab_tag}_min")
max_value = dpg.get_value(f"{tab_tag}_max")
min_idx = min_value*250
max_idx = max_value*250
dpg.set_axis_limits(axis=f"x_axis_{tab_tag}", ymin=min_value, ymax=max_value)
for serie in active_series:
signal = getSignalbyIdx(window_id,tab_tag,serie)
new_x = signal['X'][min_idx:max_idx]
new_y = signal['Y'][min_idx:max_idx]
updateKeySignalbyIdx(new_x,tab_tag,serie,'X')
updateKeySignalbyIdx(new_y,tab_tag,serie,'Y')
time.sleep(0.1)
update_plot_td(tab_tag,signal_tags,' Segment')
[docs]def interval_events(tab_tag, label1, label2,mini=15,maxi=15):
'''
Adds buttons to get event-locked segmentation interval.
:param tab_tag: Description
:param label1: str, label of input 1
:param label2: str, label of input 2
:param mini: default to 15s, value of before interval (s)
:param maxi: default to 15s, value of after interval (s)
'''
min_dif = 2
with dpg.group(horizontal=True):
dpg.add_input_int(label=label1, tag=f"{tab_tag}_emin", width=90, default_value=mini, callback=enforce_min_max, user_data=(f"{tab_tag}_emin", f"{tab_tag}_emax", min_dif))
dpg.add_input_int(label=label2, tag=f"{tab_tag}_emax", width=90, default_value=maxi, callback=enforce_min_max, user_data=(f"{tab_tag}_emin", f"{tab_tag}_emax", min_dif))
[docs]def DefaultFilter(sender,app_data,user_data):
'''
Standard filtration = 5th order Butterworth bandpass filter 1-100 Hz + Removal of cardiac component
Remove cardiac component - Fixed peak finder method - BRAVO adapted
tab_tag, signal_tags = user_data
tab_tag: Description
signal_tags: Description
'''
tab_tag, signal_tags = user_data
active_series = getActiveSeries(signal_tags)
window_id = extract_window_id(tab_tag)
signal_mode, plot_mode = None, None
wt = extract_type_window(window_id)
if wt == 'Combined':
config = dpg.get_item_configuration(f"x_axis_{tab_tag}")
for j in app_state['windows'][window_id]['Files']:
if tab_tag in j['Data'].keys():
if isinstance(j['Data'][tab_tag], dict) and 'Events' in j['Data'][tab_tag].keys():
signal_mode = plot_mode = 'segment'
if config['scale'] == 1: #if were plotting in datetime (signal w marked events) and not seconds (cutted events)
signal_mode = plot_mode = None
break
else:
signals = app_state['windows'][window_id]['Data'][tab_tag]['Signals']
for i,_ in enumerate(signals):
events = getEventsbyIdx(tab_tag,i)
if events != None: plot_mode = 'segment'
band = dpg.get_value(item=f"{tab_tag}_default_wn")[:2]
if band[0] == 0:
band = [0.1,band[1]]
dpg.delete_item(f"{tab_tag}_default_filter",children_only=True)
dpg.add_text("3rd Order Butterworth Filter and Removal of Cardiac Component", parent=f"{tab_tag}_default_filter")
else:
band = list(band)
dpg.delete_item(f"{tab_tag}_default_filter",children_only=True)
dpg.add_text("5th Order Butterworth Filter and Removal of Cardiac Component", parent=f"{tab_tag}_default_filter")
for series in active_series:
signal = {}
signal=getSignalbyIdx(window_id,tab_tag,series,mode=signal_mode)
# signal['X'], signal['Y']=dpg.get_value(series)[:2]
clean, _ = pre.removeCardiacComponent(signal, band)
clean = np.ascontiguousarray(clean)
updateSignalbyIdx(clean,tab_tag,series,mode=signal_mode)
title = 'Default Filter'
update_plot_td(tab_tag,signal_tags, title,signal_mode = plot_mode)
[docs]def CustomFilter(sender,app_data,user_data):
'''
Function that handles all the process from button/input creation to filter application on the signals.
tab_tag: Description
signal_tags: Description
:param sender: Description
:param app_data: Description
:param user_data: tab_tag, signal_tags
'''
tab_tag, signal_tags = user_data
window_id = extract_window_id(tab_tag)
if not 'Data' in app_state['windows'][window_id]:
if 'Events' in app_state['windows'][window_id]['Files'][-1]['Data'][tab_tag].keys():
mode = 'segment'
else: mode = None
else: mode = None
param_container = f"{tab_tag}_filters_container"
doc_container = f"{tab_tag}_filters_doc"
if dpg.does_item_exist(param_container):
dpg.delete_item(param_container,children_only=True)
dpg.delete_item(doc_container,children_only=True)
else:
ch = dpg.add_tree_node(label='Custom Parameters',parent=f"{tab_tag}_filters",default_open=True, tag = f"{tab_tag}_tree_filters")
with dpg.group(parent=ch,horizontal=True):
dpg.add_child_window(width=200, height=300, tag = param_container) # Space to add inputs dynamically
dpg.add_child_window(width=600, height=300, tag = doc_container)
parameter_values = {}
input_fields = {}
def create_input_fields(sender, app_data):
'''Dynamically create input fields for selected function.'''
#global parameter_values, input_fields
dpg.delete_item(param_container, children_only=True) # Clear previous inputs
params = pre.get_required_params(app_data)
if not params:
dpg.add_text("No parameters found", parent=param_container)
return
dpg.add_text("Required parameters: ", parent=param_container)
parameter_values.clear() # Reset parameter storage
input_fields.clear() # Reset input field storage
_, documentation = fill_params_features(app_data)
documentation = pre.extract_params_and_returns(documentation)
dpg.add_text(documentation, parent=doc_container, tag = f"{doc_container}_doc")
for param, default in params.items():
if param == 'fs': default = 250
input_id = dpg.add_input_text(
label=param,
default_value=str(default) if default is not None else "",
parent=param_container
)
input_fields[param] = input_id # Store input field reference
# Add save button
dpg.add_button(label="Save Parameters", callback=save_parameters, parent=param_container, user_data = app_data)
def save_parameters(sender, app_data, user_data):
'''Saves input values to the dictionary and prints them.'''
parameter_values.clear() # Reset before saving
for param, input_id in input_fields.items():
values = dpg.get_value(input_id)
if values is str and ',' in values:
try:
# Split the string at the comma and convert each part to a float
values = np.array([float(val) for val in values.split(',')])
except ValueError:
print(f"Invalid input for parameter {param}. Please enter valid numbers separated by commas.")
return # If parsing fails, exit early
parameter_values[param] = values
if not dpg.does_item_exist(f"{tab_tag}_plot_filtered"):
dpg.add_button(label="Plot filtered signal", callback=apply_filt, parent=param_container, user_data=user_data, tag=f"{tab_tag}_plot_filtered")
def apply_filt(sender, app_data, user_data):
func = user_data
tags = getActiveSeries(signal_tags)
args = pre.call_function(func, parameter_values) #gets the parameters of the filter
for tag in tags:
# if mode:
# stream=getSignalbyIdx(window_id,tab_tag,tag,mode='segment')
# else: stream = getSignalbyIdx(window_id,tab_tag,tag)
stream=getSignalbyIdx(window_id,tab_tag,tag,mode)
y = stream['Y']
filtered = pre.call_function2('filtfilt', *args, y)
updateSignalbyIdx(np.ascontiguousarray(filtered),tab_tag,tag, mode=mode)
snr = (10*np.log10(np.sum(np.array(y)**2)/np.sum(y-filtered)**2))
title = f'Custom Filter: {func}'
update_plot_td(tab_tag,signal_tags, title,signal_mode=mode)
create_input_fields(sender,app_data)
[docs]def DefaultFeatures(sender, app_data, user_data):
tab_tag, signal_tags = user_data
window_id = extract_window_id(tab_tag)
dpg.configure_item(f"window_{window_id}", autosize=True)
wt = extract_type_window(window_id)
parameter_values = {}
if app_data== 'multitaper coherence':
parameter_values, documentation = fill_params_features('dpss')
elif app_data == 'cross-correlation':
parameter_values, documentation = fill_params_features('correlate')
parameter_values['method'] = 'auto'
elif app_data == 'PAC':
parameter_values, documentation = fill_params_features('hilbert')
else: parameter_values, documentation = fill_params_features(app_data)
func = app_data
tags = getActiveSeries(signal_tags)
events = [getEventsbyTag(tab_tag,serie) for serie in tags]
if all(e is None for e in events): events = None
if events:
mode = 'segment'
else: mode = None
tab_label = f"{tab_tag}_df_{func}"
if not dpg.does_item_exist(f"{tab_tag}_features_bar"):
dpg.add_tab_bar(reorderable=True,parent=f"{tab_tag}_features", tag = f"{tab_tag}_features_bar")
if dpg.does_item_exist(tab_label):
dpg.delete_item(tab_label, children_only=True)
dpg.configure_item(tab_label,show=True)
else:
dpg.add_tab(label=f"Default {func}", parent=f"{tab_tag}_features_bar", tag=tab_label, closable=True)
dates = []
for tag in tags:
signal = getSignalbyIdx(window_id,tab_tag,tag,mode)
dates.append(signal['Date'])
dates = np.unique(dates)
if len(dates)>1:
plot_modes = ['Join Recordings', 'Separate Recordings']
else: plot_modes = [None]
continue_flag = True
if wt == 'Individual': mode = None
elif mode == None and (func == 'spectrogram' or func == 'wavelet'):
dpg.add_text(default_value=f"{func} only available in Combined Analysis for Event-lock based signals", show= True, parent=tab_label)
continue_flag = False
# dpg.add_group(parent=f"{tab_tag}_features_group", tag = f"{tab_tag}_df_group")
if continue_flag == True:
match func:
case 'spectrogram':
if plot_modes[0] != None:
if not dpg.does_item_exist(f"{tab_label}_deviation"): dpg.add_radio_button(items=plot_modes,label='Plot Mode', parent=tab_label, callback=choose, user_data=(tags, tab_label, func, tab_tag, parameter_values, mode), tag = f"{tab_label}_deviation")
else: plot_spectrogram_og(tags, tab_label, func, tab_tag, parameter_values, mode)
case 'wavelet':
if plot_modes[0] != None:
if not dpg.does_item_exist(f"{tab_label}_deviation"): dpg.add_radio_button(items=plot_modes,label='Plot Mode', parent=tab_label, callback=choose, user_data=(tags, tab_label, func, tab_tag, parameter_values, mode), tag = f"{tab_label}_deviation")
else:
plot_wavelet(tags, tab_label, func, tab_tag, parameter_values, mode=mode)
case 'multitaper coherence': plot_multitaper_coherence(parameter_values,tab_tag,tags)
case 'cross-correlation': plot_cross_correlation(parameter_values,tab_tag,tags,mode='df')
case 'PAC': plot_pac(tags,tab_tag,parameter_values,mode='df')
case 'coherence': plot_df_coherence(parameter_values,tab_tag,tags)
case _: plot_featured_signal(signal_tags, tab_label, func, tab_tag, parameter_values,mode)
dpg.configure_item(f"window_{window_id}", autosize=False)
[docs]def CustomFeatures(sender,app_data,user_data):
tab_tag, signal_tags= user_data
window_id = extract_window_id(tab_tag)
dpg.configure_item(f"window_{window_id}", autosize=True)
parameter_values = {}
input_fields = {} # Store input field IDs
tags = getActiveSeries(signal_tags)
events = [getEventsbyTag(tab_tag,serie) for serie in tags]
if all(e is None for e in events):events = None
if events: mode = 'segment'
else: mode = None
tab_label = f"{tab_tag}_{app_data}"
dates = []
for tag in tags:
signal = getSignalbyIdx(window_id,tab_tag,tag,mode)
dates.append(signal['Date'])
labels = [[dpg.get_item_label(tag), tag] for tag in tags]
bands = list(getBandsByTab(tab_tag).keys())
dates = np.unique(dates)
if len(dates)>1:
plot_modes = ['Join Recordings', 'Separate Recordings']
else: plot_modes = [None]
if dpg.does_item_exist(f"{tab_tag}_ft_input"):
dpg.delete_item(f"{tab_tag}_ft_input", children_only=True)
param_container = f"{tab_tag}_ft_input"
dpg.delete_item(f"{tab_tag}_ft_doc", children_only=True)
doc_container = f"{tab_tag}_ft_doc"
else:
ch = dpg.add_tree_node(label='Custom Parameters',parent=f"{tab_tag}_custom_features",default_open=True, tag = f"{tab_tag}_tree_features")
group = dpg.add_group(parent=ch, tag = f"{tab_tag}_cf_group", horizontal=True)
param_container = dpg.add_child_window(width=200, height=300, tag = f"{tab_tag}_ft_input", parent=group) # Space to add inputs dynamically
doc_container = dpg.add_child_window(width=600, height=300, tag = f"{tab_tag}_ft_doc", parent=group)
def create_input_fields(sender, app_data):
'''Dynamically create input fields for selected function.'''
if app_data == 'multitaper coherence':
params, documentation = fill_params_features('dpss')
elif app_data == 'cross-correlation':
params, documentation = fill_params_features('correlate')
elif app_data == 'PAC':
params, documentation = fill_params_features('hilbert')
else: params, documentation = fill_params_features(app_data)
documentation = pre.extract_params_and_returns(documentation)
if app_data == 'multitaper coherence':
for k in ['step', 'nfft','y']:
params[k] = None
elif app_data == 'cross-correlation':
params['method'] = 'auto'
params['mode'] = 'full'
params['dislocation'] = None
params['normalization'] = None
elif app_data == 'PAC':
extra = "\nInput: \n ________\n s1: signal 1\n s2: signal 2\n b1: Phase Band\n b2: Amplitude Band\n"
documentation = extra + documentation
for k in ['s1', 's2','b1', 'b2','n_bins']:
params[k] = 'choose'
dpg.add_text(documentation, parent=doc_container, tag = f"{doc_container}_doc")
if not params:
dpg.add_text("No parameters found", parent=param_container)
return
dpg.add_text("Required parameters: ", parent=param_container)
parameter_values.clear() # Reset parameter storage
input_fields.clear() # Reset input field storage
combos = {}
params = {k: params[k] for k in sorted(params)}
for param, default in params.items():
# Set defaults
if param == 'x' or param == 'data':
default = '---'
if param == 'y' and (app_data == 'coherence' or app_data=='multitaper coherence'):
default = '---'
if param == 'fs' or param == 'sfreq':
default = 250
if param == 'output':
default = 'power' # wavelet
if param == 'freqs':
default = 100 # wavelet
if param != 'window':
if ((param == 'x' or param == 'y') and
(app_data == 'coherence' or app_data=='multitaper coherence')) or (
(param=='a' or param=='v') and app_data=='cross-correlation') or (
app_data == 'PAC' and (param=='s1' or param=='s2')
):
# Use a combo box (select box) for this special case
input_id = dpg.add_combo(
label=param,
items=[l[0] for l in labels],
default_value=default,
parent=param_container,
callback=different_combo,
tag = f"{tab_label}_{param}",
user_data=(doc_container, combos)
)
combos[param] = input_id
elif app_data=='PAC' and (param=='b1' or param=='b2'):
input_id = dpg.add_combo(
label=param,
items=bands, # <-- customize your options
default_value=default,
callback=different_combo,
user_data=(doc_container, combos),
tag = f"{tab_label}_{param}",
parent=param_container,
)
combos[param] = input_id
elif (param=='x' and app_data in ['dpss', 'spectrogram','welch','periodogram']):
combos[param] = input_id
elif (param == 'data' and app_data=='wavelet'): # or (param=='x' and app_data in ['dpss', 'spectrogram','welch','periodogram']):
continue
else:
# Default to input text
input_id = dpg.add_input_text(
label=param,
default_value=default if default is not None else " ",
parent=param_container
)
input_fields[param] = input_id # Store input field reference
# Add save button
dpg.add_button(label="Save Parameters", callback=save_parameters, parent=param_container, user_data = app_data)
def save_parameters(sender, app_data, user_data):
'''Saves input values to the dictionary and prints them.'''
parameter_values.clear() # Reset before saving
for param, input_id in input_fields.items():
value = dpg.get_value(input_id)
if (param == 'x' or param == 'y') and user_data == 'coherence':
ls = [l[0] for l in labels]
index = ls.index(value)
tag = labels[index][1]
signal = getSignalbyIdx(window_id,tab_tag,tag)
channel = extract_channel_from_series_tag(tag)
extra = {'Channel': channel}
signal = signal | extra
parameter_values[param] = signal
else: parameter_values[param] = value
if not dpg.does_item_exist(f"{tab_tag}_plot_features"):
dpg.add_button(label="Plot filtered signal", callback=apply_filt, parent=param_container, user_data=user_data, tag=f"{tab_tag}_plot_features")
def apply_filt(sender, app_data, user_data):
func = user_data
tags = getActiveSeries(signal_tags)
tab_label = f"{tab_tag}_{func}"
if not dpg.does_item_exist(f"{tab_tag}_features_bar"):
dpg.add_tab_bar(reorderable=True,parent=f"{tab_tag}_features", tag = f"{tab_tag}_features_bar")
if dpg.does_item_exist(tab_label):
dpg.delete_item(tab_label, children_only=True)
dpg.configure_item(tab_label,show=True)
else:
dpg.add_tab(label=f"{func}", parent=f"{tab_tag}_features_bar", tag=tab_label, closable=True)
match func:
case 'spectrogram':
if plot_modes[0] != None:
if not dpg.does_item_exist(f"{tab_label}_deviation"): dpg.add_radio_button(items=plot_modes,label='Plot Mode', parent=tab_label, callback=choose, user_data=(tags, tab_label, func, tab_tag, parameter_values, mode), tag = f"{tab_label}_deviation")
else: plot_spectrogram_og(tags, tab_label, func, tab_tag, parameter_values, mode)
case 'wavelet':
if plot_modes[0] != None:
if not dpg.does_item_exist(f"{tab_label}_deviation"): dpg.add_radio_button(items=plot_modes,label='Plot Mode', parent=tab_label, callback=choose, user_data=(tags, tab_label, func, tab_tag, parameter_values, mode), tag = f"{tab_label}_deviation")
else:
plot_wavelet(tags, tab_label, func, tab_tag, parameter_values)
case 'multitaper coherence': plot_multitaper_coherence(parameter_values,tab_tag,tags,flag='flag')
case 'PAC': plot_pac(tags,tab_tag,parameter_values)
case 'coherence': plot_coherence(parameter_values,tab_label)
case 'cross-correlation': plot_cross_correlation(parameter_values,tab_tag,tags)
case _: plot_featured_signal(signal_tags, tab_label, func, tab_tag, parameter_values)
create_input_fields(sender,app_data)
dpg.configure_item(f"window_{window_id}", autosize=False)
[docs]def insert_events(sender, app_data, user_data):
window_id, tab_tag, signal_tags= user_data
if dpg.does_item_exist(f"{tab_tag}_emin"):
min = int(dpg.get_value(f"{tab_tag}_emin"))*250
max = int(dpg.get_value(f"{tab_tag}_emax"))*250
window_type = extract_type_window(window_id)
def read_txt(sender, app_data, user_data):
window_id, tab_tag, signal_tags = user_data
if dpg.does_item_exist(f'{tab_tag}_events_present'):
dpg.delete_item(f'{tab_tag}_events_present')
streaming_file = app_data['file_path_name']
events = utl.read_event_file(streaming_file)
scatter, dates = [], []
series = getActiveSeries(signal_tags)
window_id = extract_window_id(tab_tag)
scale = dpg.get_item_configuration(f"x_axis_{tab_tag}")['scale']
for i,tag in enumerate(series):
signal = getSignalbyIdx(window_id,tab_tag,tag)
if window_type == 'Combined':
file = extract_file_from_series_tag(tag)
if 'Events' not in app_state['windows'][window_id]['Files'][file]['Data'][tab_tag].keys():
app_state['windows'][window_id]['Files'][file]['Data'][tab_tag]['Events'] = []
else:
idx = extract_idx_from_series_tag(tag)
if 'Events' not in app_state['windows'][window_id]['Data'][tab_tag]['Signals'][idx].keys():
app_state['windows'][window_id]['Data'][tab_tag]['Signals'][idx]['Events'] = []
date = signal['Date']
channel = signal['Channel']
channel = getUpdatedElectrodeName(channel)
dates.append(f"{date.year}-{date.month}-{date.day}")
if scale == 0: #in linear
offset = DH.extract_time_offset(getDatabyTag(tag)) #already in timedelta format
new_time = [date + timedelta(seconds=val) for val in signal['X'] if isinstance(val,(int,float))]
new_x = [(d+offset).timestamp() for d in new_time]
updateKeySignalbyIdx(new_x,tab_tag,series[i],'X')
dpg.configure_item(f"x_axis_{tab_tag}",scale=dpg.mvPlotScale_Time)
dpg.configure_item(series[i], x=new_x)
else:
new_x = signal['X']
new_time = [pre.get_date_from_ts(val) for val in new_x]
for _, event in enumerate(events):
# hour, minute, second, type = event
dtt, type = event
if (dtt.year != 1970) and (utl.extract_date(date) != utl.extract_date(dtt)): #confirms that theres a real date, not buffer, and checks with the string to minimize verify idx if its not correspondant to the session
continue
hour, minute, second = dtt.hour, dtt.minute, dtt.second
tidx = utl.find_closest_index(new_time,hour,minute, second)
if tidx != None:
tidx = int(tidx)
scatter.append([new_x[tidx], type, new_time[tidx]])
date_event = new_time[tidx]
if dpg.does_item_exist(f"{tab_tag}_emin"):
imin = tidx-min
imax = tidx+max
if imin<0:
imin = 0
imax = tidx+max+min
if imax>len(new_x)-1:
imax = len(new_x)-1
imin = imax-(max+min)
segment_x = new_x[imin:imax]
new_y = signal['Y'][imin:imax]
x_shift = segment_x[min] #mid_index
aligned_x = [xi - x_shift for xi in segment_x]
segment = {'X': aligned_x,'Y':new_y,'Channel':channel,'Type':type, 'Date': date_event, 'Idx': tidx, 'Serie': i}
else:
segment = {'Channel':channel,'Type':type, 'Date': date_event, 'Idx': tidx, 'Serie': i}
if window_type == 'Combined': app_state['windows'][window_id]['Files'][file]['Data'][tab_tag]['Events'].append(segment)
else: app_state['windows'][window_id]['Data'][tab_tag]['Signals'][idx]['Events'].append(segment)
if scatter:
if window_type == 'Combined':
align_events(sender,app_data,(tab_tag,signal_tags))
else:
event_types = [s[1] for s in scatter]
event_types = np.unique(event_types).tolist()
for i, point in enumerate(scatter):
idx, etype, time = point
tag = f"{tab_tag}_{etype}_{time}"
if not dpg.does_item_exist(tag):
m = dpg.add_inf_line_series(parent=f"y_axis_{tab_tag}", x=[idx], label=f"{etype} - {time}", tag=tag)
dpg.bind_item_theme(m, change_inf_line_color(event_types.index(etype)))
signal_tags[tag] = True
dpg.configure_item(f"x_axis_{tab_tag}",label='Time (MM/DD/YYYY UTC)')
scatter = np.array(scatter, dtype=object)
types = np.unique(scatter[:,1]).tolist()
if len(types)>1:
if dpg.does_item_exist(f"{tab_tag}_By Recording:_buttons"):
dpg.delete_item(f"{tab_tag}_By Recording:_buttons")
new_parent = f"{tab_tag}_types"
if dpg.does_item_exist(f"{new_parent}_events"):
children = dpg.get_item_children(f"{new_parent}_events")
for _, child in children.items():
for kid in child:
label = dpg.get_item_label(kid)
if label not in types and label != '': types.append(label)
dpg.delete_item(f"{new_parent}_events")
with dpg.child_window(parent=new_parent, tag = f"{new_parent}_events",height=cwh):
dpg.add_text('By Event:')
for day in types:
toggleType(signal_tags,parent=f"{new_parent}_events",user_data=(day, window_id, tab_tag))
dpg.configure_item(f"{new_parent}_events", width=cww,horizontal_scrollbar=True)
else:
if not dpg.does_item_exist(f'{tab_tag}_events_present'):
dpg.add_text("No Events in the present recordings!", parent=f"{tab_tag}_add_events", tag=f'{tab_tag}_events_present')
dpg.bind_item_theme(f'{tab_tag}_events_present',change_text_color(1))
dpg.fit_axis_data(f"x_axis_{tab_tag}")
def create_file_dialog(window_id,tab_tag, signal_tags):
with dpg.file_dialog(
directory_selector=False,
show=True,
callback=read_txt,
width=700,
height=400,
modal=True,
default_path=directories['events'],
user_data=(window_id,tab_tag, signal_tags)
) as dialog_id:
dpg.add_file_extension(".txt", color=(0, 255, 0, 255), custom_text="[TEXT File]")
create_file_dialog(window_id,tab_tag, signal_tags)
[docs]def align_events(sender,app_data, user_data):
tab_tag, signal_tags = user_data
window_id = extract_window_id(tab_tag)
window_type = extract_type_window(window_id)
parent = f"y_axis_{tab_tag}"
all_events, edx, fdx = [], [], []
if window_type == 'Combined':
files = app_state['windows'][window_id]['Files']
for idx,_ in enumerate(files):
count = 0
info = files[idx]['Data'][tab_tag]
if isinstance(info, list): continue #if the file has this type of recording
if 'Events' in info.keys():
events = info['Events']
if len(events)>1: #why only 1?
for event in events:
all_events.append(event)
edx.append(count)
count+=1
fdx.append(idx)
dpg.delete_item(parent,children_only=True)
signal_tags.clear()
dpg.configure_item(f"x_axis_{tab_tag}", scale=dpg.mvPlotScale_Linear)
signal_tags = plot_td(all_events,parent,tab_tag, signal_tags,files=fdx,events=True, count=edx)
# else: dpg.add_text('To compare events, needs at least 2 events', parent=parent)
dpg.configure_item(f"{tab_tag}_add_events", show = False)
# dpg.configure_item(f"{tab_tag}_all_streams", show = False)
dpg.delete_item(f"{tab_tag}_tav")
toggleVisibility(signal_tags, tab_tag, group=f"{tab_tag}_all_streams", user_data=window_id)
dpg.configure_item(f"{tab_tag}_segmentation", show = False)
else:
signals = app_state['windows'][window_id]['Data'][tab_tag]['Signals']
events = []
for idx,_ in enumerate(signals):
temp = getEventsbyIdx(tab_tag,idx)
for t in temp: events.append(t)
if len(events)>1:
dpg.delete_item(parent,children_only=True)
signal_tags.clear()
dpg.configure_item(f"x_axis_{tab_tag}", scale=dpg.mvPlotScale_Linear)
plot_td(events,parent,tab_tag, signal_tags,events=True)
# else: dpg.add_text('To compare events, needs at least 2 events', parent=parent)
[docs]def changeTimelinePlot(sender,app_data,user_data):
signal_tags, signals, data, plot_tag = user_data
# signals = getActiveSignals(tab_tag,signal_tags)
old = list(dpg.get_item_user_data(f"{plot_tag}_export_plot"))
ploted_colors = old[-1]
for idx, signal in enumerate(signals):
for tag in signal_tags.keys():
if isinstance(tag,int) or 'series' not in tag: continue
i = extract_idx_from_series_tag(tag)
if i == idx and signal_tags[tag]==True:
if dpg.does_item_exist(f"{tag}_Left_added"):
dpg.configure_item(f"{tag}_Left_added",show=app_data)
dpg.configure_item(f"{tag}_Right_added",show=app_data)
else:
if isinstance(data,list):
dictionary = data[idx]
else: dictionary = data
if dictionary != None:
extra = DH.correctMissingTimeline(dictionary, signal,'X_missing')[0]
for side in ['Left','Right']:
tagg = f"{tag}_{side}_added"
extra_x, extra_y = extra[side]['X_missing'], extra[side]['Y_missing']
extra_x = [x.timestamp() for x in extra_x]
if extra_x != []:
if side=='Left':
color_id=-2
else: color_id=-6
ploted_colors.append(colors[color_id])
s = dpg.add_scatter_series(extra_x,extra_y,tag = tagg, parent=plot_tag)
dpg.bind_item_theme(s,change_plot_color(color_id))
old[-1] = ploted_colors
dpg.set_item_user_data(f"{plot_tag}_export_plot",old)
[docs]def circadian_rhythm(sender, app_data, user_data):
tab_tag, signal_tags, data = user_data
tab_label = tab_tag+'_circadian'
if not dpg.does_item_exist(f"{tab_tag}_features_bar"):
dpg.add_tab_bar(reorderable=True,parent=f"{tab_tag}_features", tag = f"{tab_tag}_features_bar")
if dpg.does_item_exist(tab_label):
if not dpg.get_item_configuration(tab_label)['show']:
dpg.delete_item(tab_label)
return
dpg.add_tab(label=f"Circadian Rhythm", parent=f"{tab_tag}_features_bar", tag=tab_label,closable=True)
tags = getActiveSeries(signal_tags)
if dpg.does_item_exist(f"{tab_tag}_missing_points"):
missing=dpg.get_value(f"{tab_tag}_missing_points")
else: missing=False
days = {}
mean, dates, circadian_tags = [], [], []
#preparing streams
for idx,tag in enumerate(tags):
signal = getSignalbyIdx(extract_window_id(tab_tag),tab_tag,tag)
old_x,old_y = dpg.get_value(tag)[:2]
if missing:
dictionary = getDatabyTag(tag)
side = extract_side_from_series_tag(tag).capitalize()
filtered = {side:{'X': [datetime.fromtimestamp(xx) for xx in old_x], 'Y':old_y},'Date':signal['Date']}
if dictionary != None:
# extra = DH.correctMissingTimeline(dictionary, signal)[0][side]
extra = DH.correctMissingTimeline(dictionary, filtered,days=filtered['Date'],filtered=filtered)[0][side]
old_x = [temp_x.timestamp() for temp_x in extra['X']]
old_y = extra['Y']
label = dpg.get_item_label(tag)
stim_info = DH.info_electrodes(getDatabyTag(tag))['Sense_frequency']
dates.append(label[-10:])
x = sorted(old_x)
if x[0] > old_x[0]:
old_y = list(reversed(old_y))
old_x = list(reversed(old_x))
if isinstance(old_x[0],(int,float)):
old_x = [utl.get_timestamp_from_dt(str(pre.get_date_from_ts(xx).time())) for xx in x]
elif isinstance(old_x[0], str):
old_x = [utl.get_timestamp_from_dt(xx) for xx in x]
else: #datetime
old_x = [utl.get_timestamp_from_dt(utl.full_date2str(xx)) for xx in x]
x,y = [], []
for n in range(len(old_x)-1):
x.append(old_x[n])
y.append(old_y[n])
if old_x[n+1]-old_x[n]>660:
gap_times = np.arange(old_x[n]+600, old_x[n+1], 600)
x.extend(gap_times)
y.extend([np.nan]*len(gap_times))
x.append(old_x[-1])
y.append(old_y[-1])
last = utl.get_timestamp_from_dt("23:59:00")
if x[0]>600: #day starts after theoric first point (00h10)
if x[0]>last:
x[0] = x[0]-last
diff = int((x[0]-600)//600)+1
nan_array = [np.nan]*diff
y = nan_array + y
x = list(np.arange(x[0]-(diff*600),x[0],600)) + x
if x[-1]<last: #day ends before theoric first point (23h50)
diff = int((last - x[-1])//600)+1
nan_array = [np.nan]*diff
y = y + nan_array
x = x + list(np.arange(x[-1],last,600))
x=x[:144] #6timestamps every 24hours
y=y[:144]
days[tag] = [x,y,stim_info]
mean.append(y)
std = np.nanstd(mean,axis=0)
mean = np.nanmean(mean,axis=0)
minus = mean - std
plus = mean + std
def toggle(sender,app_data,user_data):
label = dpg.get_item_label(sender)
new_mean = []
if dpg.get_value(f"{tab_label}_hide") == True:
callback = dpg.get_item_callback(f"{tab_label}_hide")
dpg.set_value(f"{tab_label}_hide",False)
if callback:
callback(f"{tab_label}_hide", None, dpg.get_item_user_data(f"{tab_label}_hide"))
toggle_tags = [f"{tag}_{tab_label}" for tag in getActiveSeries(tags)]
for t in toggle_tags:
taglabel = dpg.get_item_label(t)
if label in taglabel:
dpg.configure_item(t,show=app_data)
show = dpg.get_item_configuration(t)['show']
if show:
new_mean.append(dpg.get_value(t)[1])
if len(toggle_tags)<1:
length = len(dpg.get_value(f"{tab_label}_mean")[0])
new_mean = np.zeros(length)
dpg.configure_item(f"{tab_label}_mean",y=new_mean)
dpg.configure_item(f"{tab_label}_std",y1=new_mean,y2=new_mean)
return
new_std = np.nanstd(new_mean,axis=0)
new_mean = np.nanmean(new_mean,axis=0)
new_minus = new_mean - new_std
new_plus = new_mean + new_std
dpg.configure_item(f"{tab_label}_mean",y=new_mean)
dpg.configure_item(f"{tab_label}_std",y1=new_minus,y2=new_plus)
new_x = dpg.get_value(f"{tab_label}_mean")[0]
sign = {'X':new_x,'Y':new_mean}
info = ft.extract_timeline_features(sign)
trial = 'Mean'
if not app_data:
trial = trial + ' wo/ ' + label
if not isinstance(user_data,str):
create_table(f"{tab_label}_header",f"{tab_label}_table", info, trial_label=trial)
with dpg.group(parent=tab_label, horizontal=True, tag=f"{tab_label}_CR"):
plot_tag = f"{tab_label}_plot"
with dpg.plot(label = "Circadian Rhythm", tag = plot_tag, width=800):
dpg.bind_colormap(plot_tag, 0)
dpg.add_plot_legend(location=dpg.mvPlot_Location_NorthEast)
x_axis = dpg.add_plot_axis(dpg.mvXAxis, label='Time (MM/DD/YYYY UTC)', scale = dpg.mvPlotScale_Time) #tag=f"{tab_label}_x_axis"
y_axis = dpg.add_plot_axis(dpg.mvYAxis, label="LFP Power (µVp)", tag= f"{tab_label}_y_axis")
for tag, val in days.items():
label = dpg.get_item_label(tag)
circadian_tags.append(f"{tag}_{tab_label}")
dpg.add_line_series(val[0],val[1],label=label,parent=y_axis, tag = f"{tag}_{tab_label}",skip_nan=False)
rgba = dpg.get_colormap_color(0,list(days.keys()).index(tag))
dpg.bind_item_theme(f"{tag}_{tab_label}",create_alpha_theme(alpha=0.8,color=rgba))
new_x = [pre.get_date_from_ts(xx) for xx in val[0]]
sign = {'X':new_x,'Y':val[1],'Sense':val[2]}
if not dpg.does_item_exist(f"{tab_label}_header"):
dpg.add_collapsing_header(tag=f"{tab_label}_header", parent=tab_label, label = 'Circadian Metrics')
s = dpg.add_shade_series(val[0],y1=minus,y2=plus,parent=y_axis,label='Std. Dev',tag=f"{tab_label}_std")
dpg.bind_item_theme(s,create_alpha_theme())
m = dpg.add_line_series(val[0],mean,parent=y_axis,label='Mean',tag=f"{tab_label}_mean",skip_nan=False)
dpg.bind_item_theme(m,change_line_thickness(4))
sign = {'X':new_x,'Y':mean}
info = ft.extract_timeline_features(sign)
create_table(f"{tab_label}_header",f"{tab_label}_table", info, trial_label='Mean')
dates = sorted(np.unique(dates).tolist())
checkboxes = []
with dpg.group():
dpg.add_checkbox(label='Hide Days', tag = f"{tab_label}_hide", callback=hide,parent = f"{tab_label}_vbuttons", user_data=(circadian_tags,checkboxes))
with dpg.child_window(height=cwh, auto_resize_x = True):
a = dpg.add_text('By Day:')
for d in dates:
c = dpg.add_checkbox(label=d, default_value=True, callback=toggle)
checkboxes.append(c)
dpg.configure_item(dpg.get_item_parent(a),auto_resize_x=False,resizable_x=True)
with dpg.child_window(height=cwh,width=cww,horizontal_scrollbar=True):
a = dpg.add_text('By Hemisphere:')
for side in ['Left', 'Right']:
dpg.add_checkbox(label=side, default_value=True, callback=toggle)
dpg.configure_item(dpg.get_item_parent(a),auto_resize_x=False,resizable_x=True)
dpg.add_separator(label='Export',parent=tab_label)
b1 = dpg.add_button(label = 'Export Plot Figure',width=-1,callback = filename_window,user_data =('2D', plot_tag, y_axis,0),parent=tab_label)
b2 = dpg.add_button(label='Export Signals',width=-1,callback=filename_window,parent=tab_label,user_data=(y_axis,plot_tag,'2D'))
add_exporting_tag(extract_window_id(tab_tag),plot=b1,signal=b2)
[docs]def stimulation_timeline(sender,app_data,user_data):
tab_tag, signal_tags, data = user_data
tab_label = tab_tag+'_stimulation'
window_id = extract_window_id(tab_tag)
if not dpg.does_item_exist(f"{tab_tag}_features_bar"):
dpg.add_tab_bar(reorderable=True,parent=f"{tab_tag}_features", tag = f"{tab_tag}_features_bar")
if dpg.does_item_exist(tab_label):
if not dpg.get_item_configuration(tab_label)['show']:
dpg.delete_item(tab_label)
return
dpg.add_tab(label=f"Stimulation", parent=f"{tab_tag}_features_bar", tag=tab_label,closable=True)
if extract_type_window(window_id) == 'Combined':
files = [i for i in range(len(app_state['windows'][window_id]['Files']))]
files = sorted(files,key=lambda f: getDatabyFile(window_id, f)['Device']['SessionDate'])
else: files = [None]
copy = {'X': [], 'Y': [], 'Info':[]}
stimulation = {side: {'X': [], 'Y': [], 'Info':[]} for side in ['Left','Right']}
for side in ['Left','Right']:
temp = stimulation[side]
for file in files:
stimus = DH.getStimulation(getDatabyFile(window_id,file),side=side)
for x in stimus['X']: temp['X'].append(x)
for x in stimus['Y']: temp['Y'].append(x)
for x in stimus['Info']:
if temp['Info']!= [] and x['Date'] == temp['Info'][-1]['Date']:
temp['Info'][-1] = x
continue
temp['Info'].append(x)
with dpg.group(parent=tab_label, horizontal=True):
plot_tag = f"{tab_label}_plot"
with dpg.plot(label = "Stimulation", tag = plot_tag, width=500, height=400):
dpg.bind_colormap(plot_tag, 0)
dpg.add_plot_legend(location=dpg.mvPlot_Location_NorthEast)
x_axis = dpg.add_plot_axis(dpg.mvXAxis, label='Time (MM/DD/YYYY UTC)', scale = dpg.mvPlotScale_Time) #tag=f"{tab_label}_x_axis"
y_axis = dpg.add_plot_axis(dpg.mvYAxis, label="Stimulation Amplitude (mA)", tag= f"{tab_label}_y_axis")
for side in ['Left','Right']:
temp = stimulation[side]
x = [utl.parse_datetime(x).timestamp() for x in temp['X']]
y = temp['Y']
l = dpg.add_line_series(x,y,parent=y_axis,label=f'{side} Hemisphere')
dpg.bind_item_theme(l,change_line_thickness(4))
with dpg.group():
dpg.add_text('Stimulation Information')
for side in ['Left','Right']:
with dpg.collapsing_header(tag=f"{tab_label}_{side}_information", label=f'{side} Hemisphere'):
for info in stimulation[side]['Info']:
trial = info['Date']
info.pop('Date')
create_table(f"{tab_label}_{side}_information",f"{tab_label}_{side}_information_table",info,trial_label=trial)
dpg.add_separator(label='Export',parent=tab_label)
b1 = dpg.add_button(label = 'Export Plot Figure',parent=tab_label,width=-1,callback = filename_window,user_data =('2D', plot_tag, y_axis,0))
b2 = dpg.add_button(label='Export Signals',width=-1,callback=filename_window,parent=tab_label,user_data=(y_axis,plot_tag,'Stimulation'))
add_exporting_tag(extract_window_id(tab_tag),plot=b1,signal=b2)
[docs]def AnalyzeTimelineEvents(sender,app_data,user_data):
tab_tag, events_tags, signal_tags = user_data
event_ids = [dpg.get_value(e[0])[0] for e in events_tags]
event_labels = [dpg.get_item_label(e[0]) for e in events_tags]
timelines = [dpg.get_value(t)[:2] for t in signal_tags.keys()]
timeline_labels = [dpg.get_item_label(t) for t in signal_tags.keys()]
if dpg.does_item_exist(f"{tab_tag}_missing_points") and dpg.get_value(f"{tab_tag}_missing_points"):
timelines = []
for tag in signal_tags.keys():
dictionary = getDatabyTag(tag)
signal = getSignalbyIdx(extract_window_id(tab_tag),tab_tag,tag)
side = extract_side_from_series_tag(tag).capitalize()
if dictionary != None:
extra = DH.correctMissingTimeline(dictionary, signal)[0][side]
corrected_x = [temp_x.timestamp() for temp_x in extra['X']]
timeline.append([corrected_x,extra['Y']])
segments, event_names, checkboxes, days, temp_tags = [], [], [], [], []
def cut_segment(x,y,event):
diff = [abs(xx-event) for xx in x]
ev_id = diff.index(min(diff))
half_window = 6
if ev_id >= half_window and ev_id <= len(x) - half_window - 1:
start = ev_id - half_window
end = ev_id + half_window + 1
segment_x = x[start:end]
segment_y = y[start:end]
elif ev_id < half_window:
# Pad at the start
pad = np.full(half_window - ev_id, np.nan)
segment_x = np.concatenate([pad, x[0:ev_id + half_window + 1]])
segment_y = np.concatenate([pad, y[0:ev_id + half_window + 1]])
else:
# Pad at the end
pad = np.full(ev_id + half_window + 1 - len(x), np.nan)
segment_x = np.concatenate([x[ev_id - half_window:], pad])
segment_y = np.concatenate([y[ev_id - half_window:], pad])
onex = segment_x[0]
segment_x = [x - onex for x in segment_x]
if y == []:
return None
return segment_x,segment_y
for ex, event in enumerate(event_ids):
event = event[0]
for timeline in timelines:
x, y = timeline[0], timeline[1]
first, last = min(x), max(x)
label = f"{event_labels[ex]} - {timeline_labels[timelines.index(timeline)]}"
if not (first <= event <= last):
continue
segment = cut_segment(x,y,event)
if segment is None: continue
segments.append([segment,label])
event_names.append(label)
date = utl.extract_date(pre.get_date_from_ts(event))
days.append(date)
values = [segment[0][1] for segment in segments]
mean_value = np.nanmean(values, axis = 0)
std_dev = np.nanstd(values, axis = 0)
tab_label = tab_tag+'_events'
plot_tag = f'{tab_label}_plot'
def normal(segments, plot=True, bva=False):
for s, seg in enumerate(segments):
segment, label = seg
name = label[:label.find('-')]
name = getUpdatedEventName(name)
x,y=segment[0],segment[1]
if plot:
dpg.add_line_series(x,y,label=label,parent=y_axis,tag=f"{tab_label}_{label}_{s}")
temp_tags.append(f"{tab_label}_{label}_{s}")
else:
name = label[label.find(' | ')+3:]
name = getUpdatedEventName(name[:name.find('-')])
sign = {'X':x,'Y':y}
if 'Before' in label:
header = f"{tab_label}_{name}_before"
table = f"{tab_label}_table_before"
elif 'After' in label:
header = f"{tab_label}_{name}_after"
table = f"{tab_label}_table_after"
else:
header = f"{tab_label}_{name}_full"
table = f"{tab_label}_table_full"
parent = f"{tab_label}_{name}"
if not dpg.does_item_exist(parent):
dpg.add_tree_node(tag=parent, parent=tab_label,label=f"{name} Metrics")
for moment in ['Full','Before','After']:
this = f"{tab_label}_{name}_{moment.lower()}"
if not dpg.does_item_exist(this):
dpg.add_tree_node(tag=this, parent=parent,label=f'{moment}')
for side in ['Left','Right']:
if not dpg.does_item_exist(f"{this}_{side}"):
dpg.add_collapsing_header(label=f"{name} | {moment} | {side}",tag=f"{this}_{side}",parent=this)
if not isinstance(y,(list,np.ndarray)) or (np.all(y) == np.nan):
continue
info = ft.extract_timeline_features(sign)
if info != None:
equal = f"{tab_label}_{name}"
if 'Before' in label:
moment = '_before'
elif 'After' in label: moment = '_after'
else: moment = '_full'
if 'Right' in label: side = '_Right'
else: side = '_Left'
header = equal + moment + side
table = header + '_table'
label = label[-14:]
create_table(header,table, info, trial_label=label)
minus = mean_value - std_dev
plus = mean_value + std_dev
if plot and len(segments)>0:
std = dpg.add_shade_series(segment[0],minus, y2 = plus,label='Std. Dev', parent=y_axis, tag = f"{tab_label}_std")
dpg.bind_item_theme(std,create_alpha_theme())
mean = dpg.add_line_series(segment[0],mean_value,label='Mean',parent=y_axis, tag = f"{tab_label}_mean")
dpg.bind_item_theme(mean, change_line_thickness(5))
def bva(segments):
bva_segments = []
for segment, label in segments:
x,y = segment
before = [x[:6],y[:6]]
bva_segments.append([before,f"Before | {label}"])
after = [x[6:],y[6:]]
bva_segments.append([after,f"After | {label}"])
normal(bva_segments, plot=False, bva=True)
def toggle(sender,app_data,user_data):
name = dpg.get_item_label(sender)
mean_temp = []
c = f"{tab_label}_hide"
if dpg.get_value(c)==True:
dpg.set_value(c,False)
for s, tag in enumerate(segments):
t, label = tag
dpg.configure_item(f"{tab_label}_{label}_{s}",show=True)
for s, tag in enumerate(segments):
t, label = tag
if name in label:
dpg.configure_item(f"{tab_label}_{label}_{s}",show=app_data)
show = dpg.get_item_configuration(f"{tab_label}_{label}_{s}")['show']
if show:
mean_temp.append(t[1])
if len(segments)<1:
length = len(dpg.get_value(f"{tab_label}_mean")[0])
new_mean = np.zeros(length)
dpg.configure_item(f"{tab_label}_mean",y=new_mean)
dpg.configure_item(f"{tab_label}_std",y1=new_mean,y2=new_mean)
return
new_mean = np.nanmean(mean_temp,axis=0)
new_std = np.nanstd(mean_temp,axis=0)
minus = new_mean - new_std
plus = new_mean + new_std
dpg.configure_item(f"{tab_label}_std",y1=minus, y2=plus)
dpg.configure_item(f"{tab_label}_mean",y=new_mean)
mean_info = {'X': t[0], 'Y':new_mean}
mean_info = ft.extract_timeline_features(mean_info)
if not dpg.does_item_exist(f"{tab_label}_header"):
dpg.add_collapsing_header(tag=f"{tab_label}_header",label='Mean',parent=tab_label)
create_table(f"{tab_label}_header",f"{tab_label}_table", mean_info, trial_label=f'New Mean ~({name})')
if not dpg.does_item_exist(f"{tab_tag}_features_bar"):
dpg.add_tab_bar(reorderable=True,parent=tab_tag, tag = f"{tab_tag}_features_bar")
if dpg.does_item_exist(tab_label):
dpg.delete_item(tab_label,children_only=True)
else:
dpg.add_tab(label=f"Timeline Events", parent=f"{tab_tag}_features_bar", tag=tab_label)
e_names = np.unique(event_labels).tolist()
with dpg.group(parent=tab_label, tag=f"{plot_tag}_group", horizontal=True):
with dpg.plot(label = 'Event-locked Segments',tag = plot_tag, width=600, height=400):
dpg.bind_colormap(plot_tag, 0)
dpg.add_plot_legend(location=dpg.mvPlot_Location_NorthEast)
x_axis = dpg.add_plot_axis(dpg.mvXAxis, label="Time (MM/DD/YYYY UTC)", tag=f"x_axis_{plot_tag}", scale=dpg.mvPlotScale_Time)
y_axis = dpg.add_plot_axis(dpg.mvYAxis, label="LFP (µVP)", tag=f"y_axis_{plot_tag}")
max_timestamp = 2*60*60
dpg.set_axis_limits(x_axis, 0, max_timestamp)
normal(segments)
bva(segments)
with dpg.group(parent=f"{plot_tag}_group"):
with dpg.group(horizontal=True):
with dpg.child_window(width=cww,horizontal_scrollbar=True,height=cwh):
dpg.add_text('Hemisphere: ')
for side in ['Left', 'Right']:
c = dpg.add_checkbox(label=side,callback = toggle,default_value=True)
checkboxes.append(c)
with dpg.child_window(width=cww,horizontal_scrollbar=True,height=cwh):
dpg.add_text('Events: ')
for side in e_names:
c = dpg.add_checkbox(label=side,callback = toggle,default_value=True)
checkboxes.append(c)
with dpg.child_window(width=150,height=cwh):
dpg.add_text('Days: ')
days = sorted(np.unique(days).tolist())
for side in days:
c = dpg.add_checkbox(label=side,callback = toggle,default_value=True)
checkboxes.append(c)
dpg.add_separator()
dpg.add_checkbox(label='Show Mean Only', tag = f"{tab_label}_hide", callback=hide, user_data=(temp_tags,checkboxes))
b1 = dpg.add_button(label = 'Export Plot Figure',callback = filename_window,user_data =('2D', plot_tag, y_axis,0),width=-1)
b2 = dpg.add_button(label='Export Signals',width=-1,callback=filename_window,user_data=(y_axis,plot_tag,'2D'))
add_exporting_tag(extract_window_id(tab_tag),plot=b1,signal=b2)
[docs]def RemoveTimelineOutliers(sender,app_data,user_data):
tab_tag,signal_tags = user_data
window_id = extract_window_id(tab_tag)
tags = getActiveSeries(signal_tags)
factor = app_data
if dpg.does_item_exist(f"{tab_tag}_plot_progress"): dpg.delete_item(f"{tab_tag}_plot_progress")
# create_table(f"{tab_tag}_header",f"{tab_tag}_table", info, trial_label=label_left)
# dpg.delete_item(f"{tab_tag}_header", children_only=True)
dpg.add_progress_bar(parent=f"{tab_tag}_plot_group",overlay='Removing', default_value=0.0, width = 400, height = 50, tag = f"{tab_tag}_plot_progress")
for i, tag in enumerate(tags):
values = dpg.get_value(tag)
x,_= values[0], values[1]
data = getDatabyTag(tag)
info = dpg.get_item_configuration(tag)
series_label = info['label']
side = series_label.split(' ')[0]
stream = getSignalbyIdx(window_id,tab_tag,tag)
stream = DH.correctMissingTimeline(data,stream)[0]
new_x,y = stream[side]['X'],stream[side]['Y']
new_x = [utl.convert_to_timestamp(n) for n in new_x]
y = np.array(y)
copy = np.copy(y)
'''USING RMS'''
# threshold = np.sqrt(np.mean(np.array(y)**2))
# '''USING ITERATIVE RMS'''
rms = np.sqrt(np.mean(y**2))
for _ in range(3): # Iterate a few times
y = y[np.abs(y) <= factor * rms]
if len(y)>=(len(copy)/10)+1:
rms = np.sqrt(np.nanmean(y**2))
# threshold =rms
'''USING MEDIAN'''
mad = np.median(np.abs(y - np.median(y)))
# threshold = mad # Or some scaled version
threshold = rms
mask = copy > factor*threshold
copy[mask] = rms
dpg.set_value(tag,[new_x,copy.tolist()])
updateSignalbyIdx(copy.tolist(),tab_tag,tag,f'Filtered_{side}')
progress = np.round((i+1)/len(tags),2)
dpg.set_value(f"{tab_tag}_plot_progress", progress)
if progress == 1: dpg.configure_item(f"{tab_tag}_plot_progress", overlay = 'Removed')
[docs]def recalculate_timeline(sender,app_data,user_data):
tab_tag, tags, tables = user_data
if dpg.does_item_exist(f"{tab_tag}_rebar"): dpg.delete_item(f"{tab_tag}_rebar")
for table in tables:
dpg.delete_item(table,children_only=True)
mean = []
for tag in tags:
if not isinstance(tag,str): continue #means its a marked event
if not dpg.get_item_configuration(tag)['show']: continue
info, _ = calculate_timeline_features(tab_tag,tag)
mean.append(info)
mean_dict = {}
for key in mean[0].keys():
values = [m[key] for m in mean]
if isinstance(values[0], str):
values=[utl.get_timestamp_from_dt(t) for t in values]
mean_value = utl.extract_time(pre.get_date_from_ts(np.mean(values)))
mean_value = {key: mean_value}
else:
mean_value = {key: np.mean(values,axis=0)}
mean_dict = mean_dict | mean_value
create_table(f"{tab_tag}_header",f"{tab_tag}_table", mean_dict, trial_label='Mean')
[docs]def calculate_timeline_features(tab_tag,series_tag):
label = dpg.get_item_label(series_tag)
datestr = label[-10:]
side = label[:label.find(' ')].capitalize()
x,y = dpg.get_value(series_tag)[:2]
sign = {'X':x,'Y':y}
wt = extract_type_window(extract_window_id(tab_tag))
nodes = {f"{tab_tag}_hemisphere": "By Hemisphere"}
headers = {f"{tab_tag}_{side}": [f'{side} Hemisphere',f"{tab_tag}_hemisphere"],
f"{tab_tag}_header": ['All Individual Streams', tab_tag]}
if wt=='Combined':
channel, sensing_band = dpg.get_item_user_data(series_tag)
if channel is not None:
nodes[f"{tab_tag}_channel"] = 'By Channel'
headers[f"{tab_tag}_{channel}"] = [channel,f"{tab_tag}_channel"]
if sensing_band is not None:
nodes[f"{tab_tag}_sensing"] = 'By Sensing Band'
headers[f"{tab_tag}_{sensing_band}"] = [sensing_band,f"{tab_tag}_sensing"]
for tag, name in nodes.items():
if not dpg.does_item_exist(tag):
dpg.add_tree_node(label=name,parent=tab_tag,tag=tag,default_open=False)
for tag, data in headers.items():
name,parent = data
if not dpg.does_item_exist(tag):
dpg.add_collapsing_header(tag=tag,label=name,parent=parent,default_open=False)
'''if not dpg.does_item_exist(f"{tab_tag}_hemisphere"):
dpg.add_tree_node(tag=f"{tab_tag}_hemisphere",
label="By Hemisphere",
parent=tab_tag,
default_open=False
)
if not dpg.does_item_exist(f"{tab_tag}_{side}"):
dpg.add_collapsing_header(label = f'{side} Hemisphere', tag =f"{tab_tag}_{side}", parent = f"{tab_tag}_hemisphere")
if not dpg.does_item_exist(f"{tab_tag}_header"):
dpg.add_collapsing_header(tag=f"{tab_tag}_header", parent=tab_tag, label = 'All Streams Metrics')'''
info = ft.extract_timeline_features(sign)
create_table(f"{tab_tag}_header",f"{tab_tag}_table", info, trial_label=label)
create_table(f"{tab_tag}_{side}",f"{tab_tag}_{side}_table", info, trial_label=datestr)
if wt=='Combined':
if channel is not None:
create_table(f"{tab_tag}_{channel}",f"{tab_tag}_{channel}_table",info, trial_label=datestr)
if sensing_band is not None:
create_table(f"{tab_tag}_{sensing_band}",f"{tab_tag}_{sensing_band}_table",info, trial_label=label)
tables = list(headers.keys()).__add__(list(nodes.keys()))
return info, tables
#-------------------------------------GENERAL TABS-------------------------------------------
[docs]def device_window(Data, window_id, file_id = None):
wt = extract_type_window(window_id)
device = DH.getDeviceInfo(Data)
battery = DH.getBatteryInfo(Data)
battery_reminder = Data['Device']['BatteryReminder']
date = DH.getSessionDate(Data)
info_parent = f"device_parameteres_{window_id}"
dpg.configure_item(f"{info_parent}_title", show=True)
if wt == 'Individual':
dpg.delete_item(item=info_parent,children_only=True)
else:
dpg.configure_item(info_parent, show=True)
header_tag = info_parent + f'_{file_id}'
if dpg.does_item_exist(header_tag): dpg.delete_item(header_tag)
dpg.add_collapsing_header(tag=header_tag, parent=info_parent, label = date)
info_parent = header_tag
#WARNINGS
if battery_reminder==True:
w = dpg.add_text("WARNING: Battery needs recharge!", parent=f"window_{window_id}", show=True)
color = change_text_color(0)
dpg.bind_item_theme(w,color)
if int(device['OverdischargeCount'])>0:
w = dpg.add_text(f"Warning! Overdischarge count above 0", parent=f"window_{window_id}")
color = change_text_color(0)
dpg.bind_item_theme(w,color)
#INFO
dpg.add_text(f"Battery Charge: {battery['BatteryPercentage']}%", parent=info_parent, show=True)
dpg.add_text(f"Battery Status: {utl.after_point(battery['BatteryStatus'])}", parent=info_parent, show=True)
dpg.add_separator(parent=info_parent)
dpg.add_text(f"Neurostimulator: {device['Neurostimulator']} {device['NeurostimulatorModel']}", parent=info_parent, show=True)
dpg.add_text(f"Location: {utl.after_point(device['NeurostimulatorLocation'])}", parent=info_parent, show=True)
dpg.add_text(f"Overdischarge Count: {device['OverdischargeCount']}", parent=info_parent, show=True)
dpg.add_separator(parent=info_parent)
dpg.add_text('Versions', parent=info_parent)
dpg.add_text(f"Device Version: {device['DeviceVersion']}", parent=info_parent)
dpg.add_text(f"Product Version: {device['ProductVersion']}", parent=info_parent)
dpg.configure_item(info_parent, show=True)
[docs]def stimulation_window(Data, window_id, file_id = None):
wt = extract_type_window(window_id)
info_parent = f"stimulation_parameteres_{window_id}"
try:
stimulation = DH.info_electrodes(Data,version='Initial')
except Exception as e:
d = dpg.add_text('Stimulation OFF/not set up!', parent=info_parent)
dpg.bind_item_theme(d,change_text_color(1))
dpg.configure_item(f"{info_parent}_title", show=True)
dpg.configure_item(info_parent, show=True)
return
date = DH.getSessionDate(Data)
dpg.configure_item(f"{info_parent}_title", show=True)
if wt == 'Individual':
dpg.delete_item(item=info_parent,children_only=True)
else:
dpg.configure_item(info_parent, show=True)
header_tag = info_parent + f'_{file_id}'
if dpg.does_item_exist(header_tag): dpg.delete_item(header_tag)
dpg.add_collapsing_header(tag=header_tag, parent=info_parent, label = date)
info_parent = header_tag
try:
ch = stimulation['Sense_electrodes']
if ch in electrodes_names.keys():
ch = electrodes_names[ch]
else:
electrodes_names[ch] = ch
except Exception: pass
dpg.add_text(f"Active Group: {stimulation['Group_id']}", parent=info_parent)
dpg.add_text(f"Frequency: {stimulation['Frequency']} Hz", parent=info_parent)
dpg.add_text(f"Amplitude: {stimulation['Amplitude']} mA", parent=info_parent)
dpg.add_text(f"Pulse Width: {stimulation['Pulse']} µs", parent=info_parent)
dpg.add_text(f"Type: {stimulation['Type']}", parent=info_parent)
dpg.add_text(f"Electrodes: {stimulation['Stim_electrodes']}", parent=info_parent)
if 'PSD' in stimulation.keys():
dpg.add_separator(parent=info_parent)
d = dpg.add_text('At-home BrainSense: ON', parent=info_parent)
dpg.bind_item_theme(d,change_text_color(-1))
dpg.add_text(f"Sensing Channel: {getUpdatedElectrodeName(ch)}", parent=info_parent)
f = stimulation['Sense_frequency']
f1, f2 = np.round(f-2.5,2),np.round(f+2.5,2)
dpg.add_text(f"Sensing Band: [{f1}, {f2}] Hz", parent=info_parent)
else:
d = dpg.add_text('At-home BrainSense: OFF', parent=info_parent)
dpg.bind_item_theme(d,change_text_color(1))
dpg.configure_item(info_parent, show=True)
[docs]def session_window(Data, window_id, file_id = None):
wt = extract_type_window(window_id)
date = DH.getSessionDate(Data)
session_duration = DH.getSessionDuration(Data)
initial_stim_status, final_stim_status = DH.getStimStatus(Data)
impedance = DH.getImpedance(Data)
patient_info = DH.getPatientInfo(Data)
info_parent = f"session_parameteres_{window_id}"
dpg.configure_item(f"{info_parent}_title", show=True)
if not isinstance(impedance, str):
impedance_status, l, r = impedance
else:
impedance_status = impedance
l=False
if wt == 'Individual':
dpg.delete_item(item=f"session_parameteres_{window_id}",children_only=True)
else:
header_tag = info_parent + f'_{file_id}'
if dpg.does_item_exist(header_tag): dpg.delete_item(header_tag)
dpg.add_collapsing_header(tag=header_tag, parent=info_parent, label = date)
info_parent = header_tag
dpg.add_text(f"Session date: {date}", parent=info_parent, show=True)
dpg.add_text(f"Session duration: {session_duration}", parent=info_parent, show=True)
dpg.add_text(f"Initial Stimulation: {initial_stim_status}", parent=info_parent, show=True)
dpg.add_text(f"Final Stimulation: {final_stim_status}", parent=info_parent, show=True)
dpg.add_separator(parent=info_parent)
dpg.add_text(f"Patient Name: {patient_info['PatientFirstName']} {patient_info['PatientLastName']}", parent=info_parent, show=True)
dpg.add_text(f"Patient Date of Birth: {patient_info['PatientDateOfBirth']}", parent=info_parent, show=True)
dpg.add_text(f"Diagnosis: {utl.after_point(patient_info['Diagnosis'])}", parent=info_parent, show=True)
if patient_info['ClinicianNotes'] != "":
dpg.add_text(f"Notes: {patient_info['ClinicianNotes']}", parent=info_parent, show=True)
dpg.add_separator(parent=info_parent)
dpg.add_text(f"Impedance Status: {impedance_status}", parent=info_parent, show=True)
dpg.configure_item(info_parent, show=True)
return l
#--------------------------------------INDIVIDUAL TABS-----------------------------------------
[docs]def show_event_summary(mode,tab_tag,plot_tag,files=None):
window_id = extract_window_id(tab_tag)
wt = extract_type_window(window_id)
if wt == 'Individual':
Data = getDatabyTag(tab_tag)
daylight_savings = DH.extract_time_offset(Data)
temp = DH.getEventLog(Data)
names = [event['EventName'] for event in temp]
names = np.unique(names).tolist()
elif wt == 'Combined':
temp, names = [], []
for file in files:
Data = file['Data']
daylight_savings = DH.extract_time_offset(Data)
temp_file = DH.getEventLog(Data)
names_file = [event['EventName'] for event in temp_file]
for t in temp_file: temp.append(t)
for n in names_file: names.append(n)
names = np.unique(names).tolist()
names = [getUpdatedEventName(name) for name in names]
event_summary = {name: {} for name in names}
for event in temp:
eventname = getUpdatedEventName(event['EventName'])
# if utl.parse_datetime(previous_session).date()>utl.parse_datetime(event['DateTime']).date(): continue
if event['DateTime'] in event_summary[eventname].keys():
event_summary[eventname][event['DateTime']] = event_summary[eventname][event['DateTime']] +1
else:
event_summary[eventname][event['DateTime']] = 1
def toggle_plot(sender,app_data,user_data):
if app_data == user_data: return
if app_data=='Days':
values=[len(event_summary[key]) for key in event_summary.keys()] # numerical values
labels=list(event_summary.keys())
labels = [getUpdatedEventName(l) for l in labels]
title = 'Event Count'
dpg.bind_colormap(f"{tab_tag}_pie_plot", 0)
else:
new = {}
for _,events in event_summary.items():
for day in events.keys():
dt = int(utl.parse_datetime(day).hour)
if dt in new.keys():
new[dt] = new[dt] + events[day]
else:
new[dt] = events[day]
sorted_items = sorted(new.items())
labels, values = zip(*sorted_items)
labels = list(labels)
labels = [getUpdatedEventName(l) for l in labels]
values = list(values)
title = 'Event Circadian Rhythm'
dpg.bind_colormap(f"{tab_tag}_pie_plot", 3)
dpg.configure_item(f"{tab_tag}_pie",values=values,labels=labels)
dpg.configure_item(f"{tab_tag}_pie_plot",label=title)
for name, events in event_summary.items():
if app_data == 'Days':
x = [(utl.parse_datetime(key)+daylight_savings).timestamp() for key in events.keys()]
weight = 200
else:
x = [utl.get_timestamp_from_dt(key)+(daylight_savings.seconds) for key in events.keys()]
weight = 75
dpg.configure_item(f"{name}_{tab_tag}",x=x, weight=weight)
dpg.configure_item(f"{tab_tag}_xaxis",label=app_data)
dpg.set_item_user_data(f"axis_{tab_tag}",app_data)
with dpg.group(label = f"{tab_tag}_group", parent=tab_tag, horizontal=True ):
with dpg.group(width=600):
with dpg.plot(label=f"{mode} Signals", height=400, width=600, tag=plot_tag):
dpg.bind_colormap(plot_tag, 0)
dpg.add_plot_legend(location=dpg.mvPlot_Location_NorthEast)
x_axis = dpg.add_plot_axis(dpg.mvXAxis,scale=dpg.mvPlotScale_Time,label='Days',tag=f"{tab_tag}_xaxis")
y_axis = dpg.add_plot_axis(dpg.mvYAxis,label='Number of Events',tag = f"{tab_tag}_yaxis")
for name, events in event_summary.items():
name = getUpdatedEventName(name)
# x = [utl.get_timestamp_from_dt(key) for key in events.keys()]
x = [(utl.parse_datetime(key)+daylight_savings).timestamp() for key in events.keys()]
y = [events[key] for key in events.keys()]
dpg.add_bar_series(x,y,label=name,parent=y_axis,weight=200,tag=f"{name}_{tab_tag}")
b1 = dpg.add_button(label = 'Export Plot Figure',width=600,callback = filename_window,user_data =('Events', plot_tag, y_axis,0))
b2 = dpg.add_button(label='Export Signals',width=600,callback=filename_window,user_data=(y_axis,plot_tag,'Events'))
add_exporting_tag(extract_window_id(tab_tag),plot=b1,signal=b2)
with dpg.group(width=300):
dpg.add_text('Type of Plot:')
dpg.add_radio_button(items=['Days','Hours'],tag=f"axis_{tab_tag}",callback=toggle_plot,user_data='Days')
plot_tag = f"{tab_tag}_pie_plot"
with dpg.plot(label = 'Event Count',height=300,width=300,tag = plot_tag):
dpg.add_plot_legend(location=dpg.mvPlot_Location_NorthEast)
x_axis = dpg.add_plot_axis(dpg.mvXAxis,no_gridlines=True,no_tick_labels=True,no_tick_marks=True)
y_axis = dpg.add_plot_axis(dpg.mvYAxis,no_gridlines=True,no_tick_labels=True,no_tick_marks=True)
labels = [getUpdatedEventName(e) for e in list(event_summary.keys())]
dpg.add_pie_series(
x=0.0,
y=0.0, # (x, y) center coordinates
radius=3.0, # relative radius of the pie
values=[len(event_summary[key]) for key in event_summary.keys()], # numerical values
labels=labels, # corresponding labels
normalize=True, # ensures the values sum to 1
parent=y_axis,
label='Events',
tag=f"{tab_tag}_pie"
)
b1 = dpg.add_button(label = 'Export Plot Figure',width=300,callback = filename_window,user_data =('Pie', plot_tag, y_axis,0))
b2 = dpg.add_button(label='Export Signals',width=300,callback=filename_window,user_data=(y_axis,plot_tag,'Pie'))
add_exporting_tag(extract_window_id(tab_tag),plot=b1,signal=b2)
[docs]def event_tab(mode,tab_tag,window_id,plot_tag,signals,signal_tags):
app_state['windows'][window_id]['Data'][tab_tag]['Signals'] = {'left': [], 'right': []}
dpg.add_tab(label=mode, tag=tab_tag, parent=f"tab_bar_window_{window_id}", closable=True)
if not isinstance(signals, dict) and len(signals)==0: return
if isinstance(signals, dict):
dpg.add_text('Event Summary', parent=tab_tag)
show_event_summary(mode,tab_tag,plot_tag)
return
else:
dpg.add_text(f"Total events: {len(signals)}", parent=tab_tag, tag = f"{tab_tag}_text_recordings")
initial, final = DH.getEventsElectrodes(app_state['windows'][window_id]['Data'])
power, tables, all_mean = [], [], {}
bands = getBandsByTab(tab_tag)
max_p = 0
if not isinstance(initial, str):
for ch in initial:
if ch in electrodes_names.keys():
ch = electrodes_names[ch]
else:
electrodes_names[ch] = ch
dpg.add_text(f"Electrodes | Left: {electrodes_names[initial[0]]} | Right: {electrodes_names[initial[1]]}", parent=tab_tag)
else:
og = utl.after_point(signals[0]['Left']['Channel'])
electrodes_names[og] = og
dpg.add_text(f'{electrodes_names[og]}', parent=tab_tag)
with dpg.group(label = f"{tab_tag}_group", parent=tab_tag, horizontal=True ):
with dpg.plot(label=f"{mode} Signals", height=400, width=600, tag=plot_tag):
dpg.bind_colormap(plot_tag, 0)
dpg.add_plot_legend(location=dpg.mvPlot_Location_NorthEast,no_buttons = True)
dpg.add_plot_axis(dpg.mvXAxis, label="Frequency (Hz)", tag=f"x_axis_{tab_tag}")
dpg.add_plot_axis(dpg.mvYAxis, label="LFP Power (µVp)", tag=f"y_axis_{tab_tag}")
for idx, signal in enumerate(signals):
date = utl.parse_datetime(signal['DateInitial'])
for side in ['Left', 'Right']:
sign = signal[side]
name = getUpdatedEventName(signal['EventName'])
label = f"day {date.day:02d}/{date.month:02d}/{date.year} | {date.hour:02d}:{date.minute} | {side} | {name}"
x = sign['X']
y = sign['Y']
# Create a unique series tag for each side
series_tag = f"{tab_tag}_series_{idx}_{side.lower()}"
while len(app_state['windows'][window_id]['Data'][tab_tag]['Signals'][side.lower()]) <= idx:
app_state['windows'][window_id]['Data'][tab_tag]['Signals'][side.lower()].append(None) # Fill with placeholders
app_state['windows'][window_id]['Data'][tab_tag]['Signals'][side.lower()][idx] = sign
# Add the line series for each side
l = dpg.add_line_series(x, y, label=label, parent=f"y_axis_{tab_tag}", tag=series_tag)#, fill=[1.0, 0, 0, 0.5]) --> define transparency?
# dpg.bind_item_theme(l, 'default_line_thickness_theme')
signal_tags[series_tag] = True # Track visibility
tab_label = tab_tag
p = sign
power.append(y)
max_p = max(max_p,np.max(p['Y']))
series_label = series_tag
if not dpg.does_item_exist(f"{tab_label}_grouped"):
dpg.add_tree_node(tag=f"{tab_label}_grouped",
label="Grouped Streams",
parent=tab_label,
default_open=False
)
if not dpg.does_item_exist(f"{tab_label}_{side}_all_mean"):
dpg.add_collapsing_header(label = f'All Mean Power - {side}', tag =f"{tab_label}_{side}_all_mean", parent = f"{tab_label}_grouped")
tables.append(f"{tab_label}_{side}_all_mean_table")
if not dpg.does_item_exist(f"{tab_label}_{side}_{name}"):
dpg.add_collapsing_header(label = f"All {name}s - {side}", tag =f"{tab_label}_{side}_{name}", parent = f"{tab_label}_grouped")
tables.append(f"{tab_label}_{side}_{name}_table")
if not dpg.does_item_exist(f"{tab_label}_individuals"):
dpg.add_tree_node(tag=f"{tab_label}_individuals",
label="Individual Streams",
parent=tab_label,
default_open=False
)
if not dpg.does_item_exist(f"{tab_label}_{series_label}"):
header = dpg.get_item_label(series_label)
dpg.add_collapsing_header(label = header, tag =f"{tab_label}_{series_label}", parent = f"{tab_label}_individuals")
else:
dpg.delete_item(f"{tab_label}_{series_label}", children_only=True)
#TABLE CONTENT
for band in bands.items():
info = ft.extract_psd_features(p, band,bands=bands)
temp = info['Mean Power (mean, std. dev)']
# temp = info['Peak Power (power, frequency)']
if not isinstance(temp,(int,float)): all_mean[band[0]] = temp[-1]
else: all_mean[band[0]]=temp
create_table(f"{tab_label}_{series_label}",f"{tab_label}_{series_label}_table", info, trial_label=band[0])
create_table(f"{tab_label}_{side}_all_mean",f"{tab_label}_{side}_all_mean_table", all_mean, trial_label=header)
create_table(f"{tab_label}_{side}_{name}",f"{tab_label}_{side}_{name}_table", all_mean, trial_label=header)
tables.append(f"{tab_label}_{series_label}_table")
if not dpg.does_item_exist(f"{tab_label}_mean_metric"):
dpg.add_collapsing_header(label = 'MEAN POWER', tag =f"{tab_label}_mean_metric", parent = tab_label)
else:
dpg.delete_item(f"{tab_label}_mean_metric", children_only=True)
mean_power, minus, plus = getMeanActiveSeries(window_id,tab_tag,signal_tags)
s = dpg.add_shade_series(x,y1=minus,y2=plus,label = 'Std. Dev', parent=f"y_axis_{tab_tag}",tag=f"{tab_tag}_std")
dpg.bind_item_theme(s,create_alpha_theme())
m = dpg.add_line_series(x, mean_power, label="Mean Power", parent=f"y_axis_{tab_tag}", tag=f"{tab_tag}_mean")
dpg.bind_item_theme(m, change_line_thickness(5))
mean_power = {'Y': mean_power, 'X': p['X']}
for band in bands.items():
info = ft.extract_psd_features(mean_power, band,bands=bands)
create_table(f"{tab_label}_mean_metric",f"{tab_label}_mean_metric_table", info, trial_label=band[0])
tables.append(f"{tab_label}_mean_metric_table")
toggleDisplay2(signals,signal_tags,window_id,tab_tag,tables,plot_tag)
[docs]def timeline_tab(mode,tab_tag,window_id,plot_tag,signals,signal_tags):
dpg.add_tab(label=mode, tag=tab_tag, parent=f"tab_bar_window_{window_id}", closable=True)
data = app_state['windows'][window_id]['Data']
app_state['windows'][window_id]['Data'][tab_tag] = {'Signals': []}
flag, mean = False, []
evt = DH.getEventSummary(data)
events = DH.getSignals(data,'Events')
ocd_events = np.unique([event['EventName'] for event in events])
ocd_events = [getUpdatedEventName(o) for o in ocd_events.tolist()]
if 'Eventos' in evt.keys():
with dpg.group(parent=tab_tag, horizontal=True, tag = f"{tab_tag}_ec"): pass
dpg.add_text(f"Total days: {len(signals)}", parent=f"{tab_tag}_ec")
for e in evt['Eventos']:
name = getUpdatedEventName(e['EventName'])
event = f"| {name} Count: {e['EventCount']}"
dpg.add_text(event, parent= f"{tab_tag}_ec")
days = []
with dpg.group(parent=tab_tag, horizontal=True):
dpg.add_text('Events Legend: ')
for oc in ocd_events:
color_id = list(ocd_events).index(oc)
ev = dpg.add_text(f"{oc} ")
dpg.add_text("|")
theme = change_text_color(color_id)
dpg.bind_item_theme(ev, theme)
with dpg.group(parent=tab_tag, horizontal=True):
dpg.add_text('Signals Legend: ')
left = dpg.add_text("Left ")
dpg.add_text("|")
right = dpg.add_text(" Right")
lef = change_text_color(-1)
dpg.bind_item_theme(left, lef)
ri = change_text_color(-3)
dpg.bind_item_theme(right, ri)
initial, _ = DH.getEventsElectrodes(data)
left, right = DH.info_electrodes(data,version='Initial'), DH.info_electrodes(data,version='Initial',side=1)
if not isinstance(initial, str):
e1,e2 = getUpdatedElectrodeName(initial[0]), getUpdatedElectrodeName(initial[1])
dpg.add_text(f"Sensing Electrodes | Left: {e1} | Right: {e2}", parent=tab_tag)
if 'PSD' in left.keys():
f = []
for stim in [left,right]:
freq = stim['Sense_frequency']
f1, f2 = float(np.round(freq-2.5,2)),float(np.round(freq+2.5,2))
f.append([f1,f2])
dpg.add_separator(parent=tab_tag)
with dpg.group(horizontal=True, parent=tab_tag):
l = dpg.add_text("Left")
dpg.bind_item_theme(l, change_text_color(-1))
dpg.add_text(f"Sensing Band: {f[0]} Hz |")
l = dpg.add_text("Right")
dpg.bind_item_theme(l, change_text_color(-3))
dpg.add_text(f"Sensing Band: {f[1]} Hz")
else:
e = getUpdatedElectrodeName(initial)
dpg.add_text(f'{e}', parent=tab_tag)
ploted_colors = []
with dpg.group(label = f"{tab_tag}_group" , parent=tab_tag, horizontal=True):
dpg.add_group(width=600, tag=f"{tab_tag}_plot_group", horizontal=False)
with dpg.plot(label=f"{mode} Signals", height=400, width=800, tag=plot_tag, parent=f"{tab_tag}_plot_group"):
# dpg.bind_colormap(plot_tag, 0)
legend = dpg.add_plot_legend(location=dpg.mvPlot_Location_NorthEast,no_buttons = True)
x_axis = dpg.add_plot_axis(dpg.mvXAxis, label='Time (MM/DD/YYYY UTC)', tag=f"x_axis_{tab_tag}", scale = dpg.mvPlotScale_Time)
y_left = dpg.add_plot_axis(dpg.mvYAxis, label="Mean LFP Power (µVp)", tag=f"y_axis_{tab_tag}_left")
#y_right = dpg.add_plot_axis(dpg.mvYAxis, label="Stimulation Amplitude (mA)", tag=f"y_axis_{tab_tag}_right")
dpg.set_axis_limits_auto(y_left)
for idx, signal in enumerate(signals):
app_state['windows'][window_id]['Data'][tab_tag]['Signals'].append(signal)
stim_info = DH.info_electrodes(app_state['windows'][window_id]['Data'])['Sense_frequency']
# date = utl.parse_datetime(signal['Date'])
date = signal['Date']
extra = DH.correctMissingTimeline(data, signal,'X_missing')[0]
if extra['Right']['X_missing'] != [] and flag == False:
flag = True
for side in ['Left','Right']:
sign = signal[side] | {'Sense': stim_info}
date = signal['Date']
days.append(date)
label_left = f"{side} - day {date}"
try:
x = [utl.convert_to_timestamp(val) for val in sign['X']]
y = [float(val) for val in sign['Y']]
except ValueError as e:
print(f"Error converting data for {label_left}: {e}")
continue
# LFP power
series_tag = f"{tab_tag}_series_{idx}_{side.lower()}"
l=dpg.add_line_series(x, y, parent=y_left, tag=series_tag ,label=label_left)
if side=='Left':
color_id=-1
else: color_id=-3
ploted_colors.append(colors[color_id])
th = change_line_color(color_id)
dpg.bind_item_theme(l, th)
signal_tags[series_tag] = True # Track visibility
info, tables = calculate_timeline_features(tab_tag,series_tag)
mean.append(info)
mean_dict = {}
for key in mean[0].keys():
values = [m[key] for m in mean]
if isinstance(values[0], str):
#its datetime in string
values=[utl.get_timestamp_from_dt(t) for t in values]
mean_value = utl.extract_time(pre.get_date_from_ts(np.mean(values)))
mean_value = {key: mean_value}
else:
mean_value = {key: np.mean(values,axis=0)}
mean_dict = mean_dict | mean_value
create_table(f"{tab_tag}_header",f"{tab_tag}_table", mean_dict, trial_label='Mean')
days = sorted(np.unique(days).tolist())
sides = ['Left','Right']
for event in events:
x_event = utl.convert_to_timestamp(event['DateInitial'])
if event['DateInitial'][:10] < signals[0][side]['X'][0][:10]:
continue
label = getUpdatedEventName(event['EventName'])
color = list(ocd_events).index(label)
e = dpg.add_inf_line_series(x=[x_event], label=label, parent = y_left)
theme = change_inf_line_color(color)
ploted_colors.append(colors[color])
dpg.bind_item_theme(e, theme)
signal_tags[e] = True
dpg.configure_item(legend, show=False)
with dpg.group(label = f"{tab_tag}_buttons", parent=f"{tab_tag}_group",):
toggleVisibility(signal_tags,parent=tab_tag, group=f"{tab_tag}_buttons", user_data = window_id)
with dpg.group(horizontal=True):
with dpg.child_window(height=cwh, width=cww,horizontal_scrollbar=True):
dpg.add_text('By Day: ')
for day in days:
toggleType(signal_tags,parent=f"{tab_tag}_buttons", user_data=(day, window_id, tab_tag)) #check day
with dpg.child_window(height=cwh, width=cww,horizontal_scrollbar=True):
dpg.add_text('By Hemisphere: ')
for day in sides:
toggleType(signal_tags,parent=f"{tab_tag}_buttons", user_data=(day, window_id, tab_tag)) #check hemisphere
if len(ocd_events)>1:
with dpg.child_window(height=cwh, width=cww,horizontal_scrollbar=True):
dpg.add_text('By Event: ')
for ocd in ocd_events:
toggleType(signal_tags,parent=f"{tab_tag}_buttons", user_data=(ocd, window_id, tab_tag)) #check hemisphere
dpg.add_separator()
b1 = dpg.add_button(label = 'Export Plot Figure',width=-1,callback = filename_window,user_data =('2D', plot_tag, y_left,ploted_colors),tag=f"{y_left}_export_plot")
b2 = dpg.add_button(label='Export Signals',width=-1,callback=filename_window,user_data=(y_left,plot_tag,'Timeline'))
add_exporting_tag(window_id,plot=b1,signal=b2)
dpg.add_separator()
with dpg.group(horizontal=True):
dpg.add_slider_float(label = 'Remove Outlier', callback=RemoveTimelineOutliers,default_value=3.0,user_data=(tab_tag,signal_tags), width=100, min_value=0,max_value=10)
dpg.add_button(label='Recalculate Metrics', callback = recalculate_timeline,user_data=(tab_tag,list(signal_tags.keys()),tables))
if flag == True:
dpg.add_separator()
a = dpg.add_text('Warning: There are missing points in Timeline', tag=f"{tab_tag}_wmp", parent=f"{tab_tag}_buttons")
dpg.bind_item_theme(a,change_text_color(1))
dpg.add_checkbox(label='Show missing points',parent=f"{tab_tag}_buttons", default_value=False, callback=changeTimelinePlot, user_data=(signal_tags, signals, data, y_left), tag=f"{tab_tag}_missing_points")
add_timeline_features_buttons(tab_tag, signal_tags)
[docs]def setup_tab(mode,tab_tag,window_id,plot_tag,signals,signal_tags):
sense, calibration, artifacts = signals
def setup_off_tab():
#SENSE CHANNEL TEST - STIMULATION OFF
tab_tag_off = tab_tag + "_OFF"
app_state['windows'][window_id]['Data'][tab_tag_off]= {'Signals': []}
if dpg.does_item_exist(tab_tag_off):
dpg.delete_item(tab_tag_off)
dpg.add_tab(label='Setup OFF', tag=tab_tag_off, parent=f"tab_bar_window_{window_id}", closable=True)
with dpg.group(parent=tab_tag_off, horizontal=True): # STIM OFF
signal_tags = {}
plot_tag = f"plot_{tab_tag_off}"
with dpg.group():
# First plot (Stimulation OFF)
with dpg.plot(label=f"{mode} SETUP - Stimulation OFF", height=400, width=600, tag=plot_tag):
dpg.bind_colormap(plot_tag, 0)
dpg.add_plot_legend(location=dpg.mvPlot_Location_NorthEast, no_buttons = True)
dpg.add_plot_axis(dpg.mvXAxis, label="Time (s)", tag=f"x_axis_{tab_tag_off}")
dpg.add_plot_axis(dpg.mvYAxis, label="LFP (µV)", tag=f"y_axis_{tab_tag_off}")
parent = f"y_axis_{tab_tag_off}"
plot_td(sense, parent, tab_tag_off, signal_tags, plot_tag)
toggleDisplay(sense, signal_tags, window_id, tab_tag_off,plot_tag)
add_filters_buttons(tab_tag_off,signal_tags)
add_features_buttons(tab_tag_off, signal_tags)
def setup_on_tab():
#CALIBRATION TEST - STIMULATION OFF
tab_tag_on = tab_tag + "_ON"
if dpg.does_item_exist(tab_tag_on):
dpg.delete_item(tab_tag_on)
app_state['windows'][window_id]['Data'][tab_tag_on]= {'Signals': []}
dpg.add_tab(label='Setup ON', tag=tab_tag_on, parent=f"tab_bar_window_{window_id}", closable=True)
with dpg.group(parent=tab_tag_on, horizontal=True): # STIM ON
signal_tags = {}
plot_tag = f"plot_{tab_tag_on}"
with dpg.group():
# Second plot (Stimulation ON)
with dpg.plot(label=f"{mode} SETUP - Stimulation ON", height=400, width=600, tag=plot_tag):
dpg.bind_colormap(plot_tag, 0)
dpg.add_plot_legend(location=dpg.mvPlot_Location_NorthEast,no_buttons = True)
dpg.add_plot_axis(dpg.mvXAxis, label="Time (s)", tag=f"x_axis_{tab_tag_on}")
dpg.add_plot_axis(dpg.mvYAxis, label="LFP (µV)", tag=f"y_axis_{tab_tag_on}")
parent = f"y_axis_{tab_tag_on}"
plot_td(calibration, parent, tab_tag_on, signal_tags)
toggleDisplay(calibration, signal_tags, window_id, tab_tag_on,plot_tag)
add_filters_buttons(tab_tag_on,signal_tags)
add_features_buttons(tab_tag_on, signal_tags)
def setup_artifacts():
types_artifacts, channels = [], []
tab_tag_art = tab_tag + "_ARTIFACTS"
if dpg.does_item_exist(tab_tag_art):
dpg.delete_item(tab_tag_art)
app_state['windows'][window_id]['Data'][tab_tag_art]= {'Signals': []}
bands = getBandsByTab(tab_tag_art)
dpg.add_tab(label='Setup ARTIFACTS', tag=tab_tag_art, parent=f"tab_bar_window_{window_id}", closable=True)
with dpg.group(parent=tab_tag_art, horizontal=True): # STIM ON
signal_tags, tables = {}, []
plot_tag = f"plot_{tab_tag_art}"
with dpg.plot(label=f"{mode} SETUP - Artifacts", height=400, width=600, tag=plot_tag):
dpg.bind_colormap(plot_tag, 0)
dpg.add_plot_legend(location=dpg.mvPlot_Location_NorthEast,no_buttons = True)
x_axis = dpg.add_plot_axis(dpg.mvXAxis, label="Frequency (Hz)") #tag=f"{tab_label}_x_axis"
y_axis = dpg.add_plot_axis(dpg.mvYAxis, label="LFP Power (µVp)", tag=f"y_axis_{tab_tag_art}")
for idx, signal in enumerate(artifacts):
artifact = signal['Artifact']
label = f"{electrodes_names[signal['Channel']]} | {artifact}"
if isinstance(artifact,str): types_artifacts.append(f"{extract_artifact_type(artifact).upper()} Artifact")
channel,side = extract_channel_hemisphere(electrodes_names[signal['Channel']].upper())
channels.append(channel)
x = signal['X']
y = signal['Y']
series_tag = f"{tab_tag_art}_series_{idx}"
while len(app_state['windows'][window_id]['Data'][tab_tag_art]['Signals']) <= idx:
app_state['windows'][window_id]['Data'][tab_tag_art]['Signals'].append(None) # Fill with placeholders
app_state['windows'][window_id]['Data'][tab_tag_art]['Signals'][idx] = signal
# Add the line series for each side
dpg.add_line_series(x, y, label=label, parent=f"y_axis_{tab_tag_art}", tag=series_tag)#, fill=[1.0, 0, 0, 0.5]) --> define transparency?
signal_tags[series_tag] = True # Track visibility
if not dpg.does_item_exist(f"{tab_tag_art}_hemisphere"):
dpg.add_tree_node(tag=f"{tab_tag_art}_hemisphere",
label="By Hemisphere",
parent=tab_tag_art,
default_open=False
)
if not dpg.does_item_exist(f"{tab_tag_art}_channel"):
dpg.add_tree_node(tag=f"{tab_tag_art}_channel",
label="By Channel",
parent=tab_tag_art,
default_open=False
)
if not dpg.does_item_exist(f"{tab_tag_art}_individual"):
dpg.add_tree_node(tag=f"{tab_tag_art}_individual",
label="Individual Streams",
parent=tab_tag_art,
default_open=False
)
if not dpg.does_item_exist(f"{tab_tag_art}_{side}"):
dpg.add_collapsing_header(label = f'{side} HEMISPHERE', tag =f"{tab_tag_art}_{side}", parent = f"{tab_tag_art}_hemisphere")
if not dpg.does_item_exist(f"{tab_tag_art}_{channel}_{side}"):
dpg.add_collapsing_header(label = f"{channel}_{side}", tag =f"{tab_tag_art}_{channel}_{side}", parent = f"{tab_tag_art}_channel")
if not dpg.does_item_exist(f"{series_tag}_header"):
dpg.add_collapsing_header(tag=f"{series_tag}_header", parent=f"{tab_tag_art}_individual",label=label)
for band in bands.items():
info = ft.extract_psd_features(signal, band,bands=bands)
create_table(f"{series_tag}_header",f"{series_tag}_table", info, trial_label=band[0])
create_table(f"{tab_tag_art}_{side}",f"{tab_tag_art}_{side}_table", info, trial_label=f"{band[0]} | label")
create_table(f"{tab_tag_art}_{channel}_{side}",f"{tab_tag_art}_{channel}_{side}_table", info, trial_label=band[0])
for table in [f"{series_tag}_table",f"{tab_tag_art}_{side}_table",f"{tab_tag_art}_{channel}_{side}_table"]: tables.append(table)
mean_power = pre.MeanSignal(artifacts)
std = pre.StdDevSignal(artifacts)
minus,plus = mean_power-std,mean_power+std
s = dpg.add_shade_series(x,y1=minus,y2=plus,label='Std. Dev', parent=y_axis, tag=f"{tab_tag_art}_std")
dpg.bind_item_theme(s,create_alpha_theme())
m = dpg.add_line_series(x, mean_power, label="Mean Power", parent=y_axis, tag=f"{tab_tag_art}_mean")
dpg.bind_item_theme(m, change_line_thickness(5))
mean_power = {'Y': mean_power, 'X':x}
if not dpg.does_item_exist(f"{tab_tag_art}_mean_metric"): dpg.add_collapsing_header(tag=f"{tab_tag_art}_mean_metric",parent=tab_tag_art,label = 'Mean')
for band in bands.items():
info = ft.extract_psd_features(mean_power, band,bands=bands)
create_table(f"{tab_tag_art}_mean_metric",f"{tab_tag_art}_mean_metric_table", info, trial_label=band[0])
tables.append(f"{tab_tag_art}_mean_metric_table")
tables = np.unique(tables).tolist()
toggleDisplay(artifacts,signal_tags,window_id,tab_tag_art,plot_tag,tables=tables)
if sense:
setup_off_tab()
if calibration:
setup_on_tab()
if artifacts:
setup_artifacts()
[docs]def survey_tab(mode,tab_tag,window_id,plot_tag,signals,signal_tags):
# 'Recordings Time Domain'
dpg.add_tab(label=mode, tag=tab_tag, parent=f"tab_bar_window_{window_id}", closable=True)
dpg.add_text(f"Total recordings: {len(signals)}", parent=tab_tag)
with dpg.group(parent=tab_tag, label = f"{tab_tag}_group", horizontal=True):
with dpg.plot(label=f"{mode} Signals", height=400, width=600, tag=plot_tag):
dpg.bind_colormap(plot_tag, 0)
dpg.add_plot_legend(location=dpg.mvPlot_Location_NorthEast,no_buttons = True)
dpg.add_plot_axis(dpg.mvXAxis, label="Time (s)", tag=f"x_axis_{tab_tag}")
dpg.add_plot_axis(dpg.mvYAxis, label="LFP (µV)", tag=f"y_axis_{tab_tag}")
parent = f"y_axis_{tab_tag}"
plot_td(signals,parent,tab_tag,signal_tags)
toggleDisplay(signals, signal_tags, window_id, tab_tag,plot_tag)
add_filters_buttons(tab_tag,signal_tags)
add_features_buttons(tab_tag, signal_tags)
# 'Recordings PSDs'
new_tab_tag = f"{tab_tag}_psd"
new_signals = getDatabyTag(tab_tag)[mode]['LFPMontage']
new_plot_tag = f"{plot_tag}_psd"
bands = getBandsByTab(tab_tag)
app_state['windows'][window_id]['Data'][new_tab_tag]= {'Signals': []}
dpg.add_tab(label=f"{mode} PSD", tag=new_tab_tag, parent=f"tab_bar_window_{window_id}", closable=True)
dpg.add_text(f"Total recordings: {len(signals)}", parent=new_tab_tag)
line_tags, channels = [], []
child = dpg.add_child_window(height=100, width=-1, parent=new_tab_tag)
with dpg.group(parent=new_tab_tag, horizontal=True):
signal_tags, colors = {}, []
with dpg.plot(label=f"{mode} PSD Signals", height=400, width=600, tag=new_plot_tag):
dpg.bind_colormap(new_plot_tag, 0)
dpg.add_plot_legend(location=dpg.mvPlot_Location_NorthEast,no_buttons = True)
dpg.add_plot_axis(dpg.mvXAxis, label="Frequency (Hz)", tag=f"x_axis_{new_tab_tag}")
dpg.add_plot_axis(dpg.mvYAxis, label="LFP (µVp)", tag=f"y_axis_{new_tab_tag}")
parent = f"y_axis_{new_tab_tag}"
# plot_td(new_signals,parent,new_tab_tag,signal_tags)
for _,signal in enumerate(new_signals):
side = utl.after_point(signal['Hemisphere'])
f = signal['LFPFrequency']
m = signal['LFPMagnitude']
channel = utl.after_point(signal['SensingElectrodes'])
line_label = f"{channel} | {side}"
line_tag = f"{new_tab_tag}_series_{_}"
line_tags.append(line_tag)
channels.append(channel)
app_state['windows'][window_id]['Data'][new_tab_tag]['Signals'].append(signal)
artifact = utl.after_point(signal['ArtifactStatus'])
dpg.add_line_series(f,m,label=line_label,parent=parent, tag = line_tag)
colors.append(dpg.get_colormap_color(0,_))
tt = dpg.add_text(f"{channel}_{side}: {artifact}", parent=child)
if artifact != 'ARTIFACT_NOT_PRESENT':
dpg.bind_item_theme(tt, change_text_color(1))
signal_tags[line_tag] = True
if 'PeakFrequencyInHertz' in signal:
peak = signal['PeakFrequencyInHertz']
peak_m = m[f.index(peak)]
dpg.add_scatter_series([peak],[peak_m], parent=parent, tag =f"{line_tag}_peak")
dpg.bind_item_theme(f"{line_tag}_peak",change_plot_color(dpg.get_colormap_color(0,_)))
colors.append(dpg.get_colormap_color(0,_))
line_tags.append(f"{line_tag}_peak")
if not dpg.does_item_exist(f"side_{new_tab_tag}"):
dpg.add_tree_node(label = f"By Hemisphere", tag =f"side_{new_tab_tag}", parent = new_tab_tag,default_open=False)
if not dpg.does_item_exist(f"ind_{new_tab_tag}"):
dpg.add_tree_node(label = f"Individual Streams", tag =f"ind_{new_tab_tag}", parent = new_tab_tag,default_open=False)
if not dpg.does_item_exist(f"{side}_{new_tab_tag}"):
dpg.add_collapsing_header(label = f"{side} Hemisphere", tag =f"{side}_{new_tab_tag}", parent = f"side_{new_tab_tag}")
if not dpg.does_item_exist(f"{line_tag}_header"):
header = dpg.get_item_label(line_tag)
dpg.add_collapsing_header(label = header, tag =f"{line_tag}_header", parent = f"ind_{new_tab_tag}")
else:
dpg.delete_item(f"{line_tag}_header", children_only=True)
#TABLE CONTENT
p = {'X': f, 'Y': m}
for band in bands.items():
info = ft.extract_psd_features(p, band,bands=bands)
create_table(f"{line_tag}_header",f"{line_tag}_header_table", info, trial_label=band[0]) #individual
create_table(f"{side}_{new_tab_tag}",f"{side}_{new_tab_tag}_table", info, trial_label=f"{band[0]} | {channel}") #hemisphere
# mean, std = getMeanActiveSeries(window_id,new_tab_tag,signal_tags)
mean, std = pre.MeanSignal(new_signals,'LFPMagnitude'), pre.StdDevSignal(new_signals,'LFPMagnitude')
dev = getDeviation(new_tab_tag)
minus,plus = mean-(dev*std),mean+(dev*std)
dpg.add_shade_series(f,y1=minus,y2=plus,label='Std. Dev',tag=f"{new_tab_tag}_std",parent=parent)
dpg.bind_item_theme(f"{new_tab_tag}_std",create_alpha_theme())
colors.append(dpg.get_colormap_color(0,_+1))
dpg.add_line_series(f,mean,label='Mean Power',parent=parent, tag = f"{new_tab_tag}_mean")
dpg.bind_item_theme(f"{new_tab_tag}_mean",change_line_thickness(5))
colors.append(dpg.get_colormap_color(0,_+2))
toggleDisplay(new_signals,signal_tags,window_id,new_tab_tag,new_plot_tag,colors=colors)
[docs]def streaming_tab(mode,tab_tag,window_id,plot_tag,signals,signal_tags):
dpg.add_tab(label=mode, tag=tab_tag, parent=f"tab_bar_window_{window_id}", closable=True)
dpg.add_text(f"Total recordings: {len(signals)}", parent=tab_tag)
rdate = signals[0]['Date']
dpg.add_text(f"Day: {rdate.day}/{rdate.month}/{rdate.year}", parent=tab_tag)
Channel = signals[0]['Channel']
stimulation = DH.getStreamingStimulation(getDatabyTag(tab_tag),signals[0]['Date'])
stim_text = ''
if len(stimulation)==1:
stim_text=stimulation[0]
Channel = getUpdatedElectrodeName(Channel)
dpg.add_text(f"Channel: {extract_channel_from_series_tag(Channel).upper()}", parent=tab_tag)
dpg.add_text(stim_text, parent=tab_tag)
with dpg.group(parent=tab_tag, label = f"{tab_tag}_group", horizontal=True):
with dpg.plot(label=f"{mode} Signals", height=400, width=600, tag=plot_tag):
dpg.bind_colormap(plot_tag, 0)
dpg.add_plot_legend(location=dpg.mvPlot_Location_NorthEast,no_buttons = True)
dpg.add_plot_axis(dpg.mvXAxis, label="Time (s)", tag=f"x_axis_{tab_tag}")
dpg.add_plot_axis(dpg.mvYAxis, label="LFP (µV)", tag=f"y_axis_{tab_tag}")
parent = f"y_axis_{tab_tag}"
plot_td(signals,parent,tab_tag,signal_tags)
#plot stimulation values
'''if right!=[]: #there is stimulation values!
stim_axis = dpg.add_plot_axis(dpg.mvYAxis2,label='Stimulation Amplitude (mA)', tag = f"y_axis_stim_{tab_tag}",opposite=True)
for r, stim in right.items():
left_stim = left[r]
s1 = dpg.add_line_series(time[r],stim,parent=stim_axis,tag=f"right_stimulation_{r}_{tab_tag}")
s2 = dpg.add_line_series(time[r],left_stim,parent=stim_axis,tag=f"left_stimulation_{r}_{tab_tag}")
dpg.bind_item_theme(s1,change_line_color(-1))
dpg.bind_item_theme(s2,change_line_color(-2))
# dpg.bind_item_theme(s1,change_line_thickness(3))
# dpg.bind_item_theme(s2,change_line_thickness(3))'''
toggleDisplay(signals, signal_tags, window_id, tab_tag,plot_tag)
dpg.add_button(label='Insert Streaming Events', parent=f"{tab_tag}_buttons", callback = insert_events, user_data=(window_id, tab_tag, signal_tags),width=-1)
# if len(stimulation)>1:
# dpg.add_checkbox(label='Plot Stimulation Amplitude', callback=plot_streaming_stimulation,user_data=(tab_tag, [stimulation], plot_tag),parent=f"{tab_tag}_buttons")
add_filters_buttons(tab_tag,signal_tags)
add_features_buttons(tab_tag, signal_tags)
[docs]def indefinite_tab(mode,tab_tag,window_id,plot_tag,signals,signal_tags):
dpg.add_tab(label=mode, tag=tab_tag, parent=f"tab_bar_window_{window_id}", closable=True)
dpg.add_text(f"Total recordings: {len(signals)}", parent=tab_tag, tag = f'{tab_tag}_info')
with dpg.group(parent=tab_tag, label = f"{tab_tag}_group", horizontal=True):
with dpg.plot(label=f"{mode} Signals", height=400, width=600, tag=plot_tag):
dpg.bind_colormap(plot_tag, 0)
dpg.add_plot_legend(location=dpg.mvPlot_Location_NorthEast,no_buttons = True)
dpg.add_plot_axis(dpg.mvXAxis, label="Time (s)", tag=f"x_axis_{tab_tag}")
dpg.add_plot_axis(dpg.mvYAxis, label="LFP (µV)", tag=f"y_axis_{tab_tag}")
parent = f"y_axis_{tab_tag}"
plot_td(signals,parent,tab_tag,signal_tags)
toggleDisplay(signals, signal_tags, window_id, tab_tag,plot_tag)
with dpg.group(parent=tab_tag, tag = f"{tab_tag}_add_events", horizontal=True):
dpg.add_button(label='Insert Streaming Events', parent=f"{tab_tag}_add_events", callback = insert_events, user_data=(window_id, tab_tag, signal_tags))
add_filters_buttons(tab_tag,signal_tags)
add_features_buttons(tab_tag, signal_tags)
[docs]def impedance_tab(Data, tab_tag):
window_id = extract_window_id(tab_tag)
mode = extract_mode(tab_tag)
status, l, r = DH.getImpedance(Data)
impedance = DH.getImpedance(Data)
if not isinstance(impedance, str):
impedance_status, l, r = impedance
else:
impedance_status = impedance
l=False
def build_result_table(parent, session_data):
# Collect unique electrodes
electrodes1 = sorted(set(entry["Electrode1"] for entry in session_data))
electrodes2 = sorted(set(entry["Electrode2"] for entry in session_data))
# Build a lookup dictionary
result_dict = {
(entry["Electrode2"], entry["Electrode1"]): entry["ResultValue"]
for entry in session_data
}
table_tag = f"{parent}_table"
with dpg.table(tag=table_tag,header_row=True, borders_innerH=True, borders_innerV=True,
borders_outerH=True, borders_outerV=True, resizable=True, policy=dpg.mvTable_SizingStretchProp):
# Header row
dpg.add_table_column(label="SenSight")
for e1 in electrodes1:
new_label = utl.after_underscore(e1)
if new_label==e1:
new_label=utl.after_point(e1)
dpg.add_table_column(label=f"{new_label} (Ohm)")
# Rows
for e2 in electrodes2:
with dpg.table_row():
dpg.add_text(utl.after_underscore(e2))
for e1 in electrodes1:
val = result_dict.get((e2, e1), "")
if e2==e1: val = '-'
if val == "": val = result_dict.get((e1, e2), "")
dpg.add_text(str(val))
if not dpg.does_item_exist(f"{parent}_save"):
b1 = dpg.add_button(label='Save Table', tag = f"{parent}_save", callback=filename_window, user_data=(table_tag, 'csv'), parent=parent)
add_exporting_tag(window_id,table=b1)
def build_result_bar_graph(title, session_data, parent):
unique_results = {}
for entry in session_data:
e1 = utl.after_underscore(utl.after_point(entry["Electrode1"]))
e2 = utl.after_underscore(utl.after_point(entry["Electrode2"]))
value = entry["ResultValue"]
key = tuple(sorted([e1, e2]))
if key not in unique_results and e1 != e2:
try:
unique_results[key] = float(value)
except (ValueError, TypeError):
unique_results[key] = 0.0 # Fallback if non-numeric
labels = [f"{e1}-\n{e2}" for (e1, e2) in unique_results.keys()]
if 'Monopolar' in title:
labels = [f"{_}" for (_, e2) in unique_results.keys()]
values = list(unique_results.values())
# Create the plot
plot_tag = f"{parent}_plot"
with dpg.group(horizontal=True, parent=parent):
with dpg.plot(label=title, height=400, width=700, tag = plot_tag):
if title=='Monopolar':
x_label = "Case - Electrode Pair"
else: x_label = "Electrode Pair"
x_axis = dpg.add_plot_axis(dpg.mvXAxis, label=x_label)
y_axis = dpg.add_plot_axis(dpg.mvYAxis, label="Impedance (Ohm)")
ticks = tuple((label, float(i)) for i, label in enumerate(labels))
dpg.set_axis_ticks(x_axis, ticks)
dpg.set_item_user_data(x_axis,ticks) #had to manually set ticks to user_data cause dpg doesnt properly save info to memory, only GUI
for i, val in enumerate(values):
dpg.add_bar_series([float(i)], [val], parent=y_axis)
dpg.bind_colormap(plot_tag, dpg.mvPlotColormap_Viridis)
b1 = dpg.add_button(label = 'Export Plot Figure', callback = filename_window,user_data =('Bar', plot_tag, y_axis,dpg.mvPlotColormap_Viridis),width=-1)
add_exporting_tag(extract_window_id(tab_tag),plot=b1)
with dpg.tab(tag = tab_tag, label = mode, parent=f"tab_bar_window_{window_id}", closable=True):
dpg.add_text(f"Impedance Status: {impedance_status}", parent=tab_tag, show=True)
with dpg.collapsing_header(label='Monopolar Left',tag = f"{tab_tag}_ml"):
build_result_table(f"{tab_tag}_ml", l["Monopolar"])
build_result_bar_graph("Monopolar Left", l["Monopolar"],f"{tab_tag}_ml")
with dpg.collapsing_header(label='Monopolar Right',tag = f"{tab_tag}_mr"):
build_result_table(f"{tab_tag}_mr", r["Monopolar"])
build_result_bar_graph("Monopolar Right", r["Monopolar"],f"{tab_tag}_mr")
with dpg.collapsing_header(label='Bipolar Left',tag = f"{tab_tag}_bl"):
build_result_table(f"{tab_tag}_bl", l["Bipolar"])
build_result_bar_graph("Bipolar Left", l["Bipolar"],f"{tab_tag}_bl")
with dpg.collapsing_header(label='Bipolar Right',tag = f"{tab_tag}_br"):
build_result_table(f"{tab_tag}_br", r["Bipolar"])
build_result_bar_graph("Bipolar Right", r["Bipolar"],f"{tab_tag}_br")
[docs]def toggle(signals, tab_tag, group_parent):
channels, days = [], []
for signal in signals:
channels.append(signal['Channel'])
days.append(str(signal['Date'])[:10])
days = sorted(np.unique(days).tolist())
channels = np.unique(channels).tolist()
def tog(sender,app_data,user_data):
children = dpg.get_item_children(f"y_axis_{tab_tag}")[1][:-2]
active = [dpg.get_item_label(child) for child in children]
all_mean = []
label = dpg.get_item_label(sender)
for tag in active:
stream = children[active.index(tag)]
if label in tag:
dpg.configure_item(stream,show=app_data)
if dpg.get_item_configuration(stream)['show']==True:
all_mean.append(dpg.get_value(stream)[1])
mean = np.nanmean(all_mean,axis=0)
std = np.nanstd(all_mean,axis=0)
minus,plus = mean-std,mean+std
dpg.configure_item(f"{tab_tag}_std",y1=minus,y2=plus)
dpg.configure_item(f"{tab_tag}_mean",y=mean)
with dpg.group(parent=group_parent):
sides = ['LEFT', 'RIGHT']
groups = {
'Days': days,
'Channels': channels,
'Hemisphere': sides
}
for name,group in groups.items():
if len(group)>1:
with dpg.child_window(width=150, height=120):
dpg.add_text(f'By {name}:')
for item in group:
dpg.add_checkbox(label=item,callback=tog,default_value=True)
[docs]def electrode_tab(Data,tab_tag):
window_id = extract_window_id(tab_tag)
try:
alpha = DH.info_electrodes(Data)
alpha['PSD']
except Exception as e:
print(e)
return
plot_tag = f"{tab_tag}_plot"
bands = getBandsByTab(tab_tag)
signal_tags, all_mean, mean, tables, signals = [], {}, [], [], []
if dpg.does_item_exist(tab_tag): return
dpg.add_tab(label="Sensing PSD", tag=tab_tag, parent=f"tab_bar_window_{window_id}", closable=True)
with dpg.group(parent=tab_tag, horizontal=True,tag=f"{tab_tag}_group"):
with dpg.plot(label=f"Sensing PSD - {alpha['PSDdate']}", height=400, width=600, tag=plot_tag):
dpg.bind_colormap(plot_tag, 0)
dpg.add_plot_legend(location=dpg.mvPlot_Location_NorthEast,no_buttons = True)
dpg.add_plot_axis(dpg.mvXAxis, label="Frequency (Hz)", tag=f"x_axis_{tab_tag}")
dpg.add_plot_axis(dpg.mvYAxis, label="LFP Power (µVp)", tag=f"y_axis_{tab_tag}")
y_axis = f"y_axis_{tab_tag}"
for side in [0,1]:
values = DH.info_electrodes(Data,side=side)['PSD']
freq,psd = values['SignalFrequencies'], values['SignalPsdValues']
channel = utl.after_point(values['Channel'])
tag = dpg.add_line_series(freq,psd,label=channel,parent=y_axis)
signal_tags.append(tag)
mean.append(psd)
if not dpg.does_item_exist(f"{tab_tag}_{channel}"):
dpg.add_collapsing_header(label = channel, tag =f"{tab_tag}_{channel}", parent = tab_tag)
else:
dpg.delete_item(f"{tab_tag}_{tag}")
#TABLE CONTENT
sign = {
'X': values['SignalFrequencies'],
'Y': values['SignalPsdValues'],
'Channel': channel,
'Date': DH.getSessionDate(Data)
}
signals.append(sign)
for band in bands.items():
info = ft.extract_psd_features(sign, band,bands=bands)
temp = info['Mean Power (mean, std. dev)']
if not isinstance(temp,(int,float)): all_mean[band[0]] = temp[-1]
else: all_mean[band[0]]=temp
create_table(f"{tab_tag}_{channel}",f"{tab_tag}_{channel}_table", info, trial_label=band[0])
tables.append(f"{tab_tag}_{channel}_table")
mean_power, std_power = np.nanmean(mean,axis=0), np.nanstd(mean,axis=0)
minus, plus = mean_power-std_power, mean_power+std_power
mean_dict = {'X': freq, 'Y':mean_power}
s = dpg.add_shade_series(freq,y1=minus,y2=plus,label = 'Std. Dev', parent=y_axis,tag=f"{tab_tag}_std")
dpg.bind_item_theme(s,create_alpha_theme())
m = dpg.add_line_series(freq, mean_power, label="Mean Power", parent=y_axis, tag=f"{tab_tag}_mean")
dpg.bind_item_theme(m, change_line_thickness(5))
if not dpg.does_item_exist(f"{tab_tag}_mean_metric"):
dpg.add_collapsing_header(label = 'MEAN POWER', tag =f"{tab_tag}_mean_metric", parent = tab_tag)
else:
dpg.delete_item(f"{tab_tag}_mean_metric", children_only=True)
for band in bands.items():
info = ft.extract_psd_features(mean_dict, band,bands=bands)
create_table(f"{tab_tag}_mean_metric",f"{tab_tag}_mean_metric_table", info, trial_label=band[0])
tables.append(f"{tab_tag}_mean_metric_table")
# toggleDisplay(signals,signal_tags,window_id,tab_tag)
toggle(signals,tab_tag,f"{tab_tag}_group")
#--------------------------------------COMBINED TABS--------------------------------------------
[docs]def comb_event_tab(mode,tab_tag,window_id,plot_tag,files,signal_tags):
dpg.add_tab(label=mode, tag=tab_tag, parent=f"tab_bar_window_{window_id}", closable=True)
power, tables = [], []
max_p = 0
bands = getBandsByTab(tab_tag)
files = app_state['windows'][window_id]['Files']
all_signals, all_mean, summary = [], {}, True
for file in files:
data = file['Data']
signals = DH.getSignals(data,mode)
if not isinstance(signals,dict): summary = False
else: summary = True
initial, _ = DH.getEventsElectrodes(data)
date = utl.extract_date(DH.getSessionDate(data))
header = f"{date}_{tab_tag}"
dpg.add_collapsing_header(tag=header,label=date,parent=tab_tag)
if not isinstance(initial, str) and not summary:
file_stim = {'Left': DH.info_electrodes(data,version='Initial'), 'Right': DH.info_electrodes(data,version='Initial',side=1)}
dpg.add_text(f"Sensing Electrodes until {DH.getSessionDate(data)} | Left: {getUpdatedElectrodeName(initial[0])} | Right: {getUpdatedElectrodeName(initial[1])}", parent=header)
a = {}
for key, info in file_stim.items():
freq = info['Sense_frequency']
f1, f2 = float(np.round(freq-2.5,2)),float(np.round(freq+2.5,2))
a[key] = [f1,f2]
if summary:
show_event_summary(mode,tab_tag,plot_tag,files)
return
plotted = []
with dpg.group(parent=tab_tag, label = f"{tab_tag}_group", horizontal=True):
with dpg.plot(label=f"{mode} Signals", height=400, width=600, tag=plot_tag):
dpg.bind_colormap(plot_tag, 0)
dpg.add_plot_legend(location=dpg.mvPlot_Location_NorthEast)
dpg.add_plot_axis(dpg.mvXAxis, label="Time (s)", tag=f"x_axis_{tab_tag}")
y_axis = dpg.add_plot_axis(dpg.mvYAxis, label="LFP (µVp)", tag=f"y_axis_{tab_tag}")
ids, channels = [], []
tab_label = tab_tag
for idx, file in enumerate(files):
app_state['windows'][window_id]['Files'][idx]['Data'][tab_tag] = {}
if mode in file['Modes']:
app_state['windows'][window_id]['Files'][idx]['Data'][tab_tag]['Signals'] = []
signals = DH.getSignals(file['Data'],mode)
file['Data'][tab_tag]= {'Signals': []}
if len(signals) == 0: ids.append(idx)
if len(ids) == len(files): return #No file has a single event
for idx, file in enumerate(files):
app_state['windows'][window_id]['Files'][idx]['Data'][tab_tag] = {}
if idx in ids: continue
if mode in file['Modes']:
app_state['windows'][window_id]['Files'][idx]['Data'][tab_tag]['Signals'] = []
signals = DH.getSignals(file['Data'],mode)
file['Data'][tab_tag]= {'Signals': []}
rdate = signals[0]['DateInitial']
rdate = utl.parse_datetime(rdate)
dpg.add_text(f"Day: {rdate.day}/{rdate.month}/{rdate.year}", parent=tab_tag)
for sdx, signal in enumerate(signals):
all_signals.append(signal)
date = utl.parse_datetime(signal['DateInitial'])
if plotted != []:
if date in plotted:
continue
else: plotted.append(date)
app_state['windows'][window_id]['Files'][idx]['Data'][tab_tag]['Signals'].append({'left': [], 'right': []})
for side in ['Left', 'Right']:
sign = signal[side]
channel = getUpdatedElectrodeName(utl.after_point(sign['Channel']))
channels.append(channel)
name = getUpdatedEventName(signal['EventName'])
label = f"day {date.day:02d}/{date.month:02d}/{date.year} | {date.hour:02d}:{date.minute} | {side} | {name}"
x = sign['X']
y = sign['Y']
# Create a unique series tag for each side
series_tag = f"{tab_tag}_file_{idx}_series_{sdx}_{side.lower()}"
app_state['windows'][window_id]['Files'][idx]['Data'][tab_tag]['Signals'][sdx][side.lower()] = sign
# Add the line series for each side
dpg.add_line_series(x, y, label=label, parent=y_axis, tag=series_tag)#, fill=[1.0, 0, 0, 0.5]) --> define transparency?
signal_tags[series_tag] = True # Track visibility
p = sign
power.append(y)
max_p = max(max_p,np.max(p['Y']))
series_label = series_tag
if not dpg.does_item_exist(f"{tab_label}_grouped"):
dpg.add_tree_node(tag=f"{tab_label}_grouped",
label="Grouped Streams",
parent=tab_label,
default_open=False
)
if not dpg.does_item_exist(f"{tab_label}_{side}_all_mean"):
dpg.add_collapsing_header(label = f'All Mean Power - {side}', tag =f"{tab_label}_{side}_all_mean", parent = f"{tab_label}_grouped")
tables.append(f"{tab_label}_{side}_all_mean_table")
if not dpg.does_item_exist(f"{tab_label}_{side}_{name}"):
dpg.add_collapsing_header(label = f"All {name}s - {side}", tag =f"{tab_label}_{side}_{name}", parent = f"{tab_label}_grouped")
tables.append(f"{tab_label}_{side}_{name}_table")
if not dpg.does_item_exist(f"{tab_label}_{channel}_{name}"):
dpg.add_collapsing_header(label = f"All {name}s - {channel}", tag =f"{tab_label}_{channel}_{name}", parent = f"{tab_label}_grouped",show=False)
tables.append(f"{tab_label}_{channel}_{name}_table")
if not dpg.does_item_exist(f"{tab_label}_individuals"):
dpg.add_tree_node(tag=f"{tab_label}_individuals",
label="Individual Streams",
parent=tab_label,
default_open=False
)
if not dpg.does_item_exist(f"{tab_label}_{series_label}"):
header = dpg.get_item_label(series_label)
dpg.add_collapsing_header(label = header, tag =f"{tab_label}_{series_label}", parent = f"{tab_label}_individuals")
else:
dpg.delete_item(f"{tab_label}_{series_label}", children_only=True)
#TABLE CONTENT
for band in bands.items():
info = ft.extract_psd_features(p, band,bands=bands)
temp = info['Mean Power (mean, std. dev)']
# temp = info['Peak Power (power, frequency)']
if not isinstance(temp,(int,float)): all_mean[band[0]] = temp[-1]
else: all_mean[band[0]]=temp
create_table(f"{tab_label}_{series_label}",f"{tab_label}_{series_label}_table", info, trial_label=band[0])
create_table(f"{tab_label}_{side}_all_mean",f"{tab_label}_{side}_all_mean_table", all_mean, trial_label=header)
create_table(f"{tab_label}_{side}_{name}",f"{tab_label}_{side}_{name}_table", all_mean, trial_label=header)
create_table(f"{tab_label}_{channel}_{name}",f"{tab_label}_{channel}_{name}_table", all_mean, trial_label=header)
tables.append(f"{tab_label}_{series_label}_table")
channels = np.unique(channels).tolist()
if len(channels)==1: dpg.delete_item(f"{tab_label}_{channel}_{name}")
else: dpg.configure_item(f"{tab_label}_{channel}_{name}",show=True)
if not dpg.does_item_exist(f"{tab_label}_mean_metric"):
dpg.add_collapsing_header(label = 'MEAN POWER', tag =f"{tab_label}_mean_metric", parent = tab_label)
else:
dpg.delete_item(f"{tab_label}_mean_metric", children_only=True)
mean_power, minus, plus = getMeanActiveSeries(window_id,tab_tag,signal_tags)
s = dpg.add_shade_series(x,y1=minus,y2=plus, parent=f"y_axis_{tab_tag}",tag=f"{tab_tag}_std", label='Std. Dev')
dpg.bind_item_theme(s,create_alpha_theme())
m = dpg.add_line_series(x, mean_power, label="Mean Power", parent=f"y_axis_{tab_tag}", tag=f"{tab_tag}_mean")
dpg.bind_item_theme(m, change_line_thickness(5))
mean_power = {'Y': mean_power, 'X': p['X']}
for band in bands.items():
info = ft.extract_psd_features(mean_power, band,bands=bands)
create_table(f"{tab_label}_mean_metric",f"{tab_label}_mean_metric_table", info, trial_label=band[0])
tables.append(f"{tab_label}_mean_metric_table")
toggleDisplay2(all_signals,signal_tags,window_id,tab_tag,tables,plot_tag)
[docs]def comb_streaming_tab(mode,tab_tag,window_id,plot_tag,files,signal_tags):
dpg.add_tab(label=mode, tag=tab_tag, parent=f"tab_bar_window_{window_id}", closable=True)
stimulation_list = []
for file in files:
if file['Data'][mode]=={}:
continue
stimulation = DH.getStreamingStimulation(file['Data'],DH.getSessionDate(file['Data']))
stimulation_list.append(stimulation)
with dpg.group(parent=tab_tag, label = f"{tab_tag}_group", horizontal=True):
with dpg.plot(label=f"{mode} Signals", height=400, width=600, tag=plot_tag):
dpg.bind_colormap(plot_tag, 0)
dpg.add_plot_legend(location=dpg.mvPlot_Location_NorthEast,no_buttons = True)
dpg.add_plot_axis(dpg.mvXAxis, label="Time (s)", tag=f"x_axis_{tab_tag}")
dpg.add_plot_axis(dpg.mvYAxis, label="LFP (µV)", tag=f"y_axis_{tab_tag}")
parent = f"y_axis_{tab_tag}"
for idx, file in enumerate(files):
if mode in file['Modes']:
signals = DH.getSignals(file['Data'],mode)
file['Data'][tab_tag]= {'Signals': []}
rdate = signals[0]['Date']
channel = extract_channel_from_series_tag(signals[0]['Channel'])
dpg.add_text(f"Day: {rdate.day}/{rdate.month}/{rdate.year} | Channel: {channel.upper()}", parent=tab_tag)
plot_td(signals,parent,tab_tag,signal_tags,files=idx)
toggleDisplay(signals, signal_tags, window_id, tab_tag,plot_tag)
# dpg.add_checkbox(label='Plot Stimulation Amplitude', callback=plot_streaming_stimulation,user_data=(tab_tag, stimulation_list, plot_tag),parent=f"{tab_tag}_buttons")
with dpg.group(parent=tab_tag, tag = f"{tab_tag}_add_events", horizontal=True):
interval_events(tab_tag,'T Before (s)','T After (s)')
dpg.add_button(label='Analyze Streaming Events', parent=f"{tab_tag}_add_events", callback = insert_events, user_data=(window_id, tab_tag, signal_tags))
add_filters_buttons(tab_tag,signal_tags)
add_features_buttons(tab_tag, signal_tags)
[docs]def comb_setup_tab(mode,tab_tag,window_id,plot_tag,files,signal_tags):
def setup_plot(mode, tab_tag, type):
signals = None
plot_tag = f"plot_{tab_tag}"
dpg.add_tab(label=mode, tag=tab_tag, parent=f"tab_bar_window_{window_id}", closable=True)
with dpg.group(parent=tab_tag, label = f"{tab_tag}_group", horizontal=True):
signal_tags = {}
with dpg.plot(label=f"{mode} Signals", height=400, width=600, tag=plot_tag):
dpg.bind_colormap(plot_tag, 0)
dpg.add_plot_legend(location=dpg.mvPlot_Location_NorthEast,no_buttons = True)
dpg.add_plot_axis(dpg.mvXAxis, label="Time (s)", tag=f"x_axis_{tab_tag}")
dpg.add_plot_axis(dpg.mvYAxis, label="LFP (µV)", tag=f"y_axis_{tab_tag}")
parent = f"y_axis_{tab_tag}"
for idx, file in enumerate(files):
file['Data'][tab_tag]= {'Signals': []}
if 'Setup' in file['Modes']:
signals = DH.getSignals(file['Data'],'Setup')[type]
plot_td(signals,parent,tab_tag,signal_tags, files=idx)
toggleDisplay(signals, signal_tags, window_id, tab_tag,plot_tag)
add_filters_buttons(tab_tag,signal_tags)
add_features_buttons(tab_tag, signal_tags)
for idx, ext in enumerate(['OFF','ON']):
new_mode = mode + ' ' + ext
new_tag = tab_tag + '_' + ext
if dpg.does_item_exist(new_tag):
dpg.delete_item(new_tag)
setup_plot(new_mode, new_tag, idx)
def setup_artifacts():
types_artifacts, channels, days = [], [], []
tab_tag_art = tab_tag + "_ARTIFACTS"
if dpg.does_item_exist(tab_tag_art):
dpg.delete_item(tab_tag_art)
bands = getBandsByTab(tab_tag_art)
dpg.add_tab(label='Setup ARTIFACTS', tag=tab_tag_art, parent=f"tab_bar_window_{window_id}", closable=True)
with dpg.group(parent=tab_tag_art, horizontal=True): # STIM ON
signal_tags, all_artifacts, tables = {}, [], []
plot_tag = f"plot_{tab_tag_art}"
with dpg.plot(label=f"{mode} SETUP - Artifacts", height=400, width=600, tag=plot_tag):
dpg.bind_colormap(plot_tag, 0)
dpg.add_plot_legend(location=dpg.mvPlot_Location_NorthEast,no_buttons = True)
x_axis = dpg.add_plot_axis(dpg.mvXAxis, label="Frequency (Hz)") #tag=f"{tab_label}_x_axis"
y_axis = dpg.add_plot_axis(dpg.mvYAxis, label="LFP Power (µVp)", tag=f"y_axis_{tab_tag_art}")
for _, file in enumerate(files):
file['Data'][tab_tag_art]= {'Signals': []}
date = utl.parse_datetime(file['Data']['Device']['SessionDate'])
date = f"{date.day:02d}/{date.month:02d}/{date.year}"
days.append(date)
if 'Setup' in file['Modes']:
artifacts = DH.getSignals(file['Data'],'Setup')[2]
for idx, signal in enumerate(artifacts):
artifact = signal['Artifact']
all_artifacts.append(signal)
label = f"{signal['Channel']} | {artifact}"
if extract_type_window(window_id) == 'Combined':
label = label + f' {date}'
if isinstance(artifact,str): types_artifacts.append(f"{extract_artifact_type(artifact).upper()} Artifact")
# channel = electrodes_names[signal['Channel']].upper()
channel,side = extract_channel_hemisphere(electrodes_names[signal['Channel']].upper())
channels.append(channel)
x = signal['X']
y = signal['Y']
# Create a unique series tag for each side
series_tag = f"{tab_tag_art}_series_{idx}_{date}"
file['Data'][tab_tag_art]['Signals'].append(signal)
# Add the line series for each side
dpg.add_line_series(x, y, label=label, parent=f"y_axis_{tab_tag_art}", tag=series_tag)#, fill=[1.0, 0, 0, 0.5]) --> define transparency?
signal_tags[series_tag] = True # Track visibility
if not dpg.does_item_exist(f"{tab_tag_art}_hemisphere"):
dpg.add_tree_node(tag=f"{tab_tag_art}_hemisphere",
label="By Hemisphere",
parent=tab_tag_art,
default_open=False
)
if not dpg.does_item_exist(f"{tab_tag_art}_channel"):
dpg.add_tree_node(tag=f"{tab_tag_art}_channel",
label="By Channel",
parent=tab_tag_art,
default_open=False
)
if not dpg.does_item_exist(f"{tab_tag_art}_individual"):
dpg.add_tree_node(tag=f"{tab_tag_art}_individual",
label="Individual Streams",
parent=tab_tag_art,
default_open=False
)
if not dpg.does_item_exist(f"{tab_tag_art}_{side}"):
dpg.add_collapsing_header(label = f'{side} HEMISPHERE', tag =f"{tab_tag_art}_{side}", parent = f"{tab_tag_art}_hemisphere")
if not dpg.does_item_exist(f"{tab_tag_art}_{channel}_{side}"):
dpg.add_collapsing_header(label = f"{channel}_{side}", tag =f"{tab_tag_art}_{channel}_{side}", parent = f"{tab_tag_art}_channel")
if not dpg.does_item_exist(f"{series_tag}_header"):
dpg.add_collapsing_header(tag=f"{series_tag}_header", parent=f"{tab_tag_art}_individual",label=label)
for band in bands.items():
info = ft.extract_psd_features(signal, band,bands=bands)
create_table(f"{series_tag}_header",f"{series_tag}_table", info, trial_label=band[0])
create_table(f"{tab_tag_art}_{side}",f"{tab_tag_art}_{side}_table", info, trial_label=f"{band[0]} | label")
create_table(f"{tab_tag_art}_{channel}_{side}",f"{tab_tag_art}_{channel}_{side}_table", info, trial_label=band[0])
for table in [f"{series_tag}_table",f"{tab_tag_art}_{side}_table",f"{tab_tag_art}_{channel}_{side}_table"]: tables.append(table)
mean_power = pre.MeanSignal(all_artifacts)
std_dev = pre.StdDevSignal(all_artifacts)
minus,plus = mean_power-std_dev,mean_power+std_dev
s = dpg.add_shade_series(x, y1=minus,y2=plus, label="Std. Deviation", parent=f"y_axis_{tab_tag_art}", tag=f"{tab_tag_art}_std")
m = dpg.add_line_series(x, mean_power, label="Mean Power", parent=f"y_axis_{tab_tag_art}", tag=f"{tab_tag_art}_mean")
dpg.bind_item_theme(s,create_alpha_theme())
dpg.bind_item_theme(m, change_line_thickness(5))
mean_power = {'Y': mean_power, 'X':x}
if not dpg.does_item_exist(f"{tab_tag_art}_mean_metric"): dpg.add_collapsing_header(tag=f"{tab_tag_art}_mean_metric",parent=tab_tag_art,label = 'Mean')
for band in bands.items():
info = ft.extract_psd_features(mean_power, band,bands=bands)
create_table(f"{tab_tag_art}_mean_metric",f"{tab_tag_art}_mean_metric_table", info, trial_label=band[0])
tables.append(f"{tab_tag_art}_mean_metric_table")
tables = np.unique(tables).tolist()
toggleDisplay(all_artifacts,signal_tags,window_id,tab_tag_art,plot_tag=plot_tag,tables=tables)
setup_artifacts()
[docs]def comb_survey_tab(mode,tab_tag,window_id,plot_tag,files,signal_tags):
dpg.add_tab(label=mode, tag=tab_tag, parent=f"tab_bar_window_{window_id}", closable=True)
with dpg.group(parent=tab_tag, label = f"{tab_tag}_group", horizontal=True):
with dpg.plot(label=f"{mode} Signals", height=400, width=600, tag=plot_tag):
dpg.bind_colormap(plot_tag, 0)
dpg.add_plot_legend(location=dpg.mvPlot_Location_NorthEast,no_buttons = True)
dpg.add_plot_axis(dpg.mvXAxis, label="Time (s)", tag=f"x_axis_{tab_tag}")
dpg.add_plot_axis(dpg.mvYAxis, label="LFP (µV)", tag=f"y_axis_{tab_tag}")
parent = f"y_axis_{tab_tag}"
for idx, file in enumerate(files):
if mode in file['Modes']:
signals = DH.getSignals(file['Data'],mode)
file['Data'][tab_tag]= {'Signals': []}
rdate = signals[0]['Date']
dpg.add_text(f"Day: {rdate.day}/{rdate.month}/{rdate.year}", parent=tab_tag)
plot_td(signals,parent,tab_tag,signal_tags,files=idx)
toggleDisplay(signals, signal_tags, window_id, tab_tag,plot_tag)
with dpg.group(parent=tab_tag, tag = f"{tab_tag}_add_events", horizontal=True):
interval_events(tab_tag,'T Before (s)','T After (s)')
dpg.add_button(label='Analyze Streaming Events', parent=f"{tab_tag}_add_events", callback = insert_events, user_data=(window_id, tab_tag, signal_tags))
add_filters_buttons(tab_tag,signal_tags)
add_features_buttons(tab_tag, signal_tags)
# 'Recordings PSDs'
new_tab_tag = f"{tab_tag}_psd"
new_plot_tag = f"{plot_tag}_psd"
bands = getBandsByTab(tab_tag)
app_state['windows'][window_id]['Files'][idx]['Data'][new_tab_tag] = {'Signals': []}
dpg.add_tab(label=f"{mode} PSD", tag=new_tab_tag, parent=f"tab_bar_window_{window_id}", closable=True)
dpg.add_text(f"Total recordings: {len(signals)}", parent=new_tab_tag)
line_tags, channels, colors = [], [], []
child = dpg.add_child_window(height=100, width=-1, parent=new_tab_tag,)
with dpg.group(parent=new_tab_tag, label = f"{new_tab_tag}_group", horizontal=True):
signal_tags, dates, all_signals = {}, [], []
with dpg.plot(label=f"{mode} Signals", height=400, width=600, tag=new_plot_tag):
dpg.bind_colormap(new_plot_tag, 0)
dpg.add_plot_legend(location=dpg.mvPlot_Location_NorthEast,no_buttons = True)
dpg.add_plot_axis(dpg.mvXAxis, label="Frequency (Hz)", tag=f"x_axis_{new_tab_tag}")
dpg.add_plot_axis(dpg.mvYAxis, label="LFP (µVp)", tag=f"y_axis_{new_tab_tag}")
parent = f"y_axis_{new_tab_tag}"
for idx, file in enumerate(files):
if mode in file['Modes']:
new_signals = file['Data'][mode]['LFPMontage']
date = file['Data']['Device']['SessionDate'][:10]
dates.append(date)
for _,signal in enumerate(new_signals):
app_state['windows'][window_id]['Files'][idx]['Data'][new_tab_tag]['Signals'].append(signal)
all_signals.append(signal)
side = utl.after_point(signal['Hemisphere'])
f = signal['LFPFrequency']
m = signal['LFPMagnitude']
channel = utl.after_point(signal['SensingElectrodes'])
if len(files)>1:
line_label = f"{date} | {channel} | {side}"
else:
line_label = f"{channel} | {side}"
# line_tag = f"{file['name']}_{new_tab_tag}_{channel}_{side}"
line_tag = f"{new_tab_tag}_file_{idx}_series_{_}"
line_tags.append(line_tag)
channels.append(channel)
artifact = utl.after_point(signal['ArtifactStatus'])
dpg.add_line_series(f,m,label=line_label,parent=parent, tag = line_tag)
colors.append(dpg.get_colormap_color(0,_*(idx+1)))
artifact_info = f"{date}_{tab_tag}"
if dpg.does_item_exist(artifact_info):
tt = dpg.add_text(f"{channel}_{side}: {artifact}", parent=artifact_info)
else:
dpg.add_collapsing_header(label = date, tag=artifact_info, parent=child)
tt = dpg.add_text(f"{channel}_{side}: {artifact}", parent=artifact_info)
if artifact != 'ARTIFACT_NOT_PRESENT':
dpg.bind_item_theme(tt, change_text_color(1))
signal_tags[line_tag] = True
if 'PeakFrequencyInHertz' in signal:
peak = signal['PeakFrequencyInHertz']
peak_m = m[f.index(peak)]
dpg.add_scatter_series([peak],[peak_m], parent=parent, tag =f"{line_tag}_peak")
dpg.bind_item_theme(f"{line_tag}_peak",change_plot_color(dpg.get_colormap_color(0,_)))
colors.append(dpg.get_colormap_color(0,_*(idx+1)))
line_tags.append(f"{line_tag}_peak")
if not dpg.does_item_exist(f"side_{new_tab_tag}"):
dpg.add_tree_node(label = f"By Hemisphere", tag =f"side_{new_tab_tag}", parent = new_tab_tag,default_open=False)
if not dpg.does_item_exist(f"channel_{new_tab_tag}"):
dpg.add_tree_node(label = f"By Channel", tag =f"channel_{new_tab_tag}", parent = new_tab_tag,default_open=False)
if not dpg.does_item_exist(f"ind_{new_tab_tag}"):
dpg.add_tree_node(label = f"Individual Streams", tag =f"ind_{new_tab_tag}", parent = new_tab_tag,default_open=False)
if not dpg.does_item_exist(f"{side}_{new_tab_tag}"):
dpg.add_collapsing_header(label = f"{side} Hemisphere", tag =f"{side}_{new_tab_tag}", parent = f"side_{new_tab_tag}")
if not dpg.does_item_exist(f"{channel}_{side}_{new_tab_tag}"):
dpg.add_collapsing_header(label = f"{channel} | {side}", tag =f"{channel}_{side}_{new_tab_tag}", parent = f"channel_{new_tab_tag}")
if not dpg.does_item_exist(f"{line_tag}_header"):
header = dpg.get_item_label(line_tag)
dpg.add_collapsing_header(label = header, tag =f"{line_tag}_header", parent = f"ind_{new_tab_tag}")
else:
dpg.delete_item(f"{line_tag}_header", children_only=True)
#TABLE CONTENT
p = {'X': f, 'Y': m}
for band in bands.items():
info = ft.extract_psd_features(p, band,bands=bands)
create_table(f"{line_tag}_header",f"{line_tag}_header_table", info, trial_label=band[0]) #individual
create_table(f"{side}_{new_tab_tag}",f"{side}_{new_tab_tag}_table", info, trial_label=f"{band[0]} | {channel} | {date[:10]}") #hemisphere
create_table(f"{channel}_{side}_{new_tab_tag}",f"{channel}_{side}_{new_tab_tag}_table", info, trial_label=f"{band[0]} | {date[:10]}") #by channel
mean, std = pre.MeanSignal(new_signals,'LFPMagnitude'), pre.StdDevSignal(new_signals,'LFPMagnitude')
dev = getDeviation(new_tab_tag)
minus,plus = mean-(dev*std),mean+(dev*std)
dpg.add_shade_series(f,y1=minus,y2=plus,label='Std. Dev',tag=f"{new_tab_tag}_std",parent=parent)
dpg.bind_item_theme(f"{new_tab_tag}_std",create_alpha_theme())
dpg.add_line_series(f,mean,label='Mean Power',parent=parent, tag = f"{new_tab_tag}_mean")
dpg.bind_item_theme(f"{new_tab_tag}_mean",change_line_thickness(5))
colors.append(dpg.get_colormap_color(0,_*(idx+1)+1)) #std dev color
colors.append(dpg.get_colormap_color(0,_*(idx+1)+2)) #mean color
toggleDisplay(new_signals,signal_tags,window_id,new_tab_tag,new_plot_tag,dates=dates,colors=colors)
[docs]def comb_indefinite_tab(mode,tab_tag,window_id,plot_tag,files,signal_tags):
dpg.add_tab(label=mode, tag=tab_tag, parent=f"tab_bar_window_{window_id}", closable=True)
with dpg.group(parent=tab_tag, label = f"{tab_tag}_group", horizontal=True):
with dpg.plot(label=f"{mode} Signals", height=400, width=600, tag=plot_tag):
dpg.bind_colormap(plot_tag, 0)
dpg.add_plot_legend(location=dpg.mvPlot_Location_NorthEast,no_buttons = True)
dpg.add_plot_axis(dpg.mvXAxis, label="Time (s)", tag=f"x_axis_{tab_tag}")
dpg.add_plot_axis(dpg.mvYAxis, label="LFP (µV)", tag=f"y_axis_{tab_tag}")
parent = f"y_axis_{tab_tag}"
for idx, file in enumerate(files):
if mode in file['Modes']:
signals = DH.getSignals(file['Data'],mode)
file['Data'][tab_tag]= {'Signals': []}
rdate = signals[0]['Date']
plot_td(signals,parent,tab_tag,signal_tags,files=idx)
else:
app_state['windows'][window_id]['Files'][idx]['Data'][tab_tag] = []
toggleDisplay(signals, signal_tags, window_id, tab_tag,plot_tag)
with dpg.group(parent=tab_tag, tag = f"{tab_tag}_add_events", horizontal=True):
interval_events(tab_tag,'T Before (s)','T After (s)')
dpg.add_button(label='Analyze Streaming Events', parent=f"{tab_tag}_add_events", callback = insert_events, user_data=(window_id, tab_tag, signal_tags))
add_filters_buttons(tab_tag,signal_tags)
add_features_buttons(tab_tag, signal_tags)
[docs]def comb_timeline_tab(mode,tab_tag,window_id,plot_tag,files,signal_tags):
dpg.add_tab(label=mode, tag=tab_tag, parent=f"tab_bar_window_{window_id}", closable=True)
files = app_state['windows'][window_id]['Files']
flag = False
datas, temp_signals, events_tags, ocd_events, channels, sense = [], [], [], [], [], []
dates = [file['Data']['Device']['SessionDate'][:10] for file in files]
if len(dates) != len(np.unique(dates).tolist()):
final_index = -1
else: final_index = 10
for file in files:
data = file['Data']
initial, _ = DH.getEventsElectrodes(data)
date = data['Device']['SessionDate'][:final_index]
header = f"{date}_{tab_tag}"
if not isinstance(initial,str):
channels.append({'Left': electrodes_names[initial[0]], 'Right': electrodes_names[initial[1]]})
dpg.add_collapsing_header(tag=header,label=date,parent=tab_tag)
if not isinstance(initial, str):
file_stim = {'Left': DH.info_electrodes(data,version='Initial'), 'Right': DH.info_electrodes(data,version='Initial',side=1)}
dpg.add_text(f"Sensing Electrodes until {DH.getSessionDate(data)} | Left: {getUpdatedElectrodeName(initial[0])} | Right: {getUpdatedElectrodeName(initial[1])}", parent=header)
a = {}
for key, info in file_stim.items():
freq = info['Sense_frequency']
f1, f2 = float(np.round(freq-2.5,2)),float(np.round(freq+2.5,2))
a[key] = [f1,f2]
sense.append(a)
with dpg.group(horizontal=True, parent=header):
l = dpg.add_text("Left")
dpg.bind_item_theme(l, change_text_color(-1))
dpg.add_text(f"Sensing Band: {sense[-1]['Left']} Hz |")
l = dpg.add_text("Right")
dpg.bind_item_theme(l, change_text_color(-3))
dpg.add_text(f"Sensing Band: {sense[-1]['Right']} Hz")
else:
ch = getUpdatedElectrodeName(initial)
dpg.add_text(f'{DH.getSessionDate(data)}: {electrodes_names[ch]}', parent=header)
evt = DH.getEventSummary(data)
if 'Eventos' in evt.keys():
for e in evt['Eventos']:
event = f"{getUpdatedEventName(e['EventName'])} Count: {e['EventCount']}"
events = DH.getSignals(file['Data'],'Events')
for edx, event in enumerate(events):
ocd_events.append(getUpdatedEventName(event['EventName']))
days, ploted_colors, all_tables = [], [], []
#Signals legend
with dpg.group(parent=tab_tag, horizontal=True):
dpg.add_text('Signals Legend: ')
left = dpg.add_text("Left ")
dpg.add_text("|")
right = dpg.add_text(" Right")
dpg.bind_item_theme(left, change_text_color(-1))
dpg.bind_item_theme(right, change_text_color(-3))
ocd_events = np.unique(ocd_events).tolist()
unique_channels = [channels[idx][side] for idx in range(len(channels)) for side in channels[idx].keys()]
unique_channels = np.unique(unique_channels).tolist()
all_sensing_bands = [sensei[u] for sensei in sense for u in ['Left','Right']]
unique_sense = np.unique(all_sensing_bands).tolist()
with dpg.group(parent=tab_tag, horizontal=True):
dpg.add_text('Events Legend: ')
for count, oc in enumerate(ocd_events):
color_id = list(ocd_events).index(oc)
ev = dpg.add_text(f"{oc} ")
dpg.bind_item_theme(ev, change_text_color(color_id))
if count < len(ocd_events)-1: dpg.add_text("|")
with dpg.group(parent=tab_tag, tag = f"{tab_tag}_group", horizontal=True):
dpg.add_group(width=600, tag=f"{tab_tag}_plot_group", horizontal=False)
with dpg.plot(label=f"{mode} Signals", height=400, width=600, tag=plot_tag, parent=f"{tab_tag}_plot_group"):
dpg.bind_colormap(plot_tag, 0)
# legend = dpg.add_plot_legend(location=dpg.mvPlot_Location_NorthEast,no_buttons = True)
x_axis = dpg.add_plot_axis(dpg.mvXAxis, label="Time (MM/DD/YYYY UTC)", tag=f"x_axis_{tab_tag}", scale=dpg.mvPlotScale_Time)
y_axis = dpg.add_plot_axis(dpg.mvYAxis, label="Mean LFP Power (µVp)", tag=f"y_axis_{tab_tag}")
for idx, file in enumerate(files):
app_state['windows'][window_id]['Files'][idx]['Data'][tab_tag] = {}
if mode in file['Modes']:
left, right = sense[idx]['Left'], sense[idx]['Right']
# sensing_band = f"[{np.round(left[0]-2.5,2)},{np.round(left[0]+2.5,2)}]"
sensing_band = sense[idx]
signals = DH.getSignals(file['Data'],mode)
events = DH.getSignals(file['Data'],'Events')
file['Data'][tab_tag]= {'Signals': []}
rdate = signals[0]['Date']
for sdx, signal in enumerate(signals):
extra = DH.correctMissingTimeline(file['Data'], signal,'X_missing')[0]
signal = DH.correctMissingTimeline(file['Data'], signal,'X')[0]
temp_signals.append(signal)
if extra['Right']['X_missing'] != []:
datas.append(file['Data'])
flag = True
else: datas.append(None)
signal_tag = f"{tab_tag}_file_{idx}_series_{sdx}"
date = signal['Date'][:final_index]
app_state['windows'][window_id]['Files'][idx]['Data'][tab_tag]['Signals'].append({'Date':date,'Left': [], 'Right': []})
days.append(date)
for side in ['Left', 'Right']:
channel = getUpdatedElectrodeName(channels[idx][side])
sign = signal[side] | {'Sense': sense[idx]}
label_left = f"{side} - day {date}"
try:
x = [utl.convert_to_timestamp(val) for val in sign['X']]
y = [float(val) for val in sign['Y']]
sign['X'] = x
except ValueError as e:
print(f"Error converting data for {label_left}: {e}")
continue
# Create a unique series tag for each side
series_tag = f"{signal_tag}_{side.lower()}"
app_state['windows'][window_id]['Files'][idx]['Data'][tab_tag]['Signals'][sdx][side] = sign
labels = [dpg.get_item_label(k) for k in list(signal_tags.keys())]
if label_left not in labels:
# Add the line series for each side
series_ud = [None,None]
if len(unique_channels)>1:
series_ud[0] = f"{channel}_{side.upper()}"
if len(unique_sense)>1:
series_ud[1] = sensing_band
l = dpg.add_line_series(x, y, label=label_left, parent=y_axis, tag=series_tag,user_data=series_ud)#, fill=[1.0, 0, 0, 0.5]) --> define transparency?
if side=='Left':color_id=-1
else: color_id=-3
ploted_colors.append(colors[color_id])
th = change_line_color(color_id)
dpg.bind_item_theme(l, th)
signal_tags[series_tag] = True # Track visibility
_, tables = calculate_timeline_features(tab_tag,series_tag)
all_tables.extend(tables)
'''if not dpg.does_item_exist(f"{tab_tag}_hemisphere"):
dpg.add_tree_node(tag=f"{tab_tag}_hemisphere",
label="By Hemisphere",
parent=tab_tag,
default_open=False
)
if not dpg.does_item_exist(f"{tab_tag}_channel") and len(unique_channels)>1:
dpg.add_tree_node(tag=f"{tab_tag}_channel",
label="By Channel",
parent=tab_tag,
default_open=False
)
if not dpg.does_item_exist(f"{tab_tag}_sensing") and len(unique_sense)>1:
dpg.add_tree_node(tag=f"{tab_tag}_sensing",
label="By Sensing Band",
parent=tab_tag,
default_open=False
)
if not dpg.does_item_exist(f"{tab_tag}_individual"):
dpg.add_tree_node(tag=f"{tab_tag}_individual",
label="All Individual Streams",
parent=tab_tag,
default_open=False
)
if not dpg.does_item_exist(f"{tab_tag}_{side}"):
dpg.add_collapsing_header(label = f'{side} Hemisphere', tag =f"{tab_tag}_{side}", parent = f"{tab_tag}_hemisphere")
if not dpg.does_item_exist(f"{tab_tag}_{channel}") and len(unique_channels)>1:
dpg.add_collapsing_header(label = f"{channel}", tag =f"{tab_tag}_{channel}", parent = f"{tab_tag}_channel")
if not dpg.does_item_exist(f"{tab_tag}_{sensing_band}") and len(unique_sense)>1:
dpg.add_collapsing_header(label = f"{sensing_band} Hz", tag =f"{tab_tag}_{sensing_band}", parent = f"{tab_tag}_sensing")
if not dpg.does_item_exist(f"{tab_tag}_header"):
dpg.add_collapsing_header(tag=f"{tab_tag}_header", parent=f"{tab_tag}_individual", label = 'Streams Metrics')
info = ft.extract_timeline_features(sign)
create_table(f"{tab_tag}_header",f"{tab_tag}_table", info, trial_label=label_left)
if len(unique_channels)>1:
create_table(f"{tab_tag}_{channel}",f"{tab_tag}_{channel}_table", info, trial_label=f"{date} | {sensing_band}")
create_table(f"{tab_tag}_{side}",f"{tab_tag}_{side}_table", info, trial_label=f"{date} | {channel}")
if len(unique_sense)>1:
create_table(f"{tab_tag}_{sensing_band}",f"{tab_tag}_{sensing_band}_table", info, trial_label=f"{date} | {channel}")
'''
for edx, event in enumerate(events):
x_event = utl.convert_to_timestamp(event['DateInitial'])
if event['DateInitial'][:10]<rdate[:10]:
continue
label = getUpdatedEventName(event['EventName'])
e_tag = f"{tab_tag}_{edx}_{idx}"
try:
color = list(ocd_events).index(label)
except Exception:
ocd_events.append(label)
color = list(ocd_events).index(label)
t = dpg.add_inf_line_series(x=[x_event],parent = y_axis, tag =f"{tab_tag}_{edx}_{idx}", label=label)
events_tags.append([e_tag,idx])
theme = change_inf_line_color(color)
ploted_colors.append(colors[color])
dpg.bind_item_theme(t, theme)
signal_tags[t] = True
date = file['Data']['Device']['SessionDate'][:final_index]
dpg.add_text(f"Total days: {len(days)}", parent=f"{date}_{tab_tag}")
dpg.add_text(f"Timezone/offset: {DH.extract_time_offset(data,True)}", parent=f"{date}_{tab_tag}")
dpg.add_separator(parent=f"{date}_{tab_tag}")
days = sorted(np.unique(days).tolist())
all_tables = np.unique(all_tables).flatten().tolist()
sides = ['Left','Right']
with dpg.group(label = f"{tab_tag}_buttons", parent=f"{tab_tag}_group"):
toggleVisibility(signal_tags,parent=tab_tag, group=f"{tab_tag}_buttons", user_data = window_id)
with dpg.group(horizontal=True):
with dpg.child_window(height=cwh, width=cww,horizontal_scrollbar=True):
dpg.add_text('By day: ')
for day in days:
toggleType(signal_tags,parent=f"{tab_tag}_buttons", user_data=(day, window_id, tab_tag)) #check day
with dpg.child_window(height=cwh, width=cww,horizontal_scrollbar=True):
dpg.add_text('By hemisphere: ')
for day in sides:
toggleType(signal_tags,parent=f"{tab_tag}_buttons", user_data=(day, window_id, tab_tag))
if len(ocd_events)>1:
with dpg.child_window(height=cwh, width=cww,horizontal_scrollbar=True):
dpg.add_text('By Event: ')
for ocd in ocd_events:
toggleType(signal_tags,parent=f"{tab_tag}_buttons", user_data=(ocd, window_id, tab_tag)) #check hemisphere
dpg.add_separator()
b1 = dpg.add_button(label = 'Export Plot Figure',width=-1,callback = filename_window,user_data =('2D', plot_tag, f"y_axis_{tab_tag}",ploted_colors),tag=f"{y_axis}_export_plot")
b2 = dpg.add_button(label='Export Signals',width=-1,callback=filename_window,user_data=(y_axis,plot_tag,'2D'))
add_exporting_tag(window_id,plot=b1,signal=b2)
dpg.add_separator()
with dpg.group(horizontal=True):
dpg.add_slider_float(label = 'Remove Outlier RMS', callback=RemoveTimelineOutliers,default_value=3.0,user_data=(tab_tag,signal_tags), width=100, min_value=0,max_value=10)
dpg.add_button(label='Recalculate Metrics', callback = recalculate_timeline, user_data=(tab_tag, list(signal_tags.keys()), all_tables))
if flag == True:
dpg.add_text( 'Warning: There are missing points in Timeline', tag=f"{tab_tag}_wmp", parent=f"{tab_tag}_buttons")
dpg.add_checkbox(label='Show missing points',parent=f"{tab_tag}_buttons", default_value=False, callback=changeTimelinePlot, user_data=(signal_tags, temp_signals, datas, y_axis))
dpg.add_button(label = 'Analyze Events', callback= AnalyzeTimelineEvents, user_data=(tab_tag, events_tags, signal_tags),parent=f"{tab_tag}_buttons",width=-1)
add_timeline_features_buttons(tab_tag, signal_tags)
[docs]def comb_impedance_tab(mode,tab_tag,window_id,files):
dict_impedance = {}
dict_impedance['Left'], dict_impedance['Right'] = {}, {}
dates = []
for _, file in enumerate(files):
if mode in file['Modes']:
Data = file['Data']
date = Data['Device']['SessionDate']
dates.append(date[:10])
status, l, r = DH.getImpedance(Data)
impedance = DH.getImpedance(Data)
if not isinstance(impedance, str): #preenchido
_, l, r = impedance
dict_impedance['Left'][date] = l
dict_impedance['Right'][date] = r
else:
l=False
name_dates = sorted(dates)
if not len(dict_impedance['Left'])>1:
impedance_tab(Data,tab_tag)
return
sorted_left = {key: value for key, value in sorted(dict_impedance['Left'].items())}
sorted_right = {key: value for key, value in sorted(dict_impedance['Right'].items())}
# dict_impedance['Left'], dict_impedance['Right'] = sorted_left, sorted_right
with dpg.tab(tag = tab_tag, label = mode, parent=f"tab_bar_window_{window_id}", closable=True):
dpg.add_text(f"Impedance Status: {status}", parent=tab_tag, show=True)
def impedance_header(read,side):
if side == 'Left':
array = sorted_left
else:
array = sorted_right
with dpg.collapsing_header(label=f"{read} {side}"):
raw_values, labels = [], []
for _,id in array.items():
for v in id[read]:
value= v['ResultValue']
label = utl.after_underscore(utl.after_point(v["Electrode2"]))
if read == 'Bipolar':
label = utl.after_underscore(utl.after_point(v["Electrode2"])) + '- \n' + label
raw_values.append(value)
labels.append(label)
with dpg.plot(label=f"{read} {side}",width=800,height=400):
dpg.add_plot_legend(outside=True,location=dpg.mvPlot_Location_North)
x_axis = dpg.add_plot_axis(dpg.mvXAxis, label="Day")
y_axis = dpg.add_plot_axis(dpg.mvYAxis, label="Impedance (Ohm)")
ticks = tuple((labels[i],float(i)) for i in range(len(labels)))
dpg.set_axis_ticks(x_axis,ticks)
dpg.add_bar_group_series(raw_values, name_dates, group_size=len(name_dates),parent=y_axis)
impedance_header('Monopolar', 'Left')
impedance_header('Monopolar', 'Right')
impedance_header('Bipolar', 'Left')
impedance_header('Bipolar', 'Right')
#-----------------------------------COMBINED ANALISYS-------------------------------------
[docs]def create_multiple_window():
window_id = len(app_state["windows"]) # Unique ID
app_state["windows"][window_id] = {"Files": []} # Track window state
app_state['windows'][window_id]['Bands'] = global_bands
def file_dialog_callback(sender, app_data, user_data):
window_id = user_data # Get window-specific ID
files = app_data['selections'] # Dict { "full_path": "file_name" }
if len(list(files.values())) == 0:
dpg.bind_item_theme(f"file_list_{window_id}",change_text_color(1))
for file in files:
app_state["windows"][window_id]["Files"].append(
{"name": file, "path": files[file]})
dpg.set_item_label(f"window_{window_id}", f"Combined {window_id}")
update_file_list(window_id)
def remove_file_callback(sender,app_data,user_data):
window_id, file_index = user_data
del app_state['windows'][window_id]['Files'][file_index]
update_file_list(window_id)
def update_file_list(window_id):
dpg.delete_item(f"file_list_group_{window_id}", children_only = True)
files = app_state['windows'][window_id]['Files']
if not files:
if not dpg.does_item_exist(f"file_list_{window_id}"):
dpg.add_text('No file selected', parent=f"file_list_group_{window_id}", tag =f"file_list_{window_id}")
dpg.configure_item(f"open_file_btn_{window_id}", show=False)
dpg.configure_item(f"mode_list_group_{window_id}", show=False)
dpg.configure_item(f"modes_{window_id}", show = False)
else:
dpg.configure_item(f"file_list_{window_id}", show=False)
for idx, file in enumerate(files):
with dpg.group(horizontal=True, parent=f"file_list_group_{window_id}"):
dpg.add_text(file['name'])
dpg.add_button(label='X',width=20, height=20, callback=remove_file_callback, user_data=(window_id,idx))
dpg.configure_item(f"open_file_btn_{window_id}", show=True)
def create_file_dialog(window_id):
'''Creates a file dialog for a given window.'''
with dpg.file_dialog(
label='Multiple files can be selected simultaneously only if from the same folder!',
directory_selector=False,
show=False,
callback=file_dialog_callback,
id=f"file_dialog_{window_id}",
width=700,
height=400,
modal=True, #only allows 1 file to be selected
default_path=directories['rec'],
user_data=window_id # Pass window ID for tracking
):
dpg.add_file_extension(".*")
dpg.add_file_extension(".json", color=(0, 255, 0, 255), custom_text="[JSON File]")
def open_file_callback(sender, app_data, user_data):
window_id = user_data
files = app_state["windows"][window_id]["Files"]
dpg.delete_item(f"mode_list_group_{window_id}", children_only = True)
all_modes = []
if not files:
dpg.configure_item(f"modes_{window_id}", show = False)
dpg.configure_item(f"mode_list_group_{window_id}", show = False)
else:
for idx, file in enumerate(files):
file_path = file['path']
Data = DH.getData(file_path)
if isinstance(Data,str):
t = dpg.add_text(f"{file['name']} corrupted.", parent=f"file_list_group_{window_id}")
red = change_text_color(1)
dpg.bind_item_theme(t, red)
continue
_ = session_window(Data, window_id, idx)
device_window(Data,window_id, idx)
stimulation_window(Data,window_id, idx)
modes = DH.getModes(Data)
if _: modes.append('Impedance')
if modes == ['None. Recording is Damaged!']:
remove_file_callback('','',user_data=(window_id,idx))
dpg.add_text(f"File {file['name']} was removed due to being damaged!", parent = f"mode_list_group_{window_id}",show=True)
dpg.add_text("Try to load again!", parent = f"mode_list_group_{window_id}",show=True)
update_file_list(window_id)
else:
file['Data'] = Data
file['Modes'] = modes
for mode in modes:
if mode != ['None. Recording is Damaged!']:
all_modes.append(mode)
else: pass
for mode in np.unique(all_modes):
if mode == 'Damaged':
continue
dpg.add_button(label = mode, parent=f"mode_list_group_{window_id}",callback=open_tab_mode ,user_data=(mode,window_id),show=True)
dpg.configure_item(f"mode_list_group_{window_id}", show = True)
dpg.configure_item(f"session_parameteres_{window_id}", show = True)
# dpg.configure_item(f"window_{window_id}",width=900, height=500)
dpg.configure_item(f"window_{window_id}", autosize=True)
time.sleep(0.1)
dpg.configure_item(f"window_{window_id}", autosize=False)
def open_tab_mode(sender,app_data,user_data):
mode, window_id = user_data
files = app_state["windows"][window_id]["Files"]
dpg.configure_item(item=f"open_file_btn_{window_id}",show=False)
dpg.configure_item(item=f"{window_id}_add_file",show=False)
tab_tag = f"tab_{mode}_{window_id}"
if dpg.does_item_exist(tab_tag): #check if its already created
# if not dpg.is_item_shown(tab_tag): # If closed, show it again
# dpg.configure_item(tab_tag, show=True)
# '''maybe delete tab, create a new one from scrath enabling reset (?)'''
# return
dpg.delete_item(tab_tag)
if mode == 'Survey': dpg.delete_item(f"{tab_tag}_psd")
plot_tag = f"plot_{tab_tag}"
signal_tags = {}
match mode:
case 'Setup': comb_setup_tab(mode,tab_tag,window_id,plot_tag,files,signal_tags=signal_tags)
case 'Survey': comb_survey_tab(mode,tab_tag,window_id,plot_tag,files,signal_tags=signal_tags)
case 'Streaming': comb_streaming_tab(mode,tab_tag,window_id,plot_tag,files,signal_tags=signal_tags)
case 'Indefinite': comb_indefinite_tab(mode,tab_tag,window_id,plot_tag,files,signal_tags=signal_tags)
case 'Events': comb_event_tab(mode,tab_tag,window_id,plot_tag,files,signal_tags=signal_tags)
case 'Timeline': comb_timeline_tab(mode,tab_tag,window_id,plot_tag,files,signal_tags=signal_tags)
case 'Impedance': comb_impedance_tab(mode,tab_tag,window_id,files)
case 'Damaged': return
dpg.configure_item(f"window_{window_id}", autosize=False)
with dpg.window(label=f"Window {window_id + 1}", width=400, height=300, tag=f"window_{window_id}"):
window_menu(window_id)
dpg.add_button(label="Add File", callback=lambda: dpg.show_item(f"file_dialog_{window_id}"), tag = f"{window_id}_add_file")
dpg.add_text("No file selected", tag=f"file_list_{window_id}")
with dpg.group(tag=f"file_list_group_{window_id}"): pass
dpg.add_text('', tag=f"damage_{window_id}", show=True)
dpg.add_text('', tag=f"modes_{window_id}", show=False) #Modes in
dpg.add_button(label="Load Files", callback=open_file_callback,
tag=f"open_file_btn_{window_id}", show=False, user_data=window_id)
with dpg.group(horizontal=True):
with dpg.group():
dpg.add_text(f"Session Information", show=False, tag = f"session_parameteres_{window_id}_title")
dpg.add_child_window(tag = f"session_parameteres_{window_id}", height= 150, width=300, show=False,horizontal_scrollbar=True)
with dpg.group():
dpg.add_text(f"Device Information", show=False, tag = f"device_parameteres_{window_id}_title")
dpg.add_child_window(tag = f"device_parameteres_{window_id}", height= 150, width=300, show=False,horizontal_scrollbar=True)
with dpg.group():
dpg.add_text(f"Stimulation Information", show=False, tag = f"stimulation_parameteres_{window_id}_title")
dpg.add_child_window(tag = f"stimulation_parameteres_{window_id}", height= 150, width=300, show=False,horizontal_scrollbar=True)
with dpg.group(horizontal=True,tag = f"mode_list_group_{window_id}"): pass
with dpg.tab_bar(tag=f"tab_bar_window_{window_id}",reorderable=True): pass
create_file_dialog(window_id)
#-----------------------------------INDIVIDUAL ANALISYS-----------------------------------
[docs]def create_individual_window():
'''Dynamically creates a new independent window.'''
window_id = len(app_state["windows"]) # Unique ID
app_state["windows"][window_id] = {"Files": []} # Track window state
app_state['windows'][window_id]['Bands'] = global_bands
def file_dialog_callback(sender, app_data, user_data):
window_id = user_data
if isinstance(app_data['selections'], dict): # Check if files were selected
selected_files = list(app_data['selections'].values())
if len(selected_files) > 1:
dpg.set_value(f"file_list_{window_id}","Only one file can be selected! Please try again.")
elif len(selected_files) == 0:
dpg.bind_item_theme(f"file_list_{window_id}",change_text_color(1))
else:
print(f"Selected file: {selected_files[0]}")
# Get window-specific ID
files = app_data['selections'] # Dict { "full_path": "file_name" }
# Store full paths and filenames per window
app_state["windows"][window_id]["Files"] = [
{"name": full_path, "path": file_name}
for full_path, file_name in files.items()
]
# Update text display with full paths
file_list_text = "\n".join(file["name"] for file in app_state["windows"][window_id]["Files"]) if files else "No file selected"
dpg.set_value(f"file_list_{window_id}", file_list_text)
dpg.configure_item(f"open_file_btn_{window_id}", show=True)
def create_file_dialog(window_id):
'''Creates a file dialog for a given window.'''
with dpg.file_dialog(
tag = f"file_dialog_{window_id}",
directory_selector=False,
show=False,
callback=file_dialog_callback,
# id=f"file_dialog_{window_id}",
width=700,
height=400,
#modal=True, #if True: user must select a file before returning to main window - não vi diferença
default_path=directories['rec'],
user_data=window_id # Pass window ID for tracking
):
dpg.add_file_extension(".*")
dpg.add_file_extension(".json", color=(0, 255, 0, 255), custom_text="[JSON File]")
def open_tab_mode(sender, app_data, user_data): #create tab
mode, window_id = user_data
dpg.configure_item(item=f"open_file_btn_{window_id}",show=False)
dpg.configure_item(item=f"file_selector_{window_id}",show=False)
tab_tag = f"tab_{mode}_{window_id}"
if dpg.does_item_exist(tab_tag): #check if its already created
if not dpg.is_item_shown(tab_tag): # If closed, show it again
# dpg.configure_item(tab_tag, show=True)
# dpg.add_tab_button(label='X',parent=f"tab_bar_window_{window_id}")
dpg.delete_item(tab_tag)
if mode == 'Survey':
dpg.delete_item(f"{tab_tag}_psd")
'''maybe delete tab, create a new one from scrath enabling reset (?)'''
else: return
Data = app_state['windows'][window_id]['Data']
if mode == 'Impedance':
impedance_tab(Data,tab_tag)
'''status, l, r = DH.getImpedance(Data)
def build_result_table(session_name, session_data):
# Collect unique electrodes
electrodes1 = sorted(set(entry["Electrode1"] for entry in session_data))
electrodes2 = sorted(set(entry["Electrode2"] for entry in session_data))
# Build a lookup dictionary
result_dict = {
(entry["Electrode2"], entry["Electrode1"]): entry["ResultValue"]
for entry in session_data
}
with dpg.table(header_row=True, borders_innerH=True, borders_innerV=True,
borders_outerH=True, borders_outerV=True, resizable=True, policy=dpg.mvTable_SizingStretchProp):
# Header row
dpg.add_table_column(label="SenSight")
for e1 in electrodes1:
new_label = utl.after_underscore(e1)
if new_label==e1:
new_label=utl.after_point(e1)
dpg.add_table_column(label=new_label)
# Rows
for e2 in electrodes2:
with dpg.table_row():
dpg.add_text(utl.after_underscore(e2))
for e1 in electrodes1:
val = result_dict.get((e2, e1), "")
if e2==e1: val = '-'
if val == "": val = result_dict.get((e1, e2), "")
dpg.add_text(str(val))
with dpg.tab(tag = tab_tag, label = mode, parent=f"tab_bar_window_{window_id}"):
with dpg.collapsing_header(label='Monopolar Left'):
build_result_table("Monopolar", l["Monopolar"])
with dpg.collapsing_header(label='Monopolar Right'):
build_result_table("Monopolar", r["Monopolar"])
with dpg.collapsing_header(label='Bipolar Left'):
build_result_table("Bipolar", l["Bipolar"])
with dpg.collapsing_header(label='Bipolar Right'):
build_result_table("Bipolar", r["Bipolar"])
'''
else:
app_state['windows'][window_id]['Data'][tab_tag]= {'Signals': []}
signals = DH.getSignals(Data,mode)
plot_tag = f"plot_{tab_tag}"
signal_tags = {}
match mode:
case 'Setup': setup_tab(mode,tab_tag,window_id,plot_tag,signals,signal_tags)
case 'Survey': survey_tab(mode,tab_tag,window_id,plot_tag,signals,signal_tags)
case 'Streaming': streaming_tab(mode,tab_tag,window_id,plot_tag,signals,signal_tags)
case 'Indefinite': indefinite_tab(mode,tab_tag,window_id,plot_tag,signals,signal_tags)
case 'Events':
if len(signals)>0:
event_tab(mode,tab_tag,window_id,plot_tag,signals,signal_tags)
# if not isinstance(signals,dict): electrode_tab(Data,f"tab_electrode_{window_id}")
case 'Timeline':
if len(signals)>0:
timeline_tab(mode,tab_tag,window_id,plot_tag,signals,signal_tags)
# electrode_tab(Data,f"tab_electrode_{window_id}")
dpg.configure_item(f"window_{window_id}", autosize=False)
def open_file_callback(sender, app_data, user_data): #open file
window_id = user_data # Get window-specific ID
files = app_state["windows"][window_id]["Files"]
file_path = files[0]['path']
#check if its not opening the same file
previous_file_path = app_state['windows'][window_id].get('PreviousFilePath', None)
if file_path == previous_file_path:
return
else: #show modes and buttons
Data = DH.getData(file_path)
app_state['windows'][window_id]['Data'] = Data
if isinstance(Data,str):
warning_text = f"{files[0]['name']} file not terminated!"
og = dpg.get_value(f"damage_{window_id}")
if og != "":
text = f"{og} | {warning_text}"
else:
text = warning_text
dpg.set_value(f"damage_{window_id}", text)
red = change_text_color(1)
dpg.bind_item_theme(f"damage_{window_id}", red)
return
app_state['windows'][window_id]['Modes'] = DH.getModes(Data)
if app_state['windows'][window_id]['Modes'] == ['None. Recording is Damaged!']:
dpg.set_value(f"modes_{window_id}", 'Recording is damaged! Choose another file.')
dpg.configure_item(f"modes_{window_id}", show=True)
modes = []
else:
dpg.delete_item(item=f"mode_list_group_{window_id}",children_only=True)
l = session_window(Data,window_id)
device_window(Data,window_id)
stimulation_window(Data,window_id)
modes = app_state['windows'][window_id]['Modes']
if l: modes.append('Impedance')
if modes != ['None. Recording is Damaged!']:
dpg.set_value(f"modes_{window_id}", 'Recording modes present in the file:')
dpg.configure_item(f"modes_{window_id}", show=True)
dpg.set_item_label(f"window_{window_id}", files[0]['name'])
for mode in modes:
if mode == 'Damaged':
dpg.set_value(f"damage_{window_id}",'Session is damaged, recordings available.')
dpg.configure_item(f"damage_{window_id}",show=True)
continue
dpg.add_button(label = mode, parent=f"mode_list_group_{window_id}",callback=open_tab_mode ,user_data=(mode,window_id),show=True)
dpg.configure_item(f"mode_list_group_{window_id}", show = True)
app_state['windows'][window_id]['PreviousFilePath'] = file_path
dpg.configure_item(f"window_{window_id}", autosize=True)
time.sleep(0.1)
dpg.configure_item(f"window_{window_id}", autosize=False)
with dpg.window(label=f"Window {window_id + 1}", width=400, height=300, tag=f"window_{window_id}"): #individual window
window_menu(window_id)
dpg.add_button(label="File Selector", callback=lambda: dpg.show_item(f"file_dialog_{window_id}"), tag = f"file_selector_{window_id}")
dpg.add_text("No file selected", tag=f"file_list_{window_id}")
dpg.add_button(label="Open File", callback=open_file_callback,
tag=f"open_file_btn_{window_id}", show=False, user_data=window_id)
with dpg.group(horizontal=True):
with dpg.group():
dpg.add_text(f"Session Information", show=False, tag = f"session_parameteres_{window_id}_title")
dpg.add_child_window(tag = f"session_parameteres_{window_id}", height= 150, width=300, show=False,horizontal_scrollbar=True)
with dpg.group():
dpg.add_text(f"Device Information", show=False, tag = f"device_parameteres_{window_id}_title")
dpg.add_child_window(tag = f"device_parameteres_{window_id}", height= 150, width=300, show=False,horizontal_scrollbar=True)
with dpg.group():
dpg.add_text(f"Stimulation Information", show=False, tag = f"stimulation_parameteres_{window_id}_title")
dpg.add_child_window(tag = f"stimulation_parameteres_{window_id}", height= 150, width=300, show=False,horizontal_scrollbar=True)
dpg.add_text('', tag=f"damage_{window_id}", show=True)
dpg.add_text('', tag=f"modes_{window_id}", show=False) #Modes in
with dpg.group(horizontal=True,tag = f"mode_list_group_{window_id}"): pass
with dpg.tab_bar(tag=f"tab_bar_window_{window_id}",reorderable=True): pass
create_file_dialog(window_id) # Create a file dialog for this window
#---------------------------------------MAIN WINDOW---------------------------------------
[docs]def load_main_ui():
dpg.delete_item("Logo")
set_font()
light_theme = themes.create_theme_imgui_light()
dark_theme = themes.create_theme_imgui_dark()
default_classic_theme = dpg.add_theme() # DPG default
# Main window
with dpg.window(label="NeoDBS", width=1000, height=600, tag="Main"):
with dpg.menu_bar():
with dpg.menu(label="New"):
dpg.add_menu_item(label="Individual Window", callback=create_individual_window, user_data=default_classic_theme)
dpg.add_menu_item(label="Combined Window", callback=create_multiple_window, user_data=light_theme)
with dpg.menu(label="Edit"):
dpg.add_menu_item(label='Resize Font',callback=resize_font)
dpg.add_menu_item(label="Rename Electrodes", callback=change_electrodes_names)
dpg.add_menu_item(label='Rename Event/Group',callback = change_event_name)
with dpg.menu(label="Save"):
dpg.add_menu_item(label="Save All Plots", callback=save_all, user_data = ('Plots',None))
dpg.add_menu_item(label="Save All Tables", callback=save_all, user_data = ('Tables', None))
dpg.add_menu_item(label="Save All Signals", callback=save_all, user_data = ('Signals', None))
with dpg.menu(label="Themes"):
dpg.add_menu_item(label="Classic", callback=switch_theme, user_data=default_classic_theme)
dpg.add_menu_item(label="Light", callback=switch_theme, user_data=light_theme)
dpg.add_menu_item(label="Dark", callback=switch_theme, user_data=dark_theme)
with dpg.menu(label="Directories"):
dpg.add_menu_item(label="Recording Directory", callback=change_directory,user_data='rec')
dpg.add_menu_item(label="Events Directory", callback=change_directory, user_data='events')
dpg.add_menu_item(label="Save in... Directory", callback=change_directory,user_data='save')
with dpg.group(horizontal=True, pos=[150, 250]):
dpg.add_button(label="Individual/Recording Analysis", callback=create_individual_window,height=40,width=350,tag="individual")
dpg.bind_item_theme("individual",classic_button())
dpg.add_spacer(indent=5)
dpg.add_button(label="Combined/Event Analysis", callback=create_multiple_window,height=40,width=350,tag="combined")
dpg.bind_item_theme("combined",classic_button())
dpg.set_viewport_width(1000)
dpg.set_viewport_height(600)
dpg.set_viewport_resizable(True)
dpg.set_primary_window("Main", True)
# dpg.delete_item("Logo")
[docs]def main():
dpg.create_context()
dpg.create_viewport(title="NeoDBS", width=500, height=500)
dpg.set_viewport_large_icon('NeoDBS.ico')
dpg.set_viewport_small_icon('NeoDBS.ico')
dpg.setup_dearpygui()
with dpg.texture_registry(show=False):
image_path = utl.resource_path("NeoDBS.png")
try:
width, height, channels, data = dpg.load_image(str(image_path))
dpg.add_static_texture(width, height, data, tag="logo_tex")
flag = True
except Exception as e:
width, height = 500, 500
flag = False
# GLOBAL FONT REGISTRY
with dpg.font_registry(tag="FONT_REGISTRY"):
pass
# Create window that matches image size exactly
with dpg.window(label="", tag="Logo", no_title_bar=True, no_resize=True, no_close=True,
no_collapse=True, no_move=True, width=width, height=height):
dpg.bind_theme(design())
if flag:
dpg.add_image("logo_tex", width=width, height=height, pos=[0, 0])
start = dpg.add_button(label='start',height=30,width=200,pos=[150,340],callback=load_main_ui)
dpg.bind_item_theme(start,change_button_color(3))
dpg.bind_item_font(start,set_font(24,True))
version = dpg.add_text('Version 0.1.0')
dpg.bind_item_font(version, set_font(just_theme=True))
dpg.bind_item_theme(version,change_text_color(5))
dpg.set_viewport_resizable(False)
dpg.show_viewport()
dpg.start_dearpygui()
dpg.destroy_context()