Skip to content

Commit 87fe925

Browse files
committed
1 parent 79b9aaf commit 87fe925

File tree

4 files changed

+76443
-0
lines changed

4 files changed

+76443
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/__pycache__

Player.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Based on example at https://stackoverflow.com/questions/46325447/animated-interactive-plot-using-matplotlib
2+
3+
import numpy as np
4+
import matplotlib.pyplot as plt
5+
from matplotlib.animation import FuncAnimation
6+
import mpl_toolkits.axes_grid1
7+
import matplotlib.widgets
8+
9+
10+
class Player(FuncAnimation):
11+
def __init__(
12+
self,
13+
fig,
14+
func,
15+
frames=None,
16+
init_func=None,
17+
fargs=None,
18+
save_count=None,
19+
mini=0,
20+
maxi=100,
21+
pos=(0.125, 0.92),
22+
**kwargs
23+
):
24+
self.i = 0
25+
self.min = mini
26+
self.max = maxi
27+
self.runs = True
28+
self.forwards = True
29+
self.fig = fig
30+
self.func = func
31+
self.setup(pos)
32+
FuncAnimation.__init__(
33+
self,
34+
self.fig,
35+
self.update,
36+
frames=self.play(),
37+
init_func=init_func,
38+
fargs=fargs,
39+
save_count=save_count,
40+
**kwargs
41+
)
42+
43+
def play(self):
44+
while self.runs:
45+
self.i = self.i + self.forwards - (not self.forwards)
46+
if self.i > self.min and self.i < self.max:
47+
yield self.i
48+
else:
49+
self.stop()
50+
yield self.i
51+
52+
def start(self):
53+
self.runs = True
54+
self.event_source.start()
55+
56+
def stop(self, event=None):
57+
self.runs = False
58+
self.event_source.stop()
59+
60+
def forward(self, event=None):
61+
self.forwards = True
62+
self.start()
63+
64+
def backward(self, event=None):
65+
self.forwards = False
66+
self.start()
67+
68+
def oneforward(self, event=None):
69+
self.forwards = True
70+
self.onestep()
71+
72+
def onebackward(self, event=None):
73+
self.forwards = False
74+
self.onestep()
75+
76+
def onestep(self):
77+
if self.i > self.min and self.i < self.max:
78+
self.i = self.i + self.forwards - (not self.forwards)
79+
elif self.i == self.min and self.forwards:
80+
self.i += 1
81+
elif self.i == self.max and not self.forwards:
82+
self.i -= 1
83+
self.func(self.i)
84+
self.slider.set_val(self.i)
85+
self.fig.canvas.draw_idle()
86+
87+
def setup(self, pos):
88+
playerax = self.fig.add_axes([pos[0], pos[1], 0.64, 0.04])
89+
divider = mpl_toolkits.axes_grid1.make_axes_locatable(playerax)
90+
bax = divider.append_axes("right", size="80%", pad=0.05)
91+
sax = divider.append_axes("right", size="80%", pad=0.05)
92+
fax = divider.append_axes("right", size="80%", pad=0.05)
93+
ofax = divider.append_axes("right", size="100%", pad=0.05)
94+
sliderax = divider.append_axes("right", size="500%", pad=0.07)
95+
self.button_oneback = matplotlib.widgets.Button(playerax, label="$\u29CF$")
96+
self.button_back = matplotlib.widgets.Button(bax, label="$\u25C0$")
97+
self.button_stop = matplotlib.widgets.Button(sax, label="$\u25A0$")
98+
self.button_forward = matplotlib.widgets.Button(fax, label="$\u25B6$")
99+
self.button_oneforward = matplotlib.widgets.Button(ofax, label="$\u29D0$")
100+
self.button_oneback.on_clicked(self.onebackward)
101+
self.button_back.on_clicked(self.backward)
102+
self.button_stop.on_clicked(self.stop)
103+
self.button_forward.on_clicked(self.forward)
104+
self.button_oneforward.on_clicked(self.oneforward)
105+
self.slider = matplotlib.widgets.Slider(
106+
sliderax, "", self.min, self.max, valinit=self.i
107+
)
108+
self.slider.on_changed(self.set_pos)
109+
110+
def set_pos(self, i):
111+
self.i = int(self.slider.val)
112+
self.func(self.i)
113+
114+
def update(self, i):
115+
self.slider.set_val(i)
116+
117+
118+
if __name__ == "__main__":
119+
### using this class is as easy as using FuncAnimation:
120+
121+
fig, ax = plt.subplots()
122+
x = np.linspace(0, 6 * np.pi, num=100)
123+
y = np.sin(x)
124+
125+
ax.plot(x, y)
126+
(point,) = ax.plot([], [], marker="o", color="crimson", ms=15)
127+
128+
def update(i):
129+
point.set_data(x[i], y[i])
130+
131+
ani = Player(fig, update, maxi=len(y) - 1)
132+
133+
plt.show()

