Skip to content

Commit

Permalink
Merge pull request #5 from austinklein/aklein/generate_wcon
Browse files Browse the repository at this point in the history
Aklein/generate wcon
  • Loading branch information
pgleeson authored Aug 15, 2024
2 parents 7e6823e + 165f331 commit e341b31
Show file tree
Hide file tree
Showing 3 changed files with 362 additions and 100 deletions.
103 changes: 103 additions & 0 deletions WormSim/Model/WormPlot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import numpy as np
from matplotlib import pyplot as plt
from matplotlib.colors import LinearSegmentedColormap, Normalize
import json
import argparse
import sys
import os
import math


def validate_file(file_path):
if not os.path.exists(file_path):
raise argparse.ArgumentTypeError(f"The file {file_path} does not exist.")
if not os.path.isfile(file_path):
raise argparse.ArgumentTypeError(f"{file_path} is not a valid file.")
return file_path


def main():

parser = argparse.ArgumentParser(description="Process some arguments.")
parser.add_argument('-f', '--wcon_file', type=validate_file, help='WCON file path')

args = parser.parse_args()

fig, axs = plt.subplots(1, 2, figsize=(14, 3))

with open(args.wcon_file, 'r') as f:
wcon = json.load(f)

title_font_size = 10

t_arr = np.array(wcon["data"][0]["t"])
x_arr = np.array(wcon["data"][0]["x"])
y_arr = np.array(wcon["data"][0]["y"])

num_steps = t_arr.size
tmax = num_steps
num = 60.

axs[0].set_title('2D worm motion', fontsize=title_font_size)

for t in range(0, tmax, int(tmax/num)):
f = float(t)/tmax

color = "#%02x%02x00" % (int(0xFF*(f)),int(0xFF*(1-f)*0.8))

for x, y in zip(x_arr[t], y_arr[t]):
axs[0].plot(x, y, '.', color=color, markersize=3 if t==0 else 0.4)

axs[0].set_aspect('equal')

x_transposed = x_arr.T
y_transposed = y_arr.T

# TODO: Below is same utility function as in WormView.py. Move to a utility file.
r = 40e-3
n_bar = x_transposed.shape[0]
num_steps = x_transposed.shape[1]

n_seg = int(n_bar - 1)

# radii along the body of the worm
r_i = np.array([
r * abs(math.sin(math.acos(((i) - n_seg / 2.0) / (n_seg / 2.0 + 0.2))))
for i in range(n_bar)
]).reshape(-1, 1)

diff_x = np.diff(x_transposed, axis=0)
diff_y = np.diff(y_transposed, axis=0)

arctan = np.arctan2(diff_x, -diff_y)
d_arr = np.zeros((n_bar, num_steps))

# d of worm endpoints is based off of two points, whereas d of non-endpoints is based off of 3 (x, y) points
d_arr[:-1, :] = arctan
d_arr[1:, :] = d_arr[1:, :] + arctan
d_arr[1:-1, :] = d_arr[1:-1, :] / 2

# TODO: Determine what "0" starting position should be. Currently, worm facing left while horizontal is "white" in curvature plot.
d_arr = d_arr - np.pi/2

# Blue corresponds to -pi, white corresponds to 0, and red corresponds to pi
colors = [
(0, 'blue'),
(0.5, 'white'),
(1, 'red')
]
custom_cmap = LinearSegmentedColormap.from_list('custom_cmap', colors)
norm = Normalize(vmin=-np.pi, vmax=np.pi)

axs[1].set_title('Body curvature', fontsize=title_font_size)
axs[1].imshow(d_arr, aspect='auto', cmap=custom_cmap, norm=norm)

print(np.max(d_arr))
print(np.min(d_arr))
print(d_arr)

plt.show()


if __name__ == "__main__":
sys.exit(main())
219 changes: 119 additions & 100 deletions WormSim/Model/WormView.py
Original file line number Diff line number Diff line change
@@ -1,122 +1,141 @@
from matplotlib import pyplot as plt

from numpy import genfromtxt
import numpy as np
import json
import math
import os
import argparse
import sys
from Player import Player

data = genfromtxt("simdata.csv", delimiter=",").T

print("Loaded data: %s" % (str(data.shape)))

t = data[0]

