Skip to content

Commit 191556f

Browse files
carlocastoldisfmig
andauthored
improve resulting displacement vectors (#657)
* cart2pol now always sets phi=0 when rho=0 * Improve displace vectors computation * deprecate movement.kinematics.compute_displacement * add movement.kinematics.compute_forward_displacement * add movement.kinematics.compute_backward_displacement * adapt examples and docstrings accordingly * adapt tests to the implementation changes * update compute_kinematics example shows the existance of compute_path_length * Add backward displacement to the example. Small edits to wording and flow * Fix double ticks * Deprecation warning after short summary following numpydoc style https://numpydoc.readthedocs.io/en/latest/format.html#deprecation-warning * Factor out deprecation test for kinematics as a general test * Clarify handling of unsigned zeros in cart2pol * Fix rendering bits * Fix broken SLEAP link --------- Co-authored-by: sfmig <[email protected]>
1 parent 3edbf15 commit 191556f

File tree

9 files changed

+401
-113
lines changed

9 files changed

+401
-113
lines changed

examples/compute_kinematics.py

Lines changed: 115 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Compute and visualise kinematics.
1+
"""Compute and visualise kinematics
22
====================================
33
44
Compute displacement, velocity and acceleration, and
@@ -12,6 +12,7 @@
1212
# For interactive plots: install ipympl with `pip install ipympl` and uncomment
1313
# the following line in your notebook
1414
# %matplotlib widget
15+
import numpy as np
1516
from matplotlib import pyplot as plt
1617

1718
import movement.kinematics as kin
@@ -50,9 +51,8 @@
5051
# ---------------------------
5152
# First, let's visualise the trajectories of the mice in the XY plane,
5253
# colouring them by individual.
53-
# We use :func:`movement.plots.plot_centroid_trajectory` which is a wrapper
54-
# around :func:`matplotlib.pyplot.scatter` that simplifies plotting the
55-
# trajectories of individuals in the dataset.
54+
# For this we can use :func:`movement.plots.plot_centroid_trajectory`
55+
# which is a wrapper around :func:`matplotlib.pyplot.scatter`.
5656
# The fig and ax objects returned can be used to further customise the plot.
5757

5858
# Create a single figure and axes
@@ -77,6 +77,8 @@
7777
label=mouse_name,
7878
)
7979
ax.legend().set_alpha(1)
80+
ax.set_xlabel("x (pixels)")
81+
ax.set_ylabel("y (pixels)")
8082
fig.show()
8183

8284
# %%
@@ -86,21 +88,31 @@
8688
# follows the convention for SLEAP and most image processing tools.
8789

8890
# %%
89-
# By default :func:`plot_centroid_trajectory()<movement.plots.\
90-
# plot_centroid_trajectory>` colours data points based on their timestamps:
91-
fig, axes = plt.subplots(3, 1, sharey=True)
92-
for mouse_name, ax in zip(position.individuals.values, axes, strict=False):
91+
# We can also plot the trajectories of the mice in the XY plane independently,
92+
# colouring the data points based on their timestamps. This is the default
93+
# behaviour of
94+
# :func:`plot_centroid_trajectory()<movement.plots.plot_centroid_trajectory>`
95+
# when the ``c`` argument is not provided:
96+
fig, axes = plt.subplots(2, 2, sharey=True)
97+
for mouse_name, ax in zip(
98+
position.individuals.values, axes.flat, strict=False
99+
):
93100
ax.invert_yaxis()
94101
fig, ax = plot_centroid_trajectory(
95102
position,
96103
individual=mouse_name,
97104
ax=ax,
98105
s=2,
99106
)
107+
ax.set_aspect("equal")
108+
ax.set_xlim(150, 1250)
109+
ax.set_ylim(500, 1100)
100110
ax.set_title(f"Trajectory {mouse_name}")
101111
ax.set_xlabel("x (pixels)")
102112
ax.set_ylabel("y (pixels)")
103113
ax.collections[0].colorbar.set_label("Time (frames)")
114+
# Hide the unused subplot (4th one)
115+
axes[1, 1].set_visible(False)
104116
fig.tight_layout()
105117
fig.show()
106118

@@ -111,7 +123,7 @@
111123
# the third mouse (``AEON3B_TP2``) followed an anti-clockwise direction.
112124

113125
# %%
114-
# We can also easily plot the components of the position vector against time
126+
# We can also inspect the components of the position vector against time
115127
# using ``xarray``'s built-in plotting methods. We use
116128
# :meth:`xarray.DataArray.squeeze` to
117129
# remove the dimension of length 1 from the data (the ``keypoints`` dimension).
@@ -124,37 +136,36 @@
124136
# and the ``x`` and ``y`` coordinates of the ``position`` are in pixels.
125137

126138
# %%
127-
# Compute displacement
128-
# ---------------------
139+
# Compute displacement vectors
140+
# ----------------------------
129141
# The :mod:`movement.kinematics` module
130-
# provides functions to compute various kinematic quantities,
131-
# such as displacement, velocity, and acceleration.
132-
# We can start off by computing the distance travelled by the mice along
133-
# their trajectories:
134-
135-
displacement = kin.compute_displacement(position)
142+
# provides functions to compute various kinematic variables,
143+
# such as displacement, velocity, and acceleration. Below we showcase
144+
# how these functions can be used.
145+
#
146+
# We can compute the forward displacement vectors as follows:
147+
forward_displacement = kin.compute_forward_displacement(position)
136148

137149
# %%
138-
# The :func:`movement.kinematics.compute_displacement`
150+
# The :func:`movement.kinematics.compute_forward_displacement`
139151
# function will return a data array equivalent to the ``position`` one,
140-
# but holding displacement data along the ``space`` axis, rather than
141-
# position data.
152+
# but holding displacement data along the ``space`` axis.
142153
#
143-
# The ``displacement`` data array holds, for a given individual and keypoint
144-
# at timestep ``t``, the vector that goes from its previous position at time
145-
# ``t-1`` to its current position at time ``t``.
154+
# The ``forward_displacement`` data array holds, for a given individual and
155+
# keypoint at timestep ``t``, the vector that goes from its current position
156+
# at time ``t`` to its next position at time ``t+1``.
146157

147158
# %%
148-
# And what happens at ``t=0``, since there is no previous timestep?
149-
# We define the displacement vector at time ``t=0`` to be the zero vector.
150-
# This way the shape of the ``displacement`` data array is the
151-
# same as the ``position`` array:
159+
# And what happens in the last timestep, when there is no next timepoint?
160+
# We define the forward displacement vector then to be the
161+
# zero vector. This way the shape of the ``forward_displacement`` data array
162+
# is the same as the ``position`` array:
152163
print(f"Shape of position: {position.shape}")
153-
print(f"Shape of displacement: {displacement.shape}")
164+
print(f"Shape of displacement: {forward_displacement.shape}")
154165

155166
# %%
156-
# We can visualise these displacement vectors with a quiver plot. In this case
157-
# we focus on the mouse ``AEON3B_TP2``:
167+
# We can visualise the forward displacement vectors with a quiver plot. In
168+
# this case we focus on the mouse ``AEON3B_TP2``:
158169
mouse_name = "AEON3B_TP2"
159170

160171
fig = plt.figure()
@@ -169,12 +180,12 @@
169180
cmap="viridis",
170181
)
171182

172-
# plot displacement vectors: at t, vector from t-1 to t
183+
# plot forward displacement vectors: at t, vector from t to t+1
173184
ax.quiver(
174185
position.sel(individuals=mouse_name, space="x"),
175186
position.sel(individuals=mouse_name, space="y"),
176-
displacement.sel(individuals=mouse_name, space="x"),
177-
displacement.sel(individuals=mouse_name, space="y"),
187+
forward_displacement.sel(individuals=mouse_name, space="x"),
188+
forward_displacement.sel(individuals=mouse_name, space="y"),
178189
angles="xy",
179190
scale=1,
180191
scale_units="xy",
@@ -183,39 +194,37 @@
183194
headaxislength=9,
184195
)
185196

186-
ax.axis("equal")
187-
ax.set_xlim(450, 575)
188-
ax.set_ylim(950, 1075)
197+
ax.set_xlim(480, 600)
198+
ax.set_ylim(980, 1080)
189199
ax.set_xlabel("x (pixels)")
190200
ax.set_ylabel("y (pixels)")
191201
ax.set_title(f"Zoomed in trajectory of {mouse_name}")
192202
ax.invert_yaxis()
193203
fig.colorbar(sc, ax=ax, label="time (s)")
194204

205+
195206
# %%
196-
# Notice that this figure is not very useful as a visual check:
197-
# we can see that there are vectors defined for each point in
198-
# the trajectory, but we have no easy way to verify they are indeed
199-
# the displacement vectors from ``t-1`` to ``t``.
207+
# We can visually verify that indeed the forward displacement vector
208+
# connects the previous and current positions as expected.
200209

201210
# %%
202-
# If instead we plot
203-
# the opposite of the displacement vector, we will see that at every time
204-
# ``t``, the vectors point to the position at ``t-1``.
205-
# Remember that the displacement vector is defined as the vector at
206-
# time ``t``, that goes from the previous position ``t-1`` to the
207-
# current position at ``t``. Therefore, the opposite vector will point
208-
# from the position point at ``t``, to the position point at ``t-1``.
211+
# Similarly, with :func:`movement.kinematics.compute_backward_displacement`
212+
# we can compute the backward displacement vectors, which connect the current
213+
# position to the previous one:
214+
backward_displacement = kin.compute_backward_displacement(position)
209215

210216
# %%
211-
# We can easily do this by flipping the sign of the displacement vector in
212-
# the plot above:
213-
mouse_name = "AEON3B_TP2"
217+
# In this case, the backward displacement vector at the first timestep
218+
# is defined as the zero vector, since there is no previous position.
219+
220+
# %%
221+
# Adapting the code snippet from above, we can visually check that the
222+
# backward displacement vector is indeed the reverse of the forward
223+
# displacement vector.
214224

215225
fig = plt.figure()
216226
ax = fig.add_subplot()
217227

218-
# plot position data
219228
sc = ax.scatter(
220229
position.sel(individuals=mouse_name, space="x"),
221230
position.sel(individuals=mouse_name, space="y"),
@@ -224,22 +233,21 @@
224233
cmap="viridis",
225234
)
226235

227-
# plot displacement vectors: at t, vector from t-1 to t
228236
ax.quiver(
229237
position.sel(individuals=mouse_name, space="x"),
230238
position.sel(individuals=mouse_name, space="y"),
231-
-displacement.sel(individuals=mouse_name, space="x"), # flipped sign
232-
-displacement.sel(individuals=mouse_name, space="y"), # flipped sign
239+
backward_displacement.sel(individuals=mouse_name, space="x"),
240+
backward_displacement.sel(individuals=mouse_name, space="y"),
233241
angles="xy",
234242
scale=1,
235243
scale_units="xy",
236244
headwidth=7,
237245
headlength=9,
238246
headaxislength=9,
239247
)
240-
ax.axis("equal")
241-
ax.set_xlim(450, 575)
242-
ax.set_ylim(950, 1075)
248+
249+
ax.set_xlim(480, 600)
250+
ax.set_ylim(980, 1080)
243251
ax.set_xlabel("x (pixels)")
244252
ax.set_ylabel("y (pixels)")
245253
ax.set_title(f"Zoomed in trajectory of {mouse_name}")
@@ -248,30 +256,65 @@
248256

249257

250258
# %%
251-
# Now we can visually verify that indeed the displacement vector
252-
# connects the previous and current positions as expected.
259+
# Compute path length
260+
# --------------------
261+
# We can compute the distance travelled by the
262+
# mouse as the sum of the lengths of all
263+
# displacement vectors along its trajectory.
264+
# Both backward and forward displacement vectors
265+
# should give the same result:
266+
267+
# length of each forward displacement vector
268+
forward_displacement_lengths = compute_norm(
269+
forward_displacement.sel(individuals=mouse_name)
270+
)
253271

254-
# %%
255-
# With the displacement data we can compute the distance travelled by the
256-
# mouse along its trajectory.
272+
# length of each backward displacement vector
273+
backward_displacement_lengths = compute_norm(
274+
backward_displacement.sel(individuals=mouse_name)
275+
)
257276

258-
# length of each displacement vector
259-
displacement_vectors_lengths = compute_norm(
260-
displacement.sel(individuals=mouse_name)
277+
# check their lengths are the same
278+
np.testing.assert_almost_equal(
279+
forward_displacement_lengths.values[:-1], # exclude last timestep
280+
backward_displacement_lengths.values[1:], # exclude first timestep
261281
)
262282

263283
# sum the lengths of all displacement vectors (in pixels)
264-
total_displacement = displacement_vectors_lengths.sum(dim="time").values[0]
284+
total_displacement_fwd = forward_displacement_lengths.sum(dim="time").values[0]
285+
total_displacement_bwd = backward_displacement_lengths.sum(dim="time").values[
286+
0
287+
]
265288

266289
print(
267-
f"The mouse {mouse_name}'s trajectory is {total_displacement:.2f} "
268-
"pixels long"
290+
f"The mouse {mouse_name}'s path length is {total_displacement_fwd:.2f} "
291+
"pixels long (using forward displacement)"
269292
)
293+
print(
294+
f"The mouse {mouse_name}'s path length is {total_displacement_bwd:.2f} "
295+
"pixels long (using backward displacement)"
296+
)
297+
298+
299+
# %%
300+
# We provide a convenience function
301+
# :func:`movement.kinematics.compute_path_length`
302+
# to compute the path length for all individuals and keypoints in a position
303+
# data array. We can verify that using this function gives the same result
304+
# as before for the ``AEON3B_TP2`` mouse:
305+
306+
path_lengths = kin.compute_path_length(ds.position)
307+
308+
for mouse_name in path_lengths.individuals.values:
309+
print(
310+
f"Path length for {mouse_name}: "
311+
f"{path_lengths.sel(individuals=mouse_name).values[0]:.2f} pixels"
312+
)
270313

271314
# %%
272315
# Compute velocity
273316
# ----------------
274-
# We can easily compute the velocity vectors for all individuals in our data
317+
# We can also compute the velocity vectors for all individuals in our data
275318
# array:
276319
velocity = kin.compute_velocity(position)
277320

@@ -294,9 +337,9 @@
294337
# %%
295338
# The components of the velocity vector seem noisier than the components of
296339
# the position vector.
297-
# This is expected, since we are deriving the velocity using differences in
340+
# This is expected, since we are estimating the velocity using differences in
298341
# position (which is somewhat noisy), over small stepsizes.
299-
# More specifically, we use numpy's gradient implementation, which
342+
# More specifically, we use :func:`numpy.gradient` internally, which
300343
# uses second order central differences.
301344

302345
# %%
@@ -315,7 +358,7 @@
315358

316359
# %%
317360
# To visualise the direction of the velocity vector at each timestep, we can
318-
# use a quiver plot:
361+
# again use a quiver plot:
319362
mouse_name = "AEON3B_TP2"
320363
fig = plt.figure()
321364
ax = fig.add_subplot()

movement/io/load_poses.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ def from_sleap_file(
287287
----------
288288
.. [1] https://docs.sleap.ai/latest/learnings/export-analysis/
289289
.. [2] https://github.com/talmolab/sleap/blob/v1.3.3/sleap/info/write_tracking_h5.py#L59
290-
.. [3] https://docs.sleap.ai/latest/how-to-guides/tracking-and-proofreading/
290+
.. [3] https://docs.sleap.ai/latest/guides/tracking-and-proofreading/
291291
292292
Examples
293293
--------

movement/kinematics/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from movement.kinematics.kinematics import (
55
compute_acceleration,
66
compute_displacement,
7+
compute_forward_displacement,
8+
compute_backward_displacement,
79
compute_path_length,
810
compute_speed,
911
compute_time_derivative,
@@ -18,6 +20,8 @@
1820

1921
__all__ = [
2022
"compute_displacement",
23+
"compute_forward_displacement",
24+
"compute_backward_displacement",
2125
"compute_velocity",
2226
"compute_acceleration",
2327
"compute_speed",

0 commit comments

Comments
 (0)