Skip to content

Commit

Permalink
feature: mouse hover when paused updates shematic state
Browse files Browse the repository at this point in the history
  • Loading branch information
campagnola committed Sep 20, 2022
1 parent 2d256f7 commit 6f61578
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 46 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Requirements

* Python 3.10 or higher
* NumPy, SciPy
* PyQt6
* PyQt5 or 6
* PyQtGraph
* lmfit

Expand Down
91 changes: 70 additions & 21 deletions neurodemo/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,12 @@ def __init__(self, multiprocessing=False):
else:
print(sys.platform, "running with mp")
self.ndemo = self.proc._import('neurodemo')


self.scrolling_plot_duration = 1.0 * NU.s
self.result_buffer = ResultBuffer(max_duration=self.scrolling_plot_duration)

self.dt = 20e-6 * NU.s
self.integrator = 'solve_ivp'
self.scrolling_plot_duration = 1.0 * NU.s
self.sim = self.ndemo.Sim(temp=6.3, dt=self.dt)
if self.proc is not None:
self.sim._setProxyOptions(deferGetattr=True) # only if using remote process
Expand All @@ -92,15 +94,14 @@ def __init__(self, multiprocessing=False):

mechanisms = [self.clamp, self.hhna, self.leak, self.hhk, self.dexh, self.lgna, self.lgkf, self.lgks]
# loop to run the simulation indefinitely
self.running = False
self.runner = self.ndemo.SimRunner(self.sim)
self.runner.set_speed(0.2)
self.runner.add_request('t')

# if using remote process (only on Windows):
if self.proc is not None:
self.runner.new_result.connect(mp.proxy(self.new_result, autoProxy=False, callSync='off'))
else: # Darwin (macOS) and Linux:
self.runner.new_result.connect(self.new_result,)
self.runner.new_result.connect(self.new_result)

# set up GUI
QtGui.QWidget.__init__(self)
Expand Down Expand Up @@ -199,17 +200,14 @@ def __init__(self, multiprocessing=False):
#self.fullscreen_shortcut.setContext().Qt.ShortcutContext(QtCore.Qt.ApplicationShortcut)
self.show()


def params_changed(self, root, changes):
for param, change, val in changes:
path = self.params.childPath(param)
if path[0] == "Run/Stop":
if self.running is True:
if self.running() is True:
self.stop()
self.running = False
else:
self.start()
self.running = True
if change != 'value':
continue

Expand Down Expand Up @@ -298,10 +296,14 @@ def add_plot(self, key, pname, name):
plt.enableAutoRange(y=True)
else:
plt.setYRange(*yrange)


# Add a vertical line
plt.hover_line = pg.InfiniteLine(pos=0, angle=90, movable=False)
plt.addItem(plt.hover_line, ignoreBounds=True)
plt.hover_line.setVisible(False)

# register this plot for later..
self.channel_plots[key] = plt
self.runner.add_request(key)

# add new plot to splitter and resize all accordingly
sizes = self.plot_splitter.sizes()
Expand All @@ -313,26 +315,54 @@ def add_plot(self, key, pname, name):

# Ask sequence plotter to update as well
self.clamp_param.add_plot(key, label)

# Track mouse over plot
plt.plotItem.scene().sigMouseHover.connect(self.mouse_moved_over_plot)

return plt

def remove_plot(self, key):
plt = self.channel_plots.pop(key)
self.runner.remove_request(key)
self.clamp_param.remove_plot(key)
plt.plotItem.scene().sigMouseHover.disconnect(self.mouse_moved_over_plot)
plt.setParent(None)
plt.close()

def mouse_moved_over_plot(self, items):
# only process hover events while paused
if self.running():
return
item = items[0]
widget = item.getViewWidget()
globalPos = pg.QtGui.QCursor.pos()
localPos = widget.mapFromGlobal(globalPos)
scenePos = item.mapFromDevice(localPos)
viewPos = item.vb.mapSceneToView(scenePos)
self.set_hover_time(viewPos.x())

def set_hover_time(self, t):
"""Move vertical lines to time *t* and update the schematic accordingly.
"""
for plt in self.channel_plots.values():
plt.hover_line.setVisible(True)
plt.hover_line.setPos(t)
state = self.result_buffer.get_state_at_time(t)
if state is not None:
self.neuronview.update_state(state)

def running(self):
return self.runner.running()

def start(self):
self.runner.start(blocksize=2048)
# set button color
for plt in self.channel_plots.values():
plt.hover_line.setVisible(False)

def stop(self):
self.runner.stop()
# reset button color

def reset_dt(self, val):

was_running = self.running
was_running = self.running()
if was_running:
self.stop()
self.dt = val
Expand All @@ -345,7 +375,7 @@ def reset_dt(self, val):
self.set_scrolling_plot_dt(self.dt)

def set_scrolling_plot_dt(self, val):
was_running = self.running
was_running = self.running()
if was_running:
self.stop()
for k in self.channel_plots.keys():
Expand Down Expand Up @@ -382,7 +412,7 @@ def fullscreen(self):
self.plot_splitter.insertWidget(self.fs_widget_index, self.fullscreen_widget)
self.fullscreen_widget = None

