Source code for emodelrunner.GUI_utils.frames
"""Frame Classes for the GUI."""
# Copyright 2020-2022 Blue Brain Project / EPFL
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# pylint: disable=wrong-import-position, too-many-ancestors, too-many-lines, import-error
import tkinter as tk
from tkinter import ttk
import numpy as np
import matplotlib
matplotlib.use("TkAgg")
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
try:
from matplotlib.backends.backend_tkagg import NavigationToolbar2TkAgg
except ImportError:
from matplotlib.backends.backend_tkagg import (
NavigationToolbar2Tk as NavigationToolbar2TkAgg,
)
from emodelrunner.GUI_utils.plotshape import get_morph_lines
from emodelrunner.GUI_utils.style import style_dict
[docs]
def positive_int_callback(input_):
"""Accepts only digits or '' as entry.
Args:
input_ (str): input to check
Returns:
bool: True if input is an int or an empty string, False otherwise
"""
return input_.isdigit() or input_ == ""
[docs]
def float_callback(input_):
"""Accepts only a float or '' as entry.
Args:
input_ (str): input to check
Returns:
bool: True if input is a float or an empty string, False otherwise
"""
if input_ != "":
try:
float(input_)
except (ValueError, TypeError):
return False
return True
[docs]
class ToolbarCustom(NavigationToolbar2TkAgg):
"""Matplotlib toolbar class."""
[docs]
def set_message(self, msg):
"""Do not show message. This is unstable.
Args:
msg (str): message
"""
[docs]
class FrameSetIntFromEntry(ttk.Frame):
"""Frame containing an entry for input."""
def __init__(self, parent, gui, attr_name, label):
"""Constructor.
Args:
parent (ttk.Frame): parent frame in which to embed this frame
gui (GUI): main class containing main frames and simulation
attr_name (str): attribute of gui.simulation that can be changed with the entry
label (str): text describing the attribute to display
"""
ttk.Frame.__init__(self, parent, style="TFrame")
# label
self.label = ttk.Label(self, text=f"{label}:")
# to enforce only digits in entry
self.reg = self.register(positive_int_callback)
# string variable to be binded to entry
self.sv = tk.StringVar()
self.sv.set(str(getattr(gui.simulation, attr_name))) # default value
self.sv.trace_add(
"write",
lambda name, index, mode, sv=self.sv: self.get_value(gui, attr_name),
)
# entry
self.entry = ttk.Entry(
self,
textvariable=self.sv,
font=style_dict["base_font"],
width=style_dict["entry_width"],
)
self.entry.config(validate="key", validatecommand=(self.reg, "%P"))
# grid
self.label.grid(row=0, column=0, sticky=tk.W)
self.entry.grid(row=0, column=1, sticky=tk.E)
self.columnconfigure(1, weight=1) # only center column grows
[docs]
def get_value(self, gui, attr_name):
"""Put input value in simulation attribute.
Args:
gui (GUI): main class containing main frames and simulation
attr_name (str): attribute of gui.simulation that can be changed with the entry
"""
value = self.entry.get()
if value == "":
value = 0
try:
setattr(gui.simulation, attr_name, int(value))
gui.config_has_changed()
except (ValueError, TypeError):
tk.messagebox.showerror(
f"Bad {attr_name} value",
"Must be an int.",
)
[docs]
class FrameStepStimulus(ttk.Frame):
"""Frame containing step stimulus value input."""
def __init__(self, parent, gui):
"""Constructor.
Args:
parent (ttk.Frame): parent frame in which to embed this frame
gui (GUI): main class containing main frames and simulation
"""
ttk.Frame.__init__(self, parent, style="TFrame")
self.step_stim = tk.DoubleVar()
self.step_stim.set(gui.simulation.step_stim) # step1 is selected
self.step_stim_title = ttk.Label(self, text="Select a Step Stimulus")
self.steps = []
for step_amp in gui.simulation.steps:
protocol_step = ttk.Radiobutton(
self,
text=f"{step_amp:.3g} nA",
variable=self.step_stim,
value=step_amp,
command=lambda: self.get_step_stim(gui),
)
self.steps.append(protocol_step)
self.custom_step = ttk.Radiobutton(
self,
text="Custom stimulus [nA]: ",
variable=self.step_stim,
value=gui.simulation.step_stim,
command=lambda: self.get_custom_step_stim(gui),
)
# to enforce only float in entry
self.reg = self.register(float_callback)
# string variable to be binded to entry
self.sv = tk.StringVar()
self.sv.set("0.0") # default value
self.sv.trace_add(
"write",
lambda name, index, mode, sv=self.sv: self.get_custom_step_stim(gui),
)
# entry
self.custom_entry = ttk.Entry(
self,
textvariable=self.sv,
font=style_dict["base_font"],
width=style_dict["entry_width"],
)
self.custom_entry.config(validate="key", validatecommand=(self.reg, "%P"))
self.custom_entry.state(["!disabled"])
self.step_stim_title.grid(row=0, column=0, sticky=tk.W)
self.custom_step.grid(row=1, column=0, sticky=tk.W)
self.custom_entry.grid(row=1, column=1, sticky=tk.E)
step_row = 1
for protocol_step in self.steps:
step_row += 1
protocol_step.grid(row=step_row, column=0, sticky=tk.W)
self.columnconfigure(1, weight=1) # only center column grows
for i in range(step_row + 1):
self.rowconfigure(i, weight=1)
[docs]
def get_step_stim(self, gui):
"""Put selected step stim value into simulation attribute.
Args:
gui (GUI): main class containing main frames and simulation
"""
self.custom_entry.state(["disabled"])
value = self.step_stim.get()
gui.simulation.step_stim = value
gui.config_has_changed()
[docs]
def get_custom_step_stim(self, gui):
"""Enable input and put input value into simulation attribute.
Args:
gui (GUI): main class containing main frames and simulation
"""
self.custom_entry.state(["!disabled"])
value = self.custom_entry.get()
if value == "":
value = 0
try:
gui.simulation.step_stim = float(value)
gui.config_has_changed()
except (ValueError, TypeError):
tk.messagebox.showerror(
"Bad step stimulus value",
"Must be a float.",
)
[docs]
class FrameHoldStimulus(ttk.Frame):
"""Frame containing holding stimulus value input."""
def __init__(self, parent, gui):
"""Constructor.
Args:
parent (ttk.Frame): parent frame in which to embed this frame
gui (GUI): main class containing main frames and simulation
"""
ttk.Frame.__init__(self, parent, style="TFrame")
self.hold_stim = tk.DoubleVar()
self.hold_stim.set(gui.simulation.hypamp) # default hold stim is selected
self.hold_stim_title = ttk.Label(self, text="Select a Holding Stimulus")
self.hypamps = []
for amp in gui.simulation.hypamps:
protocol_hypamp = ttk.Radiobutton(
self,
text=f"{amp:.3g} nA",
variable=self.hold_stim,
value=amp,
command=lambda: self.get_hold_stim(gui),
)
self.hypamps.append(protocol_hypamp)
self.custom_hold = ttk.Radiobutton(
self,
text="Custom stimulus [nA]: ",
variable=self.hold_stim,
value=gui.simulation.hypamp,
command=lambda: self.get_custom_hold_stim(gui),
)
# to enforce only float in entry
self.reg = self.register(float_callback)
# string variable to be binded to entry
self.sv = tk.StringVar()
self.sv.set("0.0") # default value
self.sv.trace_add(
"write",
lambda name, index, mode, sv=self.sv: self.get_custom_hold_stim(gui),
)
# entry
self.custom_entry = ttk.Entry(
self,
textvariable=self.sv,
font=style_dict["base_font"],
width=style_dict["entry_width"],
)
self.custom_entry.config(validate="key", validatecommand=(self.reg, "%P"))
self.custom_entry.state(["!disabled"])
self.hold_stim_title.grid(row=0, column=0, sticky=tk.W)
self.custom_hold.grid(row=1, column=0, sticky=tk.W)
self.custom_entry.grid(row=1, column=1, sticky=tk.E)
hold_row = 1
for protocol_hypamp in self.hypamps:
hold_row += 1
protocol_hypamp.grid(row=hold_row, column=0, sticky=tk.W)
self.columnconfigure(1, weight=1) # only center column grows
for i in range(hold_row + 1):
self.rowconfigure(i, weight=1)
[docs]
def get_hold_stim(self, gui):
"""Put selected holding stim value into simulation attribute.
Args:
gui (GUI): main class containing main frames and simulation
"""
self.custom_entry.state(["disabled"])
value = self.hold_stim.get()
gui.simulation.hypamp = value
gui.config_has_changed()
[docs]
def get_custom_hold_stim(self, gui):
"""Enable input and put input value into simulation attribute.
Args:
gui (GUI): main class containing main frames and simulation
"""
self.custom_entry.state(["!disabled"])
value = self.custom_entry.get()
if value == "":
value = 0
try:
gui.simulation.hypamp = float(value)
gui.config_has_changed()
except (ValueError, TypeError):
tk.messagebox.showerror(
"Bad holding stimulus value",
"Must be a float.",
)
[docs]
class FrameStepProtocol(ttk.Frame):
"""Frame containing step stimulus-related input."""
def __init__(self, parent, gui):
"""Constructor.
Args:
parent (ttk.Frame): parent frame in which to embed this frame
gui (GUI): main class containing main frames and simulation
"""
ttk.Frame.__init__(self, parent, style="TFrame")
# step stimulus
self.frame_step_stim = FrameStepStimulus(self, gui)
self.frame_step_delay = FrameSetIntFromEntry(
self, gui, "step_delay", "Step stimulus delay [ms]"
)
self.frame_step_duration = FrameSetIntFromEntry(
self, gui, "step_duration", "Step stimulus duration [ms]"
)
# holding stimulus
self.frame_hold_stim = FrameHoldStimulus(self, gui)
self.frame_hold_step_delay = FrameSetIntFromEntry(
self, gui, "hold_step_delay", "Holding stimulus delay [ms]"
)
self.frame_hold_step_duration = FrameSetIntFromEntry(
self,
gui,
"hold_step_duration",
"Holding stimulus duration [ms]",
)
# display on grid
self.frame_step_stim.grid(row=0, column=0, sticky=(tk.W, tk.E))
self.frame_step_delay.grid(row=1, column=0, sticky=(tk.W, tk.E))
self.frame_step_duration.grid(row=2, column=0, sticky=(tk.W, tk.E))
self.frame_hold_stim.grid(row=3, column=0, sticky=(tk.W, tk.E))
self.frame_hold_step_delay.grid(row=4, column=0, sticky=(tk.W, tk.E))
self.frame_hold_step_duration.grid(row=5, column=0, sticky=(tk.W, tk.E))
self.columnconfigure(0, weight=1)
for i in range(6):
self.rowconfigure(i, weight=1)
[docs]
class FrameProtocols(ttk.LabelFrame):
"""Frame containing protocol-related inputs."""
def __init__(self, parent, gui, title):
"""Constructor.
Args:
parent (ttk.Frame): parent frame in which to embed this frame
gui (GUI): main class containing main frames and simulation
title (ttk.Label): frame title to display
"""
ttk.LabelFrame.__init__(self, parent, style="Boxed.TFrame", labelwidget=title)
self.frame_sim_duration = FrameSetIntFromEntry(
self, gui, "total_duration", "Simulation time [ms]"
)
self.frame_step_protocol = FrameStepProtocol(self, gui)
self.frame_sim_duration.grid(row=0, column=0, sticky=(tk.W, tk.E))
self.frame_step_protocol.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self.rowconfigure(1, weight=12)
[docs]
class FrameConfig(ttk.Frame):
"""Frame containing all inputs."""
def __init__(self, parent, gui):
"""Constructor.
Args:
parent (ttk.Frame): parent frame in which to embed this frame
gui (GUI): main class containing main frames and simulation
"""
ttk.Frame.__init__(self, parent, style="TFrame")
title_config_fig = ttk.Label(self, text="Display configuration")
self.frame_config_fig = FrameConfigFig(self, gui, title_config_fig)
title_protocols = ttk.Label(
self, text="Simulation & Step stimulus configuration"
)
self.frame_protocols = FrameProtocols(self, gui, title_protocols)
self.frame_config_fig.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
self.frame_protocols.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self.rowconfigure(1, weight=10)
[docs]
class FrameButtons(ttk.Frame):
"""Frame containing buttons to (re-)start and pause simulation."""
def __init__(self, parent, gui):
"""Constructor.
Args:
parent (ttk.Frame): parent frame in which to embed this frame
gui (GUI): main class containing main frames and simulation
"""
ttk.Frame.__init__(self, parent, style="TFrame")
self.start_button = ttk.Button(
self, text="Start", command=gui.start, style="ControlSimul.TButton"
)
self.pause_button = ttk.Button(
self,
text="Pause",
command=gui.pause,
state=tk.DISABLED,
style="ControlSimul.TButton",
)
self.continue_button = ttk.Button(
self,
text="Continue",
command=gui.continue_simul,
state=tk.DISABLED,
style="ControlSimul.TButton",
)
self.start_button.grid(row=0, column=0)
self.pause_button.grid(row=0, column=1)
self.continue_button.grid(row=0, column=2)
[docs]
def simul_running(self):
"""Disable continue button, enable pause button."""
self.pause_button["state"] = tk.NORMAL
self.continue_button["state"] = tk.DISABLED
[docs]
def simul_on_pause(self):
"""Disable pause button, enable continue button."""
self.pause_button["state"] = tk.DISABLED
self.continue_button["state"] = tk.NORMAL
[docs]
def simul_ended(self):
"""Disable pause & continue buttons."""
self.pause_button["state"] = tk.DISABLED
self.continue_button["state"] = tk.DISABLED
[docs]
class FrameFigures(ttk.Frame):
"""Frame containing the morphology and the voltage figures."""
def __init__(
self,
parent,
simulation,
plot_3d=False,
toolbar_on=False,
figsize="medium",
val_min=-80,
val_max=30,
):
"""Constructor.
Args:
parent (ttk.Frame): parent frame in which to embed this frame
simulation (NeuronSimulation): contains simulation (and cell) data
plot_3d (bool): set to True to plot the cell shapes in 3D
toolbar_on (bool): set to True to display the matplotlib toolbars
figsize (str): figures size. can be "small", "medium", or "large".
val_min (int): minimum voltage for colormap
val_max (int): maximum voltage for colormap
"""
ttk.Frame.__init__(self, parent, style="TFrame")
self.xaxis = 2 # z
self.yaxis = 0 # x
self.zaxis = 1 # y
self.plot_3d = plot_3d
self.figsize = figsize
self.val_min = val_min
self.val_max = val_max
# ---
# figure for neuron visualisation
# ---
fig_morph = Figure()
if self.plot_3d:
self.ax_morph = fig_morph.add_subplot(111, projection="3d")
else:
self.ax_morph = fig_morph.add_subplot(111)
self.ax_morph.set_aspect(aspect=1)
# get data and axis lims for left morph figure
_, self.old_vals, _ = get_morph_lines(
ax=self.ax_morph,
sim=simulation.sim,
val_min=self.val_min,
val_max=self.val_max,
do_plot=True,
plot_3d=self.plot_3d,
xaxis=self.xaxis,
yaxis=self.yaxis,
zaxis=self.zaxis,
)
self.vals_last_draw = self.old_vals.copy()
# resize & adjust figure for not cutting axis
self.set_fig_morph_display(fig_morph)
# create canva
self.canva_morph = FigureCanvasTkAgg(fig_morph, self)
self.canva_morph.get_tk_widget().grid(
row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)
)
# for interactive 3d rotating with mouse
if self.plot_3d:
self.get_interactive_3d_rotation(self.canva_morph, self.ax_morph)
# ---
# figure for neuron visualisation with synapses
# ---
fig_morph_syn = Figure()
if self.plot_3d:
self.ax_morph_syn = fig_morph_syn.add_subplot(111, projection="3d")
else:
self.ax_morph_syn = fig_morph_syn.add_subplot(111)
self.ax_morph_syn.set_aspect(aspect=1)
# get data and axis lims
get_morph_lines(
ax=self.ax_morph_syn,
sim=simulation.sim,
do_plot=True,
cmap=None,
plot_3d=self.plot_3d,
xaxis=self.xaxis,
yaxis=self.yaxis,
zaxis=self.zaxis,
)
# resize & adjust figure for not cutting axis
self.set_fig_morph_display(fig_morph_syn)
# create canva
self.canva_morph_syn = FigureCanvasTkAgg(fig_morph_syn, self)
# save background for blit
self.ax_morph_syn_bg = self.canva_morph_syn.copy_from_bbox(
self.ax_morph_syn.bbox
)
self.canva_morph_syn.get_tk_widget().grid(
row=0, column=1, sticky=(tk.W, tk.E, tk.N, tk.S)
)
# for interactive 3d rotating with mouse
if self.plot_3d:
self.get_interactive_3d_rotation(self.canva_morph_syn, self.ax_morph_syn)
# ---
# figure for voltage evolution
# ---
fig_volt = Figure()
self.ax_volt = fig_volt.add_subplot(111)
self.set_axis(x_max=simulation.protocol.total_duration)
# set fig size
self.set_fig_volt_display(fig_volt)
# create canva
self.canva_volt = FigureCanvasTkAgg(fig_volt, self)
self.canva_volt.get_tk_widget().grid(
row=2, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S)
)
# add matplotlib toolbar
if toolbar_on:
self.set_toolbars()
self.columnconfigure(0, weight=1)
self.columnconfigure(1, weight=1)
self.rowconfigure(0, weight=1)
self.rowconfigure(1, weight=1)
self.rowconfigure(2, weight=1)
self.rowconfigure(3, weight=1)
[docs]
def set_fig_morph_display(self, fig):
"""Set shape figure size and adjustment.
Args:
fig (matplotlib.figure.Figure): figure to adjust
"""
if self.figsize == "small":
fig.set_size_inches((3, 3))
elif self.figsize == "large":
fig.set_size_inches((5, 5))
else:
fig.set_size_inches((4, 4))
if self.plot_3d:
if self.figsize:
fig.subplots_adjust(right=0.8, left=0.2)
else:
fig.subplots_adjust(right=0.85)
else:
fig.subplots_adjust(right=0.98, top=0.98, bottom=0.15, left=0.20)
[docs]
def set_fig_volt_display(self, fig):
"""Set shape figure size and adjustment.
Args:
fig (matplotlib.figure.Figure): figure to adjust
"""
if self.figsize == "small":
fig.set_size_inches((4.5, 2))
elif self.figsize == "large":
fig.set_size_inches((7.5, 3))
else:
fig.set_size_inches((6, 2.5))
fig.subplots_adjust(bottom=0.2) # to avoid the xlabel being cut
[docs]
def set_toolbars(self):
"""Set a matplotlib toolbar for each figure."""
# self.ax_morph.format_coord = lambda x, y: ""
toolbar_frame_morph = tk.Frame(self)
toolbar_frame_morph.grid(row=1, column=0, sticky=tk.E)
ToolbarCustom(self.canva_morph, toolbar_frame_morph)
# self.ax_morph_syn.format_coord = lambda x, y: ""
toolbar_frame_morph_syn = tk.Frame(self)
toolbar_frame_morph_syn.grid(row=1, column=1, sticky=tk.E)
ToolbarCustom(self.canva_morph_syn, toolbar_frame_morph_syn)
# self.ax_volt.format_coord = lambda x, y: ""
toolbar_frame_volt = tk.Frame(self)
toolbar_frame_volt.grid(row=3, column=0, columnspan=2)
ToolbarCustom(self.canva_volt, toolbar_frame_volt)
[docs]
@staticmethod
def get_interactive_3d_rotation(canva, ax):
"""Connect events to canva to enable rotative 3d plots with mouse.
Args:
canva (matplotlib.backends.backend_tkagg.FigureCanvasTkAgg): canva
ax (matplotlib.axes.Axes): axes
"""
# pylint: disable=protected-access
canva.mpl_connect("button_press_event", ax._button_press)
canva.mpl_connect("button_release_event", ax._button_release)
canva.mpl_connect("motion_notify_event", ax._on_move)
[docs]
def set_axis(self, x_min=0, x_max=3000, y_min=-90, y_max=40):
"""Set the voltage figure's axis.
Args:
x_min (float): min value on x axis
x_max (float): max value on x axis
y_min (float): min value on y axis
y_max (float): max value on y axis
"""
self.ax_volt.set_xlim([x_min, x_max])
self.ax_volt.set_ylim([y_min, y_max])
self.ax_volt.set_xlabel("t [ms]")
self.ax_volt.set_ylabel("v [mV]")
[docs]
def check_change(self, root, simulation):
"""Checks the voltage change in the cell sections.
Update display if change is significant.
Args:
root (tk.Tk): root of the GUI
simulation (NeuronSimulation): contains simulation (and cell) data
Returns:
bool: True if the cell morphology with color-coded voltage figure has been updated
"""
# here, update is True if any of the morph lines has a significant change of voltage.
morph_lines, old_vals, update = get_morph_lines(
ax=self.ax_morph,
sim=simulation.sim,
val_min=self.val_min,
val_max=self.val_max,
do_plot=False,
old_vals=self.old_vals,
vals_last_draw=self.vals_last_draw,
)
if update:
self.old_vals = old_vals
self.display(root, simulation, morph_lines)
return True
return False
[docs]
def display(self, root, simulation, morph_lines=None):
"""Update both figures display.
Args:
root (tk.Tk): root of the GUI
simulation (NeuronSimulation): contains simulation (and cell) data
morph_lines (list of matplotlib.Line2D): list of lines to be actualized
if they already have been computed, else None
"""
# get voltage data
t, v = simulation.get_voltage()
# update data in Line2D
if self.ax_volt.lines:
line = self.ax_volt.lines[0]
line.set_xdata(t)
line.set_ydata(v)
else:
(line,) = self.ax_volt.plot(t, v)
# draw voltage plot to canva
self.ax_volt.draw_artist(line)
self.canva_volt.blit(self.ax_volt.bbox)
# do not blit too much on top of figure, or else
# 'older' lines tend to stack in the background
# and bias the perceived color of the line.
# so redraw everything when there is a big change in color
if morph_lines is not None:
for line in morph_lines:
self.ax_morph.draw_artist(line)
self.vals_last_draw = self.old_vals.copy()
self.canva_morph.draw()
else:
morph_lines, self.old_vals, force_draw = get_morph_lines(
ax=self.ax_morph,
sim=simulation.sim,
val_min=self.val_min,
val_max=self.val_max,
do_plot=False,
old_vals=self.old_vals,
vals_last_draw=self.vals_last_draw,
)
for line in morph_lines:
self.ax_morph.draw_artist(line)
if force_draw:
self.canva_morph.draw()
self.vals_last_draw = self.old_vals.copy()
else:
self.canva_morph.blit(self.ax_morph.bbox)
# update tkinter display
root.update()
[docs]
def update_syn_display(self, root, simulation, size_scatter=6):
"""Update the display of the synapses on the right figure.
Args:
root (tk.Tk): root of the GUI
simulation (NeuronSimulation): contains simulation (and cell) data
size_scatter (int): size of synapses for scatter plot
"""
# get all synapses previously drawn
drawn_syns = self.ax_morph_syn.collections
# hide them before redrawing shape
for syn in drawn_syns:
syn.set_visible(False)
self.canva_morph_syn.draw() # redraw morphology
# delete the synapses so that the list is not a big bunch of hidden accumulated synapses
for syn in drawn_syns:
self.ax_morph_syn.collections.remove(syn)
# restore background as the morphology without synapses
self.canva_morph_syn.restore_region(self.ax_morph_syn_bg)
# create synapse scatterplot
syn_scatterplot = {}
for mtype, data in simulation.syn_display_data.items():
if data:
data = np.array(data)
syn_x = data[:, self.xaxis]
syn_y = data[:, self.yaxis]
syn_cols = data[:, 3]
syn_colors = ["red" if x == 1 else "orange" for x in syn_cols]
if self.plot_3d:
syn_z = data[:, self.zaxis]
syn_scatterplot[mtype] = self.ax_morph_syn.scatter(
xs=syn_x,
ys=syn_y,
zs=syn_z,
s=size_scatter,
c=syn_colors,
alpha=1,
)
else:
syn_scatterplot[mtype] = self.ax_morph_syn.scatter(
x=syn_x, y=syn_y, s=size_scatter, c=syn_colors, alpha=1
)
else:
syn_scatterplot[mtype] = None
# draw selected synapses
for mtype in simulation.available_pre_mtypes:
if mtype in simulation.pre_mtypes and syn_scatterplot[mtype]:
syn_scatterplot[mtype].set_visible(True)
self.ax_morph_syn.draw_artist(syn_scatterplot[mtype])
elif syn_scatterplot[mtype]:
syn_scatterplot[mtype].set_visible(False)
# 3d does not support blitting
if self.plot_3d:
self.canva_morph_syn.draw()
# redrawing somehow draws synapse behind shape in 2d.
else:
self.canva_morph_syn.blit(self.ax_morph_syn.bbox)
# update tkinter display
root.update()
[docs]
def restart_volt(self):
"""Clean the voltage figure."""
(line,) = self.ax_volt.plot([], [])
self.ax_volt.draw_artist(line)
self.canva_volt.draw_idle()
[docs]
class FrameMain(ttk.Frame):
"""Frame containing Figures and launching button."""
def __init__(self, parent, gui):
"""Constructor.
Args:
parent (ttk.Frame): parent frame in which to embed this frame
gui (GUI): main class containing main frames and simulation
"""
ttk.Frame.__init__(self, parent, style="TFrame")
self.frame_buttons = FrameButtons(self, gui)
self.frame_figures = FrameFigures(
self, gui.simulation, gui.plot_3d, gui.toolbar_on, gui.figsize
)
self.frame_buttons.grid(row=0, column=0)
self.frame_figures.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
[docs]
def display(self, root, simulation):
"""Update figures display.
Args:
root (tk.Tk): root of the GUI
simulation (NeuronSimulation): contains simulation (and cell) data
"""
self.frame_figures.display(root, simulation)
[docs]
def check_change(self, root, simulation):
"""Checks the voltage change in the cell sections.
Update display if change is significant.
Args:
root (tk.Tk): root of the GUI
simulation (NeuronSimulation): contains simulation (and cell) data
Returns:
bool: True if the cell morphology with color-coded voltage figure has been updated
"""
return self.frame_figures.check_change(root, simulation)
[docs]
def update_syn_display(self, root, simulation):
"""Update the display of the synapses on the right figure.
Args:
Args:
root (tk.Tk): root of the GUI
simulation (NeuronSimulation): contains simulation (and cell) data
"""
self.frame_figures.update_syn_display(root, simulation)
[docs]
def simul_on_pause(self):
"""Disable pause button, enable continue button."""
self.frame_buttons.simul_on_pause()
[docs]
def simul_running(self):
"""Disable continue button, enable pause button."""
self.frame_buttons.simul_running()
[docs]
def simul_ended(self):
"""Disable pause & continue buttons."""
self.frame_buttons.simul_ended()
[docs]
class FrameSynapses(ttk.LabelFrame):
"""Frame containing all inputs."""
def __init__(self, parent, gui, title):
"""Constructor.
Args:
parent (ttk.Frame): parent frame in which to embed this frame
gui (GUI): main class containing main frames and simulation
title (ttk.Label): frame title to display
"""
ttk.LabelFrame.__init__(self, parent, style="Boxed.TFrame", labelwidget=title)
# -- labels --
self.labels = []
self.labels.append(ttk.Label(self, text="Synapses"))
self.labels.append(ttk.Label(self, text="Start time [ms]"))
self.labels.append(ttk.Label(self, text="Spike Interval [ms]"))
self.labels.append(ttk.Label(self, text="Spike number"))
self.labels.append(ttk.Label(self, text="Noise"))
for i, label in enumerate(self.labels):
label.grid(row=0, column=i)
self.columnconfigure(i, weight=1)
self.rowconfigure(0, weight=1)
# -- synapses --
self.reg = self.register(positive_int_callback)
self.mtype_buttons = []
self.var_list = [] # 0 or 1 for each button / checkbox values
self.id_list = []
self.svs = [] # entries (start, interval, number, noise) values
# list of lists : start, interval, number and noise
self.entries = [[] for x in range(4)]
# -- create buttons & parameter entries --
for i, id_ in enumerate(gui.simulation.available_pre_mtypes.keys()):
# pre-cell m-types
self.var_list.append(tk.IntVar())
self.var_list[i].set(0)
self.id_list.append(id_)
self.mtype_buttons.append(
ttk.Checkbutton(
self,
text=gui.simulation.available_pre_mtypes[id_],
variable=self.var_list[i],
command=lambda: self.load_current_mtype_list(gui),
offvalue=0,
onvalue=1,
)
)
# synapse start, interval, number, noise
for j, entry in enumerate(self.entries):
self.svs.append(tk.StringVar())
entry.append(
ttk.Entry(
self,
textvariable=self.svs[4 * i + j],
font=style_dict["base_font"],
width=style_dict["entry_width"],
)
)
entry[i].config(validate="key", validatecommand=(self.reg, "%P"))
entry[i].state(["disabled"])
# set string variables
self.set_svs(gui, i)
# -- add buttons & entries on the grid --
for i, (b, e1, e2, e3, e4) in enumerate(
zip(*[self.mtype_buttons] + self.entries)
):
b.grid(row=i + 1, column=0, sticky=(tk.W, tk.E), padx=2)
e1.grid(row=i + 1, column=1, sticky=(tk.E), padx=2)
e2.grid(row=i + 1, column=2, sticky=(tk.E), padx=2)
e3.grid(row=i + 1, column=3, sticky=(tk.E), padx=2)
e4.grid(row=i + 1, column=4, sticky=(tk.E), padx=2)
self.rowconfigure(i + 1, weight=1)
[docs]
def toggle_button(self):
"""Enable/disable entries depending on the button status."""
for i, var in enumerate(self.var_list):
if var.get():
for entry in self.entries:
entry[i].state(["!disabled"])
else:
for entry in self.entries:
entry[i].state(["disabled"])
[docs]
def set_svs(self, gui, i):
"""Returns the entry and the variable associated.
Args:
gui (GUI): main class containing main frames and simulation
i (int): mtype index
"""
# pylint: disable=cell-var-from-loop
default_var = [
gui.simulation.syn_start,
gui.simulation.syn_interval,
gui.simulation.syn_nmb_of_spikes,
gui.simulation.syn_noise,
]
for j, var in enumerate(default_var):
self.svs[4 * i + j].set(str(var))
self.svs[4 * i + j].trace_add(
"write",
lambda name, index, mode, sv=self.svs[
4 * i + j
]: self.load_current_mtype_list(gui),
)
[docs]
@staticmethod
def check_variable(x):
"""Returns the variable if it is a positive int. Returns 0 if not.
Args:
x (str): variable to check
Returns:
int: the variable if it is a positive int
"""
try:
if x == "":
x = 0
else:
x = int(x)
assert x >= 0
except (ValueError, TypeError, AssertionError):
tk.messagebox.showerror(
"Bad parameter value",
"Must be a positive int.",
)
x = 0
return x
[docs]
def load_current_mtype_list(self, gui):
"""Load current mtype list and netstim params.
Args:
gui (GUI): main class containing main frames and simulation
"""
gui.simulation.pre_mtypes = []
gui.simulation.netstim_params = {}
for idx, var, e1, e2, e3, e4 in zip(
*[self.id_list, self.var_list] + self.entries
):
if var.get():
gui.simulation.pre_mtypes.append(idx)
v1 = self.check_variable(e1.get())
v2 = self.check_variable(e2.get())
v3 = self.check_variable(e3.get())
v4 = self.check_variable(e4.get())
params = [v1, v2, v3, v4]
# synapse netstim param depending on mtype
# {mtypeidx:[start, interval, number, noise]}
gui.simulation.netstim_params[idx] = params
self.toggle_button()
gui.frames["FrameMain"].update_syn_display(gui.root, gui.simulation)
gui.config_has_changed()
[docs]
class FrameConfigFig(ttk.LabelFrame):
"""Frame containing choices for figure display, such as 2d/3d or enabling toolbar."""
def __init__(self, parent, gui, title):
"""Constructor.
Args:
parent (ttk.Frame): parent frame in which to embed this frame
gui (GUI): main class containing main frames and simulation
title (ttk.Label): frame title to display
"""
ttk.LabelFrame.__init__(self, parent, style="Boxed.TFrame", labelwidget=title)
# 2d / 3d radiobuttons
self.plot_3d_var = tk.IntVar()
self.plot_3d_var.set(int(gui.plot_3d)) # default hold stim is selected
self.plot_2d_button = ttk.Radiobutton(
self,
text="2D neuron shape",
variable=self.plot_3d_var,
value=0,
command=lambda: self.load_plot_3d_value(gui),
)
self.plot_3d_button = ttk.Radiobutton(
self,
text="3D neuron shape",
variable=self.plot_3d_var,
value=1,
command=lambda: self.load_plot_3d_value(gui),
)
# toolbar checkbutton
self.toolbar_var = tk.IntVar()
self.toolbar_var.set(int(gui.toolbar_on))
self.toolbar_button = ttk.Checkbutton(
self,
text="display figures toolbars",
variable=self.toolbar_var,
command=lambda: self.load_toolbar_value(gui),
offvalue=0,
onvalue=1,
)
# figsize choice
self.figsize_var = tk.StringVar()
self.figsize_var.set(str(gui.figsize))
self.figsize_label = ttk.Label(self, text="Figure size:")
self.figsize_small_button = ttk.Radiobutton(
self,
text="small",
variable=self.figsize_var,
value="small",
command=lambda: self.load_figsize_value(gui),
)
self.figsize_medium_button = ttk.Radiobutton(
self,
text="medium",
variable=self.figsize_var,
value="medium",
command=lambda: self.load_figsize_value(gui),
)
self.figsize_large_button = ttk.Radiobutton(
self,
text="large",
variable=self.figsize_var,
value="large",
command=lambda: self.load_figsize_value(gui),
)
# put buttons on grid
self.plot_2d_button.grid(row=0, column=0, columnspan=3, sticky=(tk.W, tk.E))
self.plot_3d_button.grid(row=1, column=0, columnspan=3, sticky=(tk.W, tk.E))
self.toolbar_button.grid(row=2, column=0, columnspan=3, sticky=(tk.W, tk.E))
self.figsize_label.grid(row=3, column=0, columnspan=3, sticky=(tk.W, tk.E))
self.figsize_small_button.grid(row=4, column=0, sticky=(tk.W, tk.E))
self.figsize_medium_button.grid(row=4, column=1, sticky=(tk.W, tk.E))
self.figsize_large_button.grid(row=4, column=2, sticky=(tk.W, tk.E))
self.rowconfigure(0, weight=1)
self.rowconfigure(1, weight=1)
self.rowconfigure(2, weight=1)
self.rowconfigure(3, weight=1)
self.rowconfigure(4, weight=1)
self.columnconfigure(0, weight=1)
self.columnconfigure(1, weight=1)
self.columnconfigure(2, weight=1)
[docs]
def load_toolbar_value(self, gui):
"""Change toolbar value in gui and reload figure frame.
Args:
gui (GUI): main class containing main frames and simulation
"""
if self.toolbar_var.get():
gui.toolbar_on = True
else:
gui.toolbar_on = False
gui.reload_figure_frame()
[docs]
def load_plot_3d_value(self, gui):
"""Change figure display in gui and reload figure frame.
Args:
gui (GUI): main class containing main frames and simulation
"""
if self.plot_3d_var.get():
gui.plot_3d = True
else:
gui.plot_3d = False
gui.reload_figure_frame()
[docs]
def load_figsize_value(self, gui):
"""Change figure size in gui and reload figure frame.
Args:
gui (GUI): main class containing main frames and simulation
"""
gui.figsize = self.figsize_var.get()
gui.reload_figure_frame()