x_offset = 1
y_offset = 2
d_offset = 3

"""
for i in [(j*3)+y_offset for j in range(49)]:
plt.plot(t,my_data[i],label=i)
plt.legend()"""

fig, ax = plt.subplots()
plt.get_current_fig_manager().set_window_title("2D WormSim replay")
ax.set_aspect("equal")

usingObjects = os.path.isfile("objects.csv")
if usingObjects:
Objects = genfromtxt("objects.csv", delimiter=",")
for o in Objects:
x = o[0]
y = o[1]
r = o[2]
# print("Circle at (%s, %s), radius %s"%(x,y,r))
circle1 = plt.Circle((x, y), r, color="b")
plt.gca().add_patch(circle1)
else:
print("No objects found")

num_t = 30
timesteps = len(t)

# Using same variable names as WormView.m
Sz = len(data)
Nt = len(data[0])
Nbar = int((Sz - 1) / 3)
NSEG = int(Nbar - 1)
D = 80e-6

R = [
D / 2.0 * abs(math.sin(math.acos(((i) - NSEG / 2.0) / (NSEG / 2.0 + 0.2))))
for i in range(Nbar)
]

CoM = np.zeros([Nt, Nbar, 3])
CoMplot = np.zeros([Nt, 2])
Dorsal = np.zeros([Nbar, 2])
Ventral = np.zeros([Nbar, 2])

print(f"Sz: {Sz}, Nt: {Nt}, Nbar: {Nbar}, NSEG: {NSEG}")
# Dt = data(2,1) - data(1,1);

ventral_plot = None
# Global variables
midline_plot = None
dorsal_plot = None
perimeter_plot = None

from Player import Player
def validate_file(file_path):
if not os.path.exists(file_path):
raise argparse.ArgumentTypeError(f"The file {file_path} does not exist.")
if not os.path.isfile(file_path):
raise argparse.ArgumentTypeError(f"{file_path} is not a valid file.")
return file_path

def get_perimeter(x, y, r):
n_bar = x.shape[0]
num_steps = x.shape[1]

def update(ti):
global dorsal_plot, ventral_plot, midline_plot
f = ti / timesteps
n_seg = int(n_bar - 1)

color = "#%02x%02x00" % (int(0xFF * (f)), int(0xFF * (1 - f) * 0.8))
print("Time step: %s, fract: %f, color: %s" % (ti, f, color))
ds = []
xs = []
ys = []
# radii along the body of the worm
r_i = np.array([
r * abs(math.sin(math.acos(((i) - n_seg / 2.0) / (n_seg / 2.0 + 0.2))))
for i in range(n_bar)
]).reshape(-1, 1)

for i in [(j * 3) + d_offset for j in range(Nbar)]:
ds.append(data[i][ti])
diff_x = np.diff(x, axis=0)
diff_y = np.diff(y, axis=0)

for i in [(j * 3) + x_offset for j in range(Nbar)]:
xs.append(data[i][ti])
arctan = np.arctan2(diff_x, -diff_y)
d_arr = np.zeros((n_bar, num_steps))

for i in [(j * 3) + y_offset for j in range(Nbar)]:
ys.append(data[i][ti])
# d of worm endpoints is based off of two points, whereas d of non-endpoints is based off of 3 (x, y) points
d_arr[:-1, :] = arctan
d_arr[1:, :] = d_arr[1:, :] + arctan
d_arr[1:-1, :] = d_arr[1:-1, :] / 2

for j in range(Nbar):
dX = R[j] * math.cos(ds[j])
dY = R[j] * math.sin(ds[j])
dx = np.cos(d_arr)*r_i
dy = np.sin(d_arr)*r_i

Dorsal[j, 0] = xs[j] + dX
Dorsal[j, 1] = ys[j] + dY
Ventral[j, 0] = xs[j] - dX
Ventral[j, 1] = ys[j] - dY
px = np.zeros((2*n_bar, x.shape[1]))
py = np.zeros((2*n_bar, x.shape[1]))

if dorsal_plot == None:
(dorsal_plot,) = ax.plot(Dorsal[:, 0], Dorsal[:, 1], color="grey", linewidth=1)
else:
dorsal_plot.set_data(Dorsal[:, 0], Dorsal[:, 1])
px[:n_bar, :] = x - dx
px[n_bar:, :] = np.flipud(x + dx) # Make perimeter counter-clockwise