def new_result(self, final_state, result):
def new_result(self, result):
for k, plt in self.channel_plots.items():
if k not in result:
continue
Expand All @@ -396,7 +426,10 @@ def new_result(self, final_state, result):
self.clamp_param.new_result(result)

# update the schematic
self.neuronview.update_state(final_state)
self.neuronview.update_state(result.get_final_state())

# store a running buffer of results
self.result_buffer.add(result)

def _get_Eh(self):
ENa = self.params.child('Ions', 'Na')
Expand Down Expand Up @@ -453,7 +486,6 @@ def use_default_erev(self):
self.set_hh_erev(ENa_revs["INa"], EK_revs["IK"], -55*NU.mV, Eh_revs["IH"])
self.set_lg_erev(ENa_revs["INa1"], EK_revs["IKf"], EK_revs["IKs"], -55*NU.mV, )


def set_hh_erev(self, ENa_erev=50*NU.mV, EK_erev=-74*NU.mV,
Eleak_erev=-55*NU.mV, Eh_erev=-43*NU.mV):
"""Set new reversal potentials for the HH currents
Expand Down Expand Up @@ -588,7 +620,6 @@ def set_duration(self, dur):

def append(self, data):
# print("len data, len self.data: ", len(data), len(self.data))
#self.data = np.append(self.data, data)
self.data = np.concatenate((self.data, data), axis=0)
if len(self.data) >= self.npts:
self.data = self.data[-self.npts:]
Expand All @@ -597,3 +628,21 @@ def append(self, data):
# print("appending npts: ", len(self.data), self.npts, self.dt, self.plot_duration)
self.data_curve.setData(t, self.data)


class ResultBuffer:
def __init__(self, max_duration=10):
self.max_duration = max_duration
self.results = []

def add(self, result):
self.results.append(result)

def get_state_at_time(self, t):
if len(self.results) == 0:
return None
if t < 0:
t = self.results[-1]['t'][-1] + t
for result in self.results:
if result['t'][0] <= t <= result['t'][-1]:
return result.get_state_at_time(t)
return None
17 changes: 15 additions & 2 deletions neurodemo/neuronsim.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,12 @@ def __getitem__(self, key):
else:
return self.extra[key]

def __contains__(self, key):
# allow lookup by (object, var)
if isinstance(key, tuple):
key = key[0].name + "." + key[1]
return key in self.indexes or key in self.dep_vars or key in self.extra

def __str__(self):
rep = "SimState:\n"
for i, k in enumerate(self.difeq_vars):
Expand All @@ -225,14 +231,21 @@ def get_final_state(self):
"""Return a dictionary of all diff. eq. state variables and dependent
variables for all objects in the simulation.
"""
return self.get_state_at_index(-1)

def get_state_at_time(self, t):
index = np.searchsorted(self['t'], t)
return self.get_state_at_index(index)

def get_state_at_index(self, index):
state = {}
s = self.copy()
clip = not np.isscalar(self["t"])
if clip:
# only get results for the last timepoint
# print('state: ', self.state)
# if self.integrator == 'odeint':
s.set_state(self.state[:, -1])
s.set_state(self.state[:, index])
# else:
# s.set_state(self.state[:, -1])

Expand All @@ -242,7 +255,7 @@ def get_final_state(self):
state[k] = s[k]
for k, v in self.extra.items():
if clip:
state[k] = v[-1]
state[k] = v[index]
else:
state[k] = v

Expand Down
28 changes: 6 additions & 22 deletions neurodemo/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,9 @@


class SimRunner(QtCore.QObject):
"""Run a simulation continuously and emit signals whenever results are ready.
Results are emitted with a dictionary containing all state variables for the
final timepoint in the simulation, as well as the complete time-record for
any variables that have been requested using add_request().
"""Run a simulation continuously and emit signals whenever results are ready.
"""
new_result = QtCore.Signal(object, object) # final_state, requested_records
new_result = QtCore.Signal(object)

def __init__(self, sim):
QtCore.QObject.__init__(self)
Expand All @@ -29,17 +25,10 @@ def __init__(self, sim):

self.sim = sim
self.speed = 1.0
self.requests = []
self.timer = QtCore.QTimer()
self.timer.timeout.connect(self.run_once)
self.counter = 0

def add_request(self, key):
self.requests.append(key)

def remove_request(self, key):
self.requests.remove(key)

def start(self, blocksize=500, **kwds):
self.starttime = def_timer()
self.blocksize = blocksize
Expand All @@ -49,19 +38,14 @@ def start(self, blocksize=500, **kwds):
def stop(self):
self.timer.stop()

def running(self):
return self.timer.isActive()

def run_once(self):
self.counter += 1
blocksize = int(max(2, self.blocksize * self.speed))
result = self.sim.run(blocksize, **self.run_args)
rec = {}
for key in self.requests:
try:
data = result[key]
except KeyError:
continue
rec[key] = data
state = result.get_final_state()
self.new_result.emit(state, rec)
self.new_result.emit(result)

def set_speed(self, speed):
self.speed = speed

0 comments on commit 6f61578

Please sign in to comment.