WormView.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
from matplotlib import pyplot as plt
2+
import numpy as np
3+
import json
4+
import math
5+
import os
6+
import argparse
7+
import sys
8+
from Player import Player
9+
10+
# Global variables
11+
midline_plot = None
12+
perimeter_plot = None
13+
14+
def validate_file(file_path):
15+
if not os.path.exists(file_path):
16+
raise argparse.ArgumentTypeError(f"The file {file_path} does not exist.")
17+
if not os.path.isfile(file_path):
18+
raise argparse.ArgumentTypeError(f"{file_path} is not a valid file.")
19+
return file_path
20+
21+
def get_perimeter(x, y, r):
22+
n_bar = x.shape[0]
23+
num_steps = x.shape[1]
24+
25+
n_seg = int(n_bar - 1)
26+
27+
# radii along the body of the worm
28+
r_i = np.array([
29+
r * abs(math.sin(math.acos(((i) - n_seg / 2.0) / (n_seg / 2.0 + 0.2))))
30+
for i in range(n_bar)
31+
]).reshape(-1, 1)
32+
33+
diff_x = np.diff(x, axis=0)
34+
diff_y = np.diff(y, axis=0)
35+
36+
arctan = np.arctan2(diff_x, -diff_y)
37+
d_arr = np.zeros((n_bar, num_steps))
38+
39+
d_mask = np.full((n_bar,num_steps), False)
40+
arctan_diff = np.abs(np.diff(arctan, axis = 0)) > np.pi
41+
d_mask[1:-1,:] = arctan_diff
42+
43+
44+
# d of worm endpoints is based off of two points, whereas d of non-endpoints is based off of 3 (x, y) points
45+
46+
d_arr[:-1, :] = arctan
47+
d_arr[1:, :] = d_arr[1:, :] + arctan
48+
d_arr[1:-1, :] = d_arr[1:-1, :] / 2
49+
d_arr = d_arr - np.pi*d_mask
50+
dx = np.cos(d_arr)*r_i
51+
dy = np.sin(d_arr)*r_i
52+
53+
54+
px = np.zeros((2*n_bar, x.shape[1]))
55+
py = np.zeros((2*n_bar, x.shape[1]))
56+
57+
px[:n_bar, :] = x - dx
58+
px[n_bar:, :] = np.flipud(x + dx) # Make perimeter counter-clockwise
59+
60+
py[:n_bar, :] = y - dy
61+
py[n_bar:, :] = np.flipud(y + dy) # Make perimeter counter-clockwise
62+
63+
return px, py
64+
65+
66+
def main():
67+
68+
# Default behavior is to use (px, py) if it exists, and if it doesn’t then automatically generate the perimeter from the midline.
69+
parser = argparse.ArgumentParser(description="Open a player for the worm behaviour.")
70+
parser.add_argument('-f', '--wcon_file', type=validate_file, help='WCON file path', required=True )
71+
parser.add_argument('-nogui', action='store_true', help="Just load file, don't show GUI")
72+
parser.add_argument('-s', '--suppress_automatic_generation', action='store_true', help='Suppress the automatic generation of a perimeter which would be computed from the midline of the worm. If (px, py) is not specified in the WCON, a perimeter will not be shown.')
73+
parser.add_argument('-i', '--ignore_wcon_perimeter', action='store_true', help='Ignore (px, py) values in the WCON. Instead, a perimeter is automatically generated based on the midline of the worm.')
74+
parser.add_argument('-r', '--minor_radius', type=float, default=40e-3, help='Minor radius of the worm in millimeters (default: 40e-3)', required=False)
75+
76+
args = parser.parse_args()
77+
78+
fig, ax = plt.subplots()
79+
plt.get_current_fig_manager().set_window_title("WCON replay")
80+
ax.set_aspect("equal")
81+
82+
with open(args.wcon_file, 'r') as f:
83+
wcon = json.load(f)
84+
85+
if "@CelegansNeuromechanicalGaitModulation" in wcon:
86+
center_x_arr = wcon["@CelegansNeuromechanicalGaitModulation"]["objects"]["circles"]["x"]
87+
center_y_arr = wcon["@CelegansNeuromechanicalGaitModulation"]["objects"]["circles"]["y"]
88+
radius_arr = wcon["@CelegansNeuromechanicalGaitModulation"]["objects"]["circles"]["r"]
89+
90+
for center_x, center_y, radius in zip(center_x_arr, center_y_arr, radius_arr):
91+
circle = plt.Circle((center_x, center_y), radius, color="b")
92+
ax.add_patch(circle)
93+
else:
94+
print("No objects found")
95+
96+
# Set the limits of the plot since we don't have any objects to help with autoscaling
97+
98+
ax.set_ylim([-1.5, 1.5])
99+
100+
t = np.array(wcon["data"][0]["t"])
101+
x = np.array(wcon["data"][0]["x"]).T
102+
y = np.array(wcon["data"][0]["y"]).T
103+
104+
print(f"Range of time: {t[0]}->{t[0]}; x range: {x.max()}->{x.min()}; y range: {y.max()}->{y.min()}")
105+
factor = 0.05
106+
if abs(x.max()-x.min())>abs(y.max()-y.min()):
107+
side = abs(x.max()-x.min())
108+
ax.set_xlim([x.min()-side*factor, x.max()+side*factor])
109+
mid = (y.max()+y.min())/2
110+
ax.set_ylim([mid-side*(.5+factor), mid+side*(.5+factor)])
111+
else:
112+
side = abs(y.max()-y.min())
113+
ax.set_ylim([y.min()-side*factor, y.max()+side*factor])
114+
mid = (x.max()+x.min())/2
115+
ax.set_xlim([mid-side*(.5+factor), mid+side*(.5+factor)])
116+
117+
118+
num_steps = t.size
119+
120+
if "px" in wcon["data"][0] and "py" in wcon["data"][0]:
121+
if args.ignore_wcon_perimeter:
122+
print("Ignoring (px, py) values in WCON file and computing perimeter from midline.")
123+
px, py = get_perimeter(x, y, args.minor_radius)
124+
else:
125+
print("Using (px, py) from WCON file")
126+
px = np.array(wcon["data"][0]["px"]).T
127+
py = np.array(wcon["data"][0]["py"]).T
128+
else:
129+
if not args.suppress_automatic_generation:
130+
print("Computing perimeter from midline")
131+
px, py = get_perimeter(x, y, args.minor_radius)
132+
else:
133+
print("Not computing perimeter from midline")
134+
px = None
135+
py = None
136+
137+
def update(ti):
138+
global midline_plot, perimeter_plot
139+
f = ti / num_steps
140+
141+
color = "#%02x%02x00" % (int(0xFF * (f)), int(0xFF * (1 - f) * 0.8))
142+
print("Time step: %s, fract: %f, color: %s" % (ti, f, color))
143+
144+
if midline_plot is None:
145+
(midline_plot,) = ax.plot(x[:, ti], y[:, ti], color="g", label="t=%sms" % t[ti], linewidth=0.5)
146+
else:
147+
midline_plot.set_data(x[:, ti], y[:, ti])
148+
149+
if px is not None and py is not None:
150+
if perimeter_plot is None:
151+
(perimeter_plot,) = ax.plot(px[:, ti], py[:, ti], color="grey", linewidth=1)
152+
else:
153+
perimeter_plot.set_data(px[:, ti], py[:, ti])
154+
155+
ani = Player(fig, update, maxi=num_steps - 1)
156+
157+
# TODO WormViewCSV and WormViewWCON - should WormViewCSV just be the original WormView? That's what it initially did.
158+
# TODO Could take out Player and WormViewWCON into separate repo - Taking out Player could be ugly. It is quite coupled with WormView due to the update function.
159+
160+
if not args.nogui:
161+
plt.show()
162+
163+
if __name__ == "__main__":
164+
sys.exit(main())

0 commit comments

Comments
 (0)