if ventral_plot == None:
(ventral_plot,) = ax.plot(
Ventral[:, 0], Ventral[:, 1], color="grey", linewidth=1
)
else:
ventral_plot.set_data(Ventral[:, 0], Ventral[:, 1])
py[:n_bar, :] = y - dy
py[n_bar:, :] = np.flipud(y + dy) # Make perimeter counter-clockwise

if midline_plot == None:
(midline_plot,) = ax.plot(
xs, ys, color="g", label="t=%sms" % t[ti], linewidth=0.5
)
else:
midline_plot.set_data(xs, ys)
return px, py


ax.plot() # Causes an autoscale update.
def main():

ani = Player(fig, update, maxi=timesteps - 1)
# Default behavior is to use (px, py) if it exists, and if it doesn’t then automatically generate the perimeter from the midline.
parser = argparse.ArgumentParser(description="Process some arguments.")
parser.add_argument('-f', '--wcon_file', type=validate_file, help='WCON file path')
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.')
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.')
parser.add_argument('-r', '--minor_radius', type=float, default=40e-3, help='Minor radius of the worm in millimeters (default: 40e-3)', required=False)

plt.show()
args = parser.parse_args()

fig, ax = plt.subplots()
plt.get_current_fig_manager().set_window_title("2D WormSim replay")
ax.set_aspect("equal")

with open(args.wcon_file, 'r') as f:
wcon = json.load(f)

if "@CelegansNeuromechanicalGaitModulation" in wcon:
center_x_arr = wcon["@CelegansNeuromechanicalGaitModulation"]["objects"]["circles"]["x"]
center_y_arr = wcon["@CelegansNeuromechanicalGaitModulation"]["objects"]["circles"]["y"]
radius_arr = wcon["@CelegansNeuromechanicalGaitModulation"]["objects"]["circles"]["r"]

for center_x, center_y, radius in zip(center_x_arr, center_y_arr, radius_arr):
circle = plt.Circle((center_x, center_y), radius, color="b")
ax.add_patch(circle)
else:
print("No objects found")

# Set the limits of the plot since we don't have any objects to help with autoscaling
ax.set_xlim([-1.5, 1.5])
ax.set_ylim([-1.5, 1.5])

t = np.array(wcon["data"][0]["t"])
x = np.array(wcon["data"][0]["x"]).T
y = np.array(wcon["data"][0]["y"]).T

num_steps = t.size

if "px" in wcon["data"][0] and "py" in wcon["data"][0]:
if args.ignore_wcon_perimeter:
print("Ignoring (px, py) values in WCON file and computing perimeter from midline.")
px, py = get_perimeter(x, y, args.minor_radius)
else:
print("Using (px, py) from WCON file")
px = np.array(wcon["data"][0]["px"]).T
py = np.array(wcon["data"][0]["py"]).T
else:
if not args.suppress_automatic_generation:
print("Computing perimeter from midline")
px, py = get_perimeter(x, y, args.minor_radius)
else:
print("Not computing perimeter from midline")
px = None
py = None

def update(ti):
global midline_plot, perimeter_plot
f = ti / num_steps

color = "#%02x%02x00" % (int(0xFF * (f)), int(0xFF * (1 - f) * 0.8))
print("Time step: %s, fract: %f, color: %s" % (ti, f, color))

if midline_plot is None:
(midline_plot,) = ax.plot(x[:, ti], y[:, ti], color="g", label="t=%sms" % t[ti], linewidth=0.5)
else:
midline_plot.set_data(x[:, ti], y[:, ti])

if px is not None and py is not None:
if perimeter_plot is None:
(perimeter_plot,) = ax.plot(px[:, ti], py[:, ti], color="grey", linewidth=1)
else:
perimeter_plot.set_data(px[:, ti], py[:, ti])

ani = Player(fig, update, maxi=num_steps - 1)

# TODO WormViewCSV and WormViewWCON - should WormViewCSV just be the original WormView? That's what it initially did.
# 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.

plt.show()

if __name__ == "__main__":
sys.exit(main())
Loading

0 comments on commit e341b31

Please sign in to comment.