OSCR

Cortical development dynamics across autism spectrum disorder mouse models.

Code ↔ Paper

4 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 4 matches
  1. [1] § Methods › Behavioural analysis of Bckdk-mutant and Kmt5b-mutant animals › Analysis of mouse pose estimation and gait ↔ tadpose/gait.py, lines 962–1074 · score 0.74 · gait metric, body length, neck, nose, displacement, tail
  2. [2] § Methods › Behavioural analysis of Bckdk-mutant and Kmt5b-mutant animals › Analysis of mouse pose estimation and gait ↔ tadpose/gait.py, lines 564–678 · score 0.63 · opposite paw, stance phase, paw part, gait, validated, Strides
  3. [3] § Methods › Behavioural analysis of Bckdk-mutant and Kmt5b-mutant animals › Analysis of mouse pose estimation and gait ↔ tadpose/gait.py, lines 392–504 · score 0.63 · opposite paw, stance phase, paw part, gait, validated, Strides
  4. [4] § Methods › Behavioural analysis of Bckdk-mutant and Kmt5b-mutant animals › Analysis of mouse pose estimation and gait ↔ tadpose/gait.py, lines 41–87 · score 0.58 · peak prominence, find peaks, Gaussian, signal, paw, smoothed

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Python · 1,353 lines · 51 KB · BSD-3-Clause · 3 matches

  1. import numpy as np
  2. import pandas as pd
  3. from .analysis import angular_velocity
  4. from matplotlib import pyplot as plt
  5. from scipy import ndimage as ndi
  6. from scipy.signal import find_peaks
  7. from scipy.stats import pearsonr
  8. from scipy.ndimage import gaussian_filter1d
  9. from matplotlib.patches import Rectangle
  10. from skimage import measure as skm
  11. from .alignment import RotationalAligner
  12. from .tadpose import Tadpole
  13. class Stride:
  14. def __init__(self, mouse, paw_part, min_peak_prominence_px, sigma=0, track_idx=0):
  15. """
  16. Initializes the gait analysis object for a specific mouse and paw part.
  17. Args:
  18. mouse: The mouse object associated with the gait analysis.
  19. paw_part: The specific paw part (e.g., 'left_hind', 'right_fore') to analyze.
  20. min_peak_prominence_px (float): Minimum peak prominence in pixels for stride detection.
  21. sigma (float, optional): Standard deviation for Gaussian smoothing of the paw signal. Defaults to 0 (no smoothing).
  22. track_idx (int, optional): Index of the tracking data to use. Defaults to 0.
  23. """
  24. self.mouse = mouse
  25. self.paw_part = paw_part
  26. self.min_peak_prominence = min_peak_prominence_px
  27. self.sigma = sigma
  28. self.track_idx = track_idx
  29. self._stride_frames = None
  30. self.paw_signal = None
  31. self._valid_strides_bind = None
  32. def find_strides(self):
  33. """
  34. Detects stride cycles based on the paw position signal.
  35. This method processes the paw position signal for a specified paw part,
  36. optionally applies Gaussian smoothing, and identifies local maxima and minima
  37. as stance and swing phase transitions, respectively. It then constructs an array
  38. of stride frames, where each row contains the indices for stance start, swing start,
  39. and stride stop for each detected stride.
  40. Sets the following attributes:
  41. - self.paw_signal: The processed paw position signal.
  42. - self._stride_frames: A (N-1, 3) array of stride frame indices, where each row is
  43. [stance_start, swing_start, stride_stop].
  44. """
  45. self.paw_signal = self.mouse.ego_locs(
  46. parts=(self.paw_part,), fill_missing=True, track_idx=self.track_idx
  47. )[:, 0, 1]
  48. if self.sigma > 0:
  49. self.paw_signal = ndi.gaussian_filter1d(self.paw_signal, sigma=self.sigma)
  50. # hpr = hpr[38:]
  51. maxima, _ = find_peaks(self.paw_signal, prominence=self.min_peak_prominence)
  52. minima, _ = find_peaks(-self.paw_signal, prominence=self.min_peak_prominence)
  53. if len(maxima) == 0 or len(minima) == 0:
  54. raise ValueError(
  55. f"Stride.find_strides(): no peaks found for '{self.paw_part}'. "
  56. "Lower min_peak_prominence_px or check the paw signal."
  57. )
  58. if maxima[0] > minima[0]:
  59. minima = minima[1:]
  60. if minima[-1] < maxima[-1]:
  61. maxima = maxima[:-1]
  62. assert len(maxima) == len(minima), (
  63. f"maxima vs minima len {len(maxima)} != {len(minima)}"
  64. )
  65. self._stride_frames = np.zeros((len(maxima), 3), dtype=np.uint32)
  66. self._stride_frames[:, 0] = maxima # stance start
  67. self._stride_frames[:, 1] = minima # swing start
  68. self._stride_frames[:-1, 2] = maxima[1:] # stride stop
  69. self._stride_frames = self._stride_frames[:-1]
  70. def plot(
  71. self,
  72. ax,
  73. slc=None,
  74. color_stance="gray",
  75. alpha_stance=0.1,
  76. color_swing="gray",
  77. alpha_swing=0.02,
  78. ):
  79. """
  80. Plot the paw signal with shaded stance and swing phases.
  81. Parameters
  82. ----------
  83. ax : matplotlib.axes.Axes
  84. Axes to draw on.
  85. slc : slice, optional
  86. Frame range to display. Defaults to the full recording.
  87. color_stance : str
  88. Colour for the paw signal line and stance-phase patches.
  89. alpha_stance : float
  90. Opacity of stance-phase patches.
  91. color_swing : str
  92. Colour for swing-phase patches.
  93. alpha_swing : float
  94. Opacity of swing-phase patches.
  95. """
  96. if slc is None:
  97. slc = slice(0, self.mouse.nframes)
  98. frames = np.arange(slc.start, slc.stop)
  99. sig_min = self.paw_signal[slc].min()
  100. sig_height = self.paw_signal[slc].max() - self.paw_signal[slc].min()
  101. ax.plot(
  102. frames, self.paw_signal[slc], color=color_stance, label=f"{self.paw_part}"
  103. )
  104. # plot stance
  105. first_time = True
  106. for stance_start, swing_start, stride_stop in self.stride_frames:
  107. if stance_start < slc.start or stride_stop > slc.stop:
  108. continue
  109. rect_stance = Rectangle(
  110. (stance_start, sig_min),
  111. swing_start - stance_start,
  112. sig_height,
  113. color=color_stance,
  114. alpha=alpha_stance,
  115. label="Stance" if first_time else None,
  116. )
  117. ax.add_patch(rect_stance)
  118. rect_swing = Rectangle(
  119. (swing_start, sig_min),
  120. stride_stop - swing_start,
  121. sig_height,
  122. color=color_swing,
  123. alpha=alpha_swing,
  124. label="Swing" if first_time else None,
  125. )
  126. ax.add_patch(rect_swing)
  127. first_time = False
  128. ax.set_xlim(slc.start, slc.stop)
  129. ax.set_ylabel(f"Aligned\n{self.paw_part}")
  130. def validate_strides(self, other):
  131. """
  132. Validates the strides of another object by checking if each stride interval in the current object
  133. contains at least one stance start from the other object's stride frames.
  134. """
  135. bins = np.r_[self._stride_frames[:, 0], self._stride_frames[-1, 2]]
  136. other_stance_start = other._stride_frames[:, 0]
  137. hist, edges = np.histogram(other_stance_start, bins=bins)
  138. self._valid_strides_bind = hist >= 1
  139. @property
  140. def stride_frames(self):
  141. """
  142. Validated stride frames as an (N, 3) array.
  143. Returns all detected strides if ``validate_strides`` has not been
  144. called yet, otherwise only strides that contain at least one
  145. contralateral stance start (i.e. bilateral strides).
  146. Returns
  147. -------
  148. np.ndarray of shape (N, 3)
  149. Each row is ``[stance_start, swing_start, stride_stop]`` in
  150. frame indices.
  151. """
  152. if self._valid_strides_bind is None:
  153. strides_bind = np.ones(len(self._stride_frames), dtype=bool)
  154. else:
  155. strides_bind = self._valid_strides_bind
  156. return self._stride_frames[strides_bind]
  157. def describe(self):
  158. """Print a short summary of total and validated stride counts."""
  159. print(
  160. f"Strides for '{self.paw_part}'\n # Strides: {len(self._stride_frames)}\n # Strides (valid): {len(self.stride_frames)}"
  161. )
  162. def iter(self, start, stop):
  163. """
  164. Yield validated strides whose full extent falls within [start, stop].
  165. Parameters
  166. ----------
  167. start : int
  168. First frame of the window (exclusive for stance start).
  169. stop : int
  170. Last frame of the window (exclusive for stride stop).
  171. Yields
  172. ------
  173. tuple of int
  174. ``(stance_start, swing_start, stride_stop)`` for each qualifying stride.
  175. """
  176. a = self.stride_frames[:, 0] > start
  177. b = self.stride_frames[:, 2] < stop
  178. for stance_start, swing_start, stride_stop in self.stride_frames[a & b]:
  179. yield stance_start, swing_start, stride_stop
  180. def strides_in_label(self, mask, min_duration):
  181. """
  182. Return validated strides that fall entirely within labelled mask regions.
  183. Regions shorter than ``min_duration`` frames are ignored. A stride is
  184. included only if it belongs to exactly one qualifying region (strides
  185. that straddle region boundaries are excluded).
  186. Parameters
  187. ----------
  188. mask : np.ndarray of bool, shape (nframes,)
  189. Boolean mask marking frames of interest (e.g. moving bouts).
  190. min_duration : float
  191. Minimum region length in frames.
  192. Returns
  193. -------
  194. np.ndarray of shape (N, 3)
  195. Subset of ``stride_frames`` whose strides lie within a valid region.
  196. """
  197. self._mask = np.zeros_like(mask)
  198. in_label_strides = np.zeros(len(self.stride_frames), dtype=np.uint8)
  199. for rp in skm.regionprops(skm.label(mask)[:, None]):
  200. if rp.area > min_duration:
  201. slc = rp.slice[0]
  202. self._mask[slc] = True
  203. in_label_strides += np.logical_and(
  204. self.stride_frames[:, 0] >= slc.start,
  205. self.stride_frames[:, 2] <= slc.stop,
  206. )
  207. return self.stride_frames[in_label_strides == 1]
  208. def strides_in_label_slice_iter(self, mask, min_duration):
  209. """
  210. Yield frame slices for each mask region longer than ``min_duration``.
  211. Parameters
  212. ----------
  213. mask : np.ndarray of bool, shape (nframes,)
  214. Boolean mask marking frames of interest.
  215. min_duration : float
  216. Minimum region length in frames; shorter regions are skipped.
  217. Yields
  218. ------
  219. slice
  220. Frame slice ``[start, stop)`` of each qualifying region.
  221. """
  222. for rp in skm.regionprops(skm.label(mask)[:, None]):
  223. if rp.area > min_duration:
  224. yield rp.slice[0]
  225. def stride_idx_of_frame(self, frame):
  226. """
  227. Return the index of the stride that contains ``frame``.
  228. Parameters
  229. ----------
  230. frame : int
  231. Frame number to look up.
  232. Returns
  233. -------
  234. int
  235. Index into ``stride_frames`` of the containing stride, or ``-1``
  236. if ``frame`` does not fall within any validated stride.
  237. """
  238. sidx = np.nonzero(
  239. np.logical_and(
  240. self.stride_frames[:, 0] <= frame,
  241. self.stride_frames[:, 2] > frame,
  242. )
  243. )[0]
  244. if len(sidx) == 0:
  245. return -1
  246. return sidx[0]
  247. def project_point_on_line(line_p1, line_p2, pnt, must_be_on_line=False):
  248. """
  249. Project a 2-D point onto the line defined by two anchor points.
  250. Parameters
  251. ----------
  252. line_p1, line_p2 : array-like of shape (2,)
  253. Two distinct points defining the line.
  254. pnt : array-like of shape (2,)
  255. The point to project.
  256. must_be_on_line : bool
  257. If ``True``, raise ``AttributeError`` when the projection falls
  258. outside the segment ``[line_p1, line_p2]`` (i.e. ``t`` not in (0, 1)).
  259. Returns
  260. -------
  261. np.ndarray of shape (2,)
  262. Coordinates of the projected point on the line.
  263. Raises
  264. ------
  265. AttributeError
  266. If ``line_p1 == line_p2`` (degenerate line), or if
  267. ``must_be_on_line`` is ``True`` and the projection is off-segment.
  268. """
  269. line_dist = np.sum((line_p1 - line_p2) ** 2)
  270. if line_dist == 0:
  271. raise AttributeError(
  272. "project_point_on_line(): line_p1 and line_p2 are the same points"
  273. )
  274. t = np.sum((pnt - line_p1) * (line_p2 - line_p1)) / line_dist
  275. if must_be_on_line and not (0 < t < 1):
  276. raise AttributeError("project_point_on_line(): pnt not in line")
  277. pnt_projection = line_p1 + t * (line_p2 - line_p1)
  278. return pnt_projection
  279. class StrideProperties:
  280. def __init__(self, mouse, mask, pixel_size, min_duration_sec, track_idx):
  281. """
  282. Initializes the gait analysis object with mouse data, mask, and analysis parameters.
  283. Args:
  284. mouse: An object representing the mouse, expected to have a 'video_fps' attribute.
  285. mask: The mask to be applied for gait analysis.
  286. pixel_size: The size of a pixel in real-world units.
  287. min_duration_sec: Minimum duration (in seconds) for a valid gait event.
  288. track_idx: Index of the track to be analyzed.
  289. """
  290. self.mouse = mouse
  291. self.fps = mouse.video_fps
  292. self.mask = mask
  293. self.min_duration = min_duration_sec * self.fps
  294. self.pixel_size = pixel_size
  295. self.track_idx = track_idx
  296. self.mask_for_strides = np.zeros_like(mask)
  297. for rp in skm.regionprops(skm.label(mask)[:, None]):
  298. if rp.area > self.min_duration:
  299. slc = rp.slice[0]
  300. self.mask_for_strides[slc] = True
  301. def angular_velocity(self, strides, part_axis):
  302. """
  303. Computes the mean angular velocity for each stride segment defined in the input.
  304. Parameters:
  305. strides: An object providing stride segmentation, expected to have a `strides_in_label` method.
  306. part_axis: A tuple or list specifying the body part axes to compute angular velocity between.
  307. Returns:
  308. np.ndarray: Array of mean angular velocities for each stride segment.
  309. Notes:
  310. - Uses the `angular_velocity` function to compute per-frame angular velocities between specified axes.
  311. - The result is scaled by the frame rate (`self.fps`).
  312. - Only stride segments that satisfy `self.mask` and `self.min_duration` are considered.
  313. """
  314. av_seq = (
  315. angular_velocity(
  316. self.mouse, part_axis[0], part_axis[1], track_idx=self.track_idx
  317. )
  318. * self.fps
  319. )
  320. res = []
  321. for stance_start, swing_start, stride_stop in strides.strides_in_label(
  322. self.mask, self.min_duration
  323. ):
  324. res.append(av_seq[stance_start:stride_stop].mean())
  325. return np.array(res)
  326. def distance(self, strides, part_a, part_b):
  327. """
  328. Calculates the mean Euclidean distance between two specified body parts over each detected stride.
  329. For each stride segment defined by the input `strides`, this method computes the average distance
  330. (in physical units, accounting for `self.pixel_size`) between `part_a` and `part_b` using their
  331. tracked locations.
  332. Args:
  333. strides: An object providing stride segmentation, expected to have a `strides_in_label` method
  334. that yields (stance_start, swing_start, stride_stop) tuples.
  335. part_a: The name or index of the first body part to measure.
  336. part_b: The name or index of the second body part to measure.
  337. Returns:
  338. np.ndarray: An array of mean distances (one per stride) between `part_a` and `part_b`.
  339. """
  340. locs = self.mouse.locs(
  341. parts=(part_a, part_b), track_idx=self.track_idx
  342. ).squeeze()
  343. res = []
  344. for stance_start, swing_start, stride_stop in strides.strides_in_label(
  345. self.mask, self.min_duration
  346. ):
  347. dist = (
  348. np.linalg.norm(
  349. locs[stance_start:stride_stop, 0]
  350. - locs[stance_start:stride_stop, 1],
  351. axis=1,
  352. ).mean()
  353. * self.pixel_size
  354. )
  355. res.append(dist)
  356. return np.array(res)
  357. def stride_length(self, strides):
  358. """
  359. Calculates the stride lengths for a series of strides.
  360. Parameters:
  361. strides: An object containing stride information, including paw part and stride intervals.
  362. Returns:
  363. np.ndarray: An array of stride lengths, where each element corresponds to the distance (in physical units)
  364. between the stance start and stride stop locations for each stride.
  365. Notes:
  366. - Uses the mouse's tracked locations for the specified paw part and track index.
  367. - Stride intervals are determined using the provided mask and minimum duration.
  368. - Distances are scaled by the pixel size to convert from pixels to physical units.
  369. """
  370. locs = self.mouse.locs(
  371. parts=(strides.paw_part,), track_idx=self.track_idx
  372. ).squeeze()
  373. res = []
  374. for stance_start, swing_start, stride_stop in strides.strides_in_label(
  375. self.mask, self.min_duration
  376. ):
  377. dist = (
  378. np.linalg.norm(locs[stance_start] - locs[stride_stop]) * self.pixel_size
  379. )
  380. res.append(dist)
  381. return np.array(res)
  382. def stride_speed(self, strides, part_for_speed):
  383. """
  384. Calculates the average speed of a specified body part during each stride.
  385. Parameters
  386. ----------
  387. strides : object
  388. An object that provides stride intervals via the `strides_in_label` method.
  389. part_for_speed : str or int
  390. The identifier for the body part whose speed is to be calculated.
  391. Returns
  392. -------
  393. np.ndarray
  394. An array containing the mean speed of the specified body part for each stride interval.
  395. Notes
  396. -----
  397. - The speed is computed using the `mouse.speed` method, scaled by `pixel_size` and `fps`.
  398. - Only stride intervals that satisfy the mask and minimum duration are considered.
  399. """
  400. part_speed = (
  401. self.mouse.speed(
  402. part=part_for_speed, sigma=0, pre_sigma=0, track_idx=self.track_idx
  403. )
  404. * self.pixel_size
  405. * self.fps
  406. )
  407. res = []
  408. for stance_start, swing_start, stride_stop in strides.strides_in_label(
  409. self.mask, self.min_duration
  410. ):
  411. res.append(np.nanmean(part_speed[stance_start:stride_stop]))
  412. return np.array(res)
  413. def stride_acceleration(self, strides, part_for_acc):
  414. """
  415. Calculates the average acceleration of a specified body part during each stride.
  416. Parameters
  417. ----------
  418. strides : object
  419. An object that provides stride intervals via the `strides_in_label` method.
  420. part_for_acc : str or int
  421. The identifier for the body part whose acceleration is to be calculated.
  422. Returns
  423. -------
  424. np.ndarray
  425. An array containing the mean acceleration of the specified body part for each stride interval.
  426. Notes
  427. -----
  428. - The acceleration is computed using the `mouse.acceleration` method, scaled by `pixel_size` and `fps`.
  429. - Only stride intervals that satisfy the mask and minimum duration are considered.
  430. """
  431. part_acc = (
  432. self.mouse.acceleration(part=part_for_acc, track_idx=self.track_idx)
  433. * self.pixel_size
  434. * self.fps**2
  435. )
  436. res = []
  437. for stance_start, swing_start, stride_stop in strides.strides_in_label(
  438. self.mask, self.min_duration
  439. ):
  440. res.append(np.nanmean(part_acc[stance_start:stride_stop]))
  441. return np.array(res)
  442. def duty_factor(self, strides):
  443. """
  444. Calculates the duty factor for each stride based on stride frame indices.
  445. The duty factor is defined as the ratio of the stance phase duration to the total stride duration.
  446. Args:
  447. strides: An object that provides the `strides_in_label(mask, min_duration)` method,
  448. which returns a NumPy array of shape (N, 3), where each row contains the start,
  449. stance, and end frame indices for a stride.
  450. Returns:
  451. numpy.ndarray: An array of duty factors for each stride, computed as
  452. (stance_duration / stride_duration).
  453. """
  454. stride_frames = strides.strides_in_label(self.mask, self.min_duration)
  455. stance_duration = stride_frames[:, 1] - stride_frames[:, 0]
  456. stride_duration = stride_frames[:, 2] - stride_frames[:, 0]
  457. return stance_duration / stride_duration
  458. def stride_duration(self, strides):
  459. """
  460. Calculate stance and stride durations for given strides.
  461. Parameters:
  462. strides: An object with a `strides_in_label` method that returns an array of stride frame indices.
  463. The method is called with `self.mask` and `self.min_duration` as arguments.
  464. Returns:
  465. np.ndarray: A stacked array of shape (N, 3), where N is the number of strides.
  466. - The first column contains stance durations (frames).
  467. - The second column contains stride durations (frames).
  468. - The third column contains the starting frame index of each stride.
  469. """
  470. stride_frames = strides.strides_in_label(self.mask, self.min_duration)
  471. stance_duration = stride_frames[:, 1] - stride_frames[:, 0]
  472. stride_duration = stride_frames[:, 2] - stride_frames[:, 0]
  473. return np.stack(
  474. [stance_duration, stride_duration, stride_frames[:, 0]], axis=-1
  475. )
  476. def step_distances(self, strides, strides_opposite, debug_plot=-1):
  477. """
  478. Calculates the step lengths and widths for each stride in a gait cycle.
  479. For each stride in the provided `strides` object, this method finds the corresponding stance phase
  480. in the opposite limb (`strides_opposite`), projects the opposite paw location onto the stride line,
  481. and computes the step length (distance along the stride direction) and step width (perpendicular distance).
  482. Optionally, a debug plot can be generated for a specific stride.
  483. Args:
  484. strides: An object representing the strides of the limb in question, containing stride frames and paw part information.
  485. strides_opposite: An object representing the strides of the opposite limb, containing stride frames and paw part information.
  486. debug_plot (int, optional): The index of the stride for which to generate a debug plot. Defaults to -1 (no plot).
  487. Returns:
  488. np.ndarray: An array of shape (N, 3), where N is the number of strides. Each row contains
  489. [step_length, step_width, step_phase]. Lengths and widths are in physical units
  490. (e.g., cm). Step phase is the fraction of the stride cycle (0–1) at which the
  491. contralateral stance starts.
  492. """
  493. paw_locs = self.mouse.locs(
  494. parts=(
  495. strides_opposite.paw_part,
  496. strides.paw_part,
  497. ),
  498. track_idx=self.track_idx,
  499. )
  500. # strides_frames
  501. strides_frames = strides.strides_in_label(self.mask, self.min_duration)
  502. #
  503. strides_frames_opp = strides_opposite._stride_frames
  504. # real locs 0 for the stride opposite side, 1 for the side in question
  505. cnt = 0
  506. step_widths = []
  507. step_lengths = []
  508. step_phases = []
  509. for stride in strides_frames:
  510. match_index = np.nonzero(
  511. np.logical_and(
  512. stride[0] <= strides_frames_opp[:, 0],
  513. strides_frames_opp[:, 0] < stride[2],
  514. )
  515. )[0]
  516. if len(match_index) == 0:
  517. print(
  518. "WARNING: No match index found. That should not happen. Strides validated??"
  519. )
  520. step_widths.append(np.nan)
  521. step_lengths.append(np.nan)
  522. step_phases.append(np.nan)
  523. continue
  524. stride_opp_stance = strides_frames_opp[match_index[0], 0]
  525. p1 = paw_locs[stride[0], 1]
  526. p2 = paw_locs[stride[2], 1]
  527. p_opp = paw_locs[stride_opp_stance, 0]
  528. try:
  529. p_opp_proj = project_point_on_line(p2, p1, p_opp, must_be_on_line=True)
  530. except AttributeError:
  531. step_widths.append(np.nan)
  532. step_lengths.append(np.nan)
  533. step_phases.append(np.nan)
  534. continue
  535. step_length = np.linalg.norm(p2 - p_opp_proj) * self.pixel_size
  536. step_width = np.linalg.norm(p_opp - p_opp_proj) * self.pixel_size
  537. step_phase = (stride_opp_stance - stride[0]) / (stride[2] - stride[0])
  538. step_phases.append(step_phase)
  539. step_widths.append(step_width)
  540. step_lengths.append(step_length)
  541. if cnt == debug_plot:
  542. f, (ax, ax_p) = plt.subplots(1, 2, figsize=(12, 5))
  543. ax.imshow(
  544. self.mouse.image(stride[0]),
  545. "Reds",
  546. alpha=0.5,
  547. )
  548. ax.imshow(
  549. self.mouse.image(stride[2]),
  550. "Greens",
  551. alpha=0.5,
  552. )
  553. ax.plot((p1[0], p2[0]), (p1[1], p2[1]), "g-", label="Stride length")
  554. ax.plot(*p1, "g.", label="Stride Start")
  555. ax.plot(*p2, "r.", label="Stride End")
  556. ax.plot(
  557. (p_opp[0], p_opp_proj[0]),
  558. (p_opp[1], p_opp_proj[1]),
  559. "b-",
  560. label="Step width",
  561. )
  562. ax.set_aspect(1.0)
  563. ax.set_xlim(p2[0] - 180, p2[0] + 180)
  564. ax.set_ylim(p2[1] + 180, p2[1] - 180)
  565. ax_p.plot(strides.paw_signal)
  566. ax_p.axvline(stride[0], color="g", label="Stride Start")
  567. ax_p.axvline(stride[2], color="r", label="Stride End")
  568. ax_p.set_xlim(int(stride[0]) - 100, int(stride[2]) + 100)
  569. ax_p.set_xlabel("Time (frames)")
  570. ax_p.set_ylabel("Aligned paw y-location")
  571. ax.legend()
  572. cnt += 1
  573. return np.stack((step_lengths, step_widths, step_phases), axis=-1)
  574. def raw_lateral_displacement(self, strides, part, part_to_proj_on, debug_plot=8):
  575. """
  576. Compute signed lateral displacement of a body part relative to the stride axis.
  577. For each stride the stride axis is defined by the trajectory of
  578. ``part_to_proj_on`` from stance start to stride end. Each frame's
  579. location of ``part`` is projected onto that axis; the signed
  580. perpendicular distance is returned (positive = left of the axis,
  581. negative = right).
  582. Parameters
  583. ----------
  584. strides : Stride
  585. Stride object whose ``strides_in_label`` method defines the
  586. stride epochs to analyse.
  587. part : str
  588. Body part whose lateral displacement is measured.
  589. part_to_proj_on : str
  590. Body part whose trajectory defines the reference (stride) axis
  591. (e.g. ``"Spine_Center"``).
  592. debug_plot : int
  593. Index of the stride for which to generate a diagnostic figure.
  594. Pass ``-1`` to suppress all plots.
  595. Returns
  596. -------
  597. dict
  598. Maps stride index (int) to a 1-D ``np.ndarray`` of signed
  599. lateral displacements in pixels, one value per frame within
  600. the stride.
  601. """
  602. def cross2d(x, y):
  603. return x[..., 0] * y[..., 1] - x[..., 1] * y[..., 0]
  604. strides_frames = strides.strides_in_label(self.mask, self.min_duration)
  605. # real locs 0 for the stride opposite side, 1 for the side in question
  606. part_to_proj_on_locs = self.mouse.locs(
  607. parts=(part_to_proj_on,), track_idx=self.track_idx
  608. ).squeeze()
  609. parts_locs = self.mouse.locs(parts=(part,), track_idx=self.track_idx).squeeze()
  610. displacements = {}
  611. for stride_ix, (stance_start, swing_start, stride_end) in enumerate(
  612. strides_frames
  613. ):
  614. p1 = part_to_proj_on_locs[stance_start]
  615. p2 = part_to_proj_on_locs[
  616. min(stride_end + 1, len(part_to_proj_on_locs) - 1)
  617. ]
  618. ps = [parts_locs[pt] for pt in range(stance_start, stride_end + 1)]
  619. ps_proj = [project_point_on_line(p1, p2, p) for p in ps]
  620. ps_proj_sign = [np.sign(cross2d(p2 - p1, p1 - p)) for p in ps]
  621. displacements[stride_ix] = np.array(
  622. [
  623. sig * np.linalg.norm(pp - p)
  624. for pp, p, sig in zip(ps_proj, ps, ps_proj_sign)
  625. ]
  626. )
  627. if stride_ix == debug_plot:
  628. fig = plt.figure(figsize=(6, 10))
  629. axs = fig.subplot_mosaic(
  630. mosaic="""
  631. AAAAAA
  632. AAAAAA
  633. AAAAAA
  634. BBBBBB
  635. """,
  636. )
  637. ax1 = axs["A"]
  638. ax2 = axs["B"]
  639. ax1.imshow(
  640. self.mouse.image(stance_start),
  641. "Reds",
  642. alpha=0.5,
  643. )
  644. ax1.imshow(
  645. self.mouse.image(stride_end),
  646. "Greens",
  647. alpha=0.5,
  648. )
  649. ax1.axline(
  650. p1,
  651. p2,
  652. marker=".",
  653. color="darkblue",
  654. ls="--",
  655. label="Stride Body Axis",
  656. )
  657. ax1.plot(*p1, "go", label="Center Stride Start")
  658. ax1.plot(*p2, "ro", label="Center Stride End")
  659. for p, p_proj, p_sign in zip(ps, ps_proj, ps_proj_sign):
  660. color = "m"
  661. if p_sign < 0:
  662. color = "c"
  663. ax1.plot((p[0], p_proj[0]), (p[1], p_proj[1]), f"{color}")
  664. ax1.plot(
  665. *np.array(ps).T, "-", label=f"{part} Trajectory", color="darkred"
  666. )
  667. ax1.set_aspect(1.0)
  668. ax1.set_xlim(
  669. p1[0] - 180,
  670. p1[0] + 180,
  671. )
  672. ax1.set_ylim(
  673. p1[1] + 180,
  674. p1[1] - 180,
  675. )
  676. ax1.set_axis_off()
  677. ax1.legend()
  678. y = displacements[stride_ix]
  679. x = np.linspace(0, 100, len(y))
  680. ax2.plot(x, y, color="darkred")
  681. ax2.hlines(0, xmin=0, xmax=100, color="darkblue", ls="--")
  682. ax2.set_xlabel("Time (%Stride)")
  683. ax2.set_ylabel("Lateral Displacement (px)")
  684. # axl.plot(range(stance_start, stride_end), displacements[stride_ix])
  685. plt.tight_layout()
  686. return displacements
  687. def lateral_displacements_metrics(
  688. self, strides, part, part_to_proj_on, sigma_pf=3, debug_plot=-1
  689. ):
  690. """
  691. Computes lateral displacement metrics for a specified body part across multiple strides.
  692. For each stride, calculates the maximum lateral displacement and the phase (as a fraction of stride duration)
  693. at which the maximum peak occurs after smoothing the displacement signal.
  694. Args:
  695. strides (iterable): Collection of stride data to analyze.
  696. part (str or int): Identifier for the body part whose displacement is measured.
  697. part_to_proj_on (str or int): Identifier for the body part or axis onto which the displacement is projected.
  698. sigma_pf (float, optional): Standard deviation for Gaussian smoothing of the displacement signal. Default is 3.
  699. debug_plot (int, optional): If non-negative, enables debug plotting for the specified stride index. Default is -1.
  700. Returns:
  701. tuple:
  702. - **np.ndarray of shape (N, 2)**: One row per stride; column 0 is the peak-to-peak
  703. lateral displacement in physical units; column 1 is the phase (0–1) of the
  704. maximum peak.
  705. - **list of np.ndarray**: Raw (unscaled) per-frame displacement vectors, one 1-D
  706. array per stride.
  707. Notes:
  708. - Uses Gaussian smoothing to reduce noise in the displacement signal.
  709. - The phase is computed relative to a normalized stride duration.
  710. """
  711. rld = self.raw_lateral_displacement(
  712. strides, part, part_to_proj_on, debug_plot=debug_plot
  713. )
  714. phase = []
  715. displ = []
  716. raw_vec = []
  717. for k, v in rld.items():
  718. displ.append(v.max() - v.min())
  719. x = np.linspace(0, 1, 100)
  720. y = np.interp(x * (len(v) - 1), np.arange(len(v)), v)
  721. sy = gaussian_filter1d(y, sigma_pf)
  722. i_peaks, _ = find_peaks(sy)
  723. i_max_peak = np.nan
  724. if len(i_peaks) > 0:
  725. i_max_peak = i_peaks[np.argmax(sy[i_peaks])]
  726. phase.append(i_max_peak)
  727. raw_vec.append(v)
  728. return np.stack(
  729. (np.array(displ) * self.pixel_size, np.array(phase) / 100), axis=-1
  730. ), raw_vec
  731. def stride_x_corr(self, strides, xcorr_part, debug_plot=1):
  732. """
  733. Compute the Pearson correlation between the paw signal and a second body part per stride.
  734. Both signals are taken from ego-centric (aligned) coordinates,
  735. z-scored within each stride, then correlated. Strides whose stance
  736. phase is shorter than 2 frames or whose signal has zero variance are
  737. returned as ``np.nan``.
  738. Parameters
  739. ----------
  740. strides : Stride
  741. Stride object defining the paw and the epochs to analyse.
  742. xcorr_part : str
  743. Name of the body part to cross-correlate against the paw signal.
  744. debug_plot : int
  745. Index of the stride for which to show a diagnostic plot.
  746. Pass ``-1`` to suppress all plots.
  747. Returns
  748. -------
  749. tuple
  750. - **np.ndarray of shape (N,)** : Pearson correlation coefficient for each stride
  751. (``nan`` for strides that could not be computed).
  752. - **list of tuple** : Raw signal pairs ``(sig, opp_sig)`` for each stride, where each
  753. element is a pair of 1-D arrays (z-scored paw signal and z-scored partner signal).
  754. """
  755. strides_frames = strides.strides_in_label(self.mask, self.min_duration)
  756. # real locs 0 for the stride opposite side, 1 for the side in question
  757. xcorr_ego_locs = self.mouse.ego_locs(
  758. parts=(
  759. strides.paw_part,
  760. xcorr_part,
  761. ),
  762. track_idx=self.track_idx,
  763. )
  764. res = []
  765. vec = []
  766. for stride_ix, (stance_start, swing_start, stride_end) in enumerate(
  767. strides_frames
  768. ):
  769. # if swing_start - stance_start < 2:
  770. # res.append(np.nan)
  771. # vec.append((np.nan, np.nan))
  772. # continue
  773. sig = xcorr_ego_locs[stance_start:stride_end, 0, 1]
  774. opp_sig = xcorr_ego_locs[stance_start:stride_end, 1, 1]
  775. vec.append((sig, opp_sig))
  776. # if sig.std() == 0 or opp_sig.std() == 0:
  777. # res.append(np.nan)
  778. # vec.append((np.nan, np.nan))
  779. # continue
  780. sig = (sig - sig.mean()) / sig.std()
  781. opp_sig = (opp_sig - opp_sig.mean()) / opp_sig.std()
  782. sig_corr_stride = pearsonr(sig, opp_sig).statistic
  783. # sig_corr_stance = pearsonr(
  784. # sig[: swing_start - stance_start], opp_sig[: swing_start - stance_start]
  785. # ).statistic
  786. # sig_corr_swing = pearsonr(
  787. # sig[swing_start - stance_start :], opp_sig[swing_start - stance_start :]
  788. # ).statistic
  789. res.append(sig_corr_stride)
  790. # res.append((sig_corr_stride, sig_corr_stance, sig_corr_swing))
  791. # cc = np.correlate(sig, opp_sig, mode="full")
  792. if stride_ix == debug_plot:
  793. f, ax = plt.subplots()
  794. ax.plot(
  795. sig,
  796. label=strides.paw_part,
  797. )
  798. ax.plot(opp_sig, label=xcorr_part)
  799. # ax.plot(cc, label="corr")
  800. ax.axvline(
  801. swing_start - stance_start,
  802. color="gray",
  803. ls="--",
  804. label="Swing Start",
  805. )
  806. # ax.set_title(
  807. # f"Stride {stride_ix} xcorr:\nstride: {sig_corr_stride:.3f}\nstance: {sig_corr_stance:.3f}\nswing{sig_corr_swing:.3f}"
  808. # )
  809. ax.set_title(f"Stride {stride_ix} xcorr: {sig_corr_stride:.3f}")
  810. ax.legend()
  811. return np.array(res), vec
  812. def stride_to_dataframe(
  813. sp,
  814. strides,
  815. strides_opposite=None,
  816. stride_type="",
  817. part_for_speed=None,
  818. parts_angular_velocity=None,
  819. parts_lat_displacement=None,
  820. part_xcorr=None,
  821. parts_body_size=None,
  822. ):
  823. """
  824. Compute all per-stride gait metrics and return them as a tidy DataFrame.
  825. One row per stride. Multiple paws can be stacked with pd.concat() because
  826. the 'paw' column distinguishes them.
  827. Parameters
  828. ----------
  829. sp : StrideProperties
  830. Carries mouse, pixel_size, fps, mask, min_duration, track_idx.
  831. strides : Stride
  832. The paw whose strides define the rows.
  833. strides_opposite : Stride, optional
  834. Contralateral paw — required for step_length, step_width, step_phase.
  835. stride_type : str
  836. Label for the limb pair, e.g. "Hind" or "Fore".
  837. part_for_speed : str, optional
  838. Body part used for stride_speed (e.g. "Spine_Center").
  839. parts_angular_velocity : tuple of str, optional
  840. Two body parts defining the angular-velocity axis, e.g. ("Neck_Base", "Tail_Base").
  841. parts_lat_displacement : dict, optional
  842. Mapping ``label -> (part, part_to_proj_on)`` for lateral displacement metrics,
  843. e.g. ``{"Nose": ("Nose", "Spine_Center"), "Tail_base": ("Tail_Base", "Spine_Center")}``.
  844. part_xcorr : str, optional
  845. Body part to cross-correlate the paw signal against.
  846. parts_body_size : tuple of str, optional
  847. Two body-part names (e.g. ``("Neck_Base", "Tail_Base")``) whose mean distance
  848. is stored as ``stride_body_lengths_cm`` for body-length normalisation.
  849. Returns
  850. -------
  851. pd.DataFrame
  852. Always-present columns: Video_fn, Track_idx, Stride_type, Paw,
  853. stance_start_frame, swing_start_frame, stride_stop_frame,
  854. stance_start_x_cm, stance_start_y_cm, stride_stop_x_cm, stride_stop_y_cm,
  855. stride_duration_sec, duty_factor, stride_length_cm.
  856. Optional columns (present when the corresponding argument is supplied):
  857. [stride_speed_cm_s], [angular_vel_deg_s],
  858. [step_length_cm, step_width_cm, step_phase],
  859. [xcorr, xcorr_vec, xcorr_vec_opp],
  860. [lat_displ_dist_<label>_cm, lat_displ_phase_<label>, lat_displ_vec_<label>],
  861. [stride_body_lengths_cm].
  862. """
  863. stride_frames = strides.strides_in_label(sp.mask, sp.min_duration)
  864. if len(stride_frames) == 0:
  865. return pd.DataFrame()
  866. paw_locs = sp.mouse.locs(
  867. parts=(strides.paw_part,), track_idx=sp.track_idx
  868. ).squeeze()
  869. rows = {
  870. "Video_fn": sp.mouse.video_fn,
  871. "Track_idx": sp.track_idx,
  872. "Stride_type": stride_type,
  873. "Paw": strides.paw_part,
  874. "stance_start_frame": stride_frames[:, 0].astype(int),
  875. "swing_start_frame": stride_frames[:, 1].astype(int),
  876. "stride_stop_frame": stride_frames[:, 2].astype(int),
  877. "stance_start_x_cm": paw_locs[stride_frames[:, 0], 0] * sp.pixel_size,
  878. "stance_start_y_cm": paw_locs[stride_frames[:, 0], 1] * sp.pixel_size,
  879. "stride_stop_x_cm": paw_locs[stride_frames[:, 2], 0] * sp.pixel_size,
  880. "stride_stop_y_cm": paw_locs[stride_frames[:, 2], 1] * sp.pixel_size,
  881. "stride_duration_sec": (stride_frames[:, 2] - stride_frames[:, 0]) / sp.fps,
  882. "duty_factor": sp.duty_factor(strides),
  883. "stride_length_cm": sp.stride_length(strides),
  884. }
  885. if part_for_speed is not None:
  886. rows["stride_speed_cm_s"] = sp.stride_speed(strides, part_for_speed)
  887. if parts_angular_velocity is not None:
  888. rows["angular_vel_deg_s"] = sp.angular_velocity(strides, parts_angular_velocity)
  889. if strides_opposite is not None:
  890. step = sp.step_distances(strides, strides_opposite, debug_plot=-1)
  891. rows["step_length_cm"] = step[:, 0]
  892. rows["step_width_cm"] = step[:, 1]
  893. rows["step_phase"] = step[:, 2]
  894. if part_xcorr is not None:
  895. xcorr, vecs = sp.stride_x_corr(strides, part_xcorr, debug_plot=-1)
  896. vec, vec_opp = zip(*vecs)
  897. rows["xcorr"] = xcorr
  898. rows["xcorr_vec"] = vec
  899. rows["xcorr_vec_opp"] = vec_opp
  900. if parts_lat_displacement is not None:
  901. for label, (part, proj_part) in parts_lat_displacement.items():
  902. ld, raw = sp.lateral_displacements_metrics(
  903. strides, part, proj_part, debug_plot=-1
  904. )
  905. rows[f"lat_displ_dist_{label}_cm"] = ld[:, 0]
  906. rows[f"lat_displ_phase_{label}"] = ld[:, 1]
  907. rows[f"lat_displ_vec_{label}"] = raw
  908. if parts_body_size is not None:
  909. rows["stride_body_lengths_cm"] = sp.distance(strides, *parts_body_size)
  910. return pd.DataFrame(rows)
  911. class GaitAnalysis:
  912. """
  913. High-level pipeline for rodent gait analysis from a SLEAP tracking file.
  914. Loads a video/tracking file, sets up rotational alignment, detects
  915. moving bouts, finds and validates stride cycles for each defined limb
  916. pair, and exposes methods to extract and summarise per-stride features.
  917. Parameters
  918. ----------
  919. video_fn : str
  920. Path to the SLEAP HDF5 file (used by ``Tadpole.from_sleap``).
  921. track_idx : int
  922. Which SLEAP track to use when multiple animals are present.
  923. cfg : dict
  924. Configuration dictionary with the following keys:
  925. - ``ALIGN_CENTRAL`` / ``ALIGN_TOP`` : body parts used by
  926. ``RotationalAligner`` to orient each frame head-up.
  927. - ``ARENA_SIZE_CM`` / ``ARENA_SIZE_PX`` : arena dimensions used
  928. to convert pixel distances to centimetres.
  929. - ``MOVING_SIGMA_SEC`` : Gaussian smoothing sigma (seconds) for
  930. the forward-speed signal.
  931. - ``MOVING_THRESH`` : forward-speed threshold (cm/s) above which
  932. the animal is considered moving.
  933. - ``MOVING_MIN_DURATION_SEC`` : minimum bout length (seconds) for
  934. a moving epoch to be included in stride analysis.
  935. - ``PEAK_MIN_PROMINENCE_PX`` : minimum peak prominence (pixels)
  936. for stride detection in the paw signal.
  937. - ``PEAK_SMOOTH_SIGMA_SEC`` : Gaussian smoothing sigma (seconds)
  938. applied to the paw signal before peak finding.
  939. - ``STRIDE_DEF`` : dict mapping a stride-type label (e.g.
  940. ``"Hind"``) to a ``(left_paw_part, right_paw_part)`` tuple.
  941. - ``STRIDE_SPEED_NODE`` : body-part name used for stride speed.
  942. - ``STRIDE_ANG_VEL_AXIS`` : two body-part names defining the
  943. angular-velocity axis.
  944. - ``STRIDE_LAT_DISPL_DEF`` : dict passed to
  945. ``stride_to_dataframe`` for lateral displacement metrics.
  946. - ``ANIMAL_LENGTH_DEF`` : two body-part names whose distance
  947. defines body length for normalisation.
  948. Attributes
  949. ----------
  950. mouse : Tadpole
  951. Loaded tracking object with rotational alignment attached.
  952. pixel_size : float
  953. Centimetres per pixel.
  954. mouse_fw_speed : np.ndarray
  955. Per-frame forward speed in cm/s.
  956. mouse_is_moving_mask : np.ndarray of bool
  957. True for frames where forward speed exceeds ``MOVING_THRESH``.
  958. stride_props : StrideProperties
  959. Shared property computer (pixel size, mask, fps).
  960. stride_objs : dict
  961. Maps each stride-type label to a ``(Stride_left, Stride_right)``
  962. tuple with validated stride cycles.
  963. Methods
  964. -------
  965. plot()
  966. Plot paw signals with stance/swing patches and the speed trace.
  967. compute_raw_features()
  968. Return a tidy DataFrame with one row per stride and one column
  969. per raw metric (lengths, speed, duty factor, etc.).
  970. compute_features()
  971. Like ``compute_raw_features`` but also adds body-length-normalised
  972. columns and the duty-factor temporal-symmetry index.
  973. """
  974. def __init__(self, mouse: Tadpole, track_idx: int, cfg: dict):
  975. self.cfg = cfg
  976. self.mouse = mouse
  977. self.mouse.aligner = RotationalAligner(
  978. central_part=self.cfg["ALIGN_CENTRAL"],
  979. aligned_part=self.cfg["ALIGN_TOP"],
  980. align_to=(0, 1),
  981. )
  982. self.pixel_size = self.cfg["ARENA_SIZE_CM"] / self.cfg["ARENA_SIZE_PX"]
  983. self.speed_calibration_factor = self.pixel_size * self.mouse.video_fps
  984. self.track_idx = track_idx
  985. self.mouse_fw_speed = (
  986. self.mouse.forward_speed(
  987. part="Spine_Center",
  988. sigma=self.cfg["MOVING_SIGMA_SEC"],
  989. track_idx=self.track_idx,
  990. )
  991. * self.speed_calibration_factor
  992. )
  993. self.mouse_is_moving_mask = self.mouse_fw_speed > self.cfg["MOVING_THRESH"]
  994. self.stride_props = StrideProperties(
  995. self.mouse,
  996. self.mouse_is_moving_mask,
  997. pixel_size=self.pixel_size,
  998. min_duration_sec=self.cfg["MOVING_MIN_DURATION_SEC"],
  999. track_idx=self.track_idx,
  1000. )
  1001. self.stride_objs = {}
  1002. for stride_type, (paw_L, paw_R) in self.cfg["STRIDE_DEF"].items():
  1003. strides_L = Stride(
  1004. self.mouse,
  1005. paw_L,
  1006. min_peak_prominence_px=self.cfg["PEAK_MIN_PROMINENCE_PX"],
  1007. sigma=self.cfg["PEAK_SMOOTH_SIGMA_SEC"],
  1008. track_idx=self.track_idx,
  1009. )
  1010. strides_R = Stride(
  1011. self.mouse,
  1012. paw_R,
  1013. min_peak_prominence_px=self.cfg["PEAK_MIN_PROMINENCE_PX"],
  1014. sigma=self.cfg["PEAK_SMOOTH_SIGMA_SEC"],
  1015. track_idx=self.track_idx,
  1016. )
  1017. strides_L.find_strides()
  1018. strides_R.find_strides()
  1019. strides_L.validate_strides(strides_R)
  1020. strides_R.validate_strides(strides_L)
  1021. self.stride_objs[stride_type] = (strides_L, strides_R)
  1022. def plot(self):
  1023. """
  1024. Plot paw signals, stance/swing phases, and the forward-speed trace.
  1025. Produces one figure per stride type defined in ``cfg["STRIDE_DEF"]``.
  1026. Each figure has three vertically stacked subplots sharing the x-axis:
  1027. the left-paw signal (red stance patches), the right-paw signal (green
  1028. stance patches), and the forward speed with the moving-bout mask
  1029. overlaid.
  1030. """
  1031. from matplotlib import pyplot as plt
  1032. for stride_type, (strides_L, strides_R) in self.stride_objs.items():
  1033. f, (ax1, ax2, ax3) = plt.subplots(3, 1, sharex=True)
  1034. strides_L.plot(
  1035. ax=ax1,
  1036. color_stance="red",
  1037. alpha_swing=0.2,
  1038. )
  1039. strides_R.plot(
  1040. ax=ax2,
  1041. color_stance="green",
  1042. alpha_swing=0.2,
  1043. )
  1044. ax3.plot(self.mouse_fw_speed)
  1045. ax3.plot(self.stride_props.mask_for_strides * self.cfg["MOVING_THRESH"])
  1046. ax1.set_xlim(0, 400)
  1047. ax1.legend()
  1048. ax2.legend()
  1049. def compute_raw_features(self):
  1050. """
  1051. Compute per-stride gait metrics for all defined limb pairs.
  1052. For each stride type in ``cfg["STRIDE_DEF"]`` both the left and
  1053. right paw are processed with ``stride_to_dataframe``, using the
  1054. contralateral paw as the opposite-side reference.
  1055. Returns
  1056. -------
  1057. pd.DataFrame
  1058. Tidy table with one row per stride. Columns include frame
  1059. indices, spatial coordinates, duty factor, stride length,
  1060. speed, angular velocity, step distances, lateral displacement
  1061. metrics, cross-correlation, and body-length normalisation
  1062. inputs. See ``stride_to_dataframe`` for the full column list.
  1063. """
  1064. result_tab = []
  1065. for stride_type, (strides_L, strides_R) in self.stride_objs.items():
  1066. props_L = stride_to_dataframe(
  1067. self.stride_props,
  1068. strides_L,
  1069. strides_R,
  1070. stride_type=stride_type,
  1071. part_for_speed=self.cfg["STRIDE_SPEED_NODE"],
  1072. parts_angular_velocity=self.cfg["STRIDE_ANG_VEL_AXIS"],
  1073. parts_lat_displacement=self.cfg["STRIDE_LAT_DISPL_DEF"],
  1074. part_xcorr=strides_R.paw_part,
  1075. parts_body_size=self.cfg["ANIMAL_LENGTH_DEF"],
  1076. )
  1077. props_R = stride_to_dataframe(
  1078. self.stride_props,
  1079. strides_R,
  1080. strides_L,
  1081. stride_type=stride_type,
  1082. part_for_speed=self.cfg["STRIDE_SPEED_NODE"],
  1083. parts_angular_velocity=self.cfg["STRIDE_ANG_VEL_AXIS"],
  1084. parts_lat_displacement=self.cfg["STRIDE_LAT_DISPL_DEF"],
  1085. part_xcorr=strides_L.paw_part,
  1086. parts_body_size=self.cfg["ANIMAL_LENGTH_DEF"],
  1087. )
  1088. result_tab.extend([props_L, props_R])
  1089. return pd.concat(result_tab, ignore_index=True)
  1090. def compute_features(self):
  1091. """
  1092. Compute raw gait features and add derived summary metrics.
  1093. Extends ``compute_raw_features`` with:
  1094. - **Body-length-normalised columns** — if ``cfg["ANIMAL_LENGTH_DEF"]``
  1095. is set, spatial metrics (stride length, speed, step length/width,
  1096. lateral displacement distances) are divided by the mean body length
  1097. across all strides and stored in ``relative_*`` columns.
  1098. - **Duty-factor temporal symmetry** — for each stride type, the
  1099. symmetry index ``(L - R) / (0.5 * (L + R))`` is computed from
  1100. the mean duty factors of the left and right paw and broadcast to
  1101. every row of that stride type.
  1102. Returns
  1103. -------
  1104. pd.DataFrame
  1105. All columns from ``compute_raw_features`` plus ``speed_mean_cm_s``,
  1106. ``speed_std_cm_s`` and ``time_ratio_moving``
  1107. (whole-session), ``relative_*`` body-length-normalised
  1108. columns, and ``duty_factor_temporal_symmetry``.
  1109. """
  1110. tab = self.compute_raw_features()
  1111. ### avg spped
  1112. inst_speeds = self.mouse.speed(part=self.cfg["STRIDE_SPEED_NODE"])
  1113. tab["speed_mean_cm_s"] = np.nanmean(inst_speeds) * self.speed_calibration_factor
  1114. tab["speed_std_cm_s"] = np.nanstd(inst_speeds) * self.speed_calibration_factor
  1115. ### time moving
  1116. tab["time_ratio_moving"] = self.mouse_is_moving_mask.sum() / len(self.mouse_is_moving_mask)
  1117. ### Body length normalization
  1118. if "stride_body_lengths_cm" in tab.columns:
  1119. body_length = tab.stride_body_lengths_cm.mean()
  1120. cols_to_norm = [
  1121. "stride_length_cm",
  1122. "stride_speed_cm_s",
  1123. "step_length_cm",
  1124. "step_width_cm",
  1125. "stride_body_lengths_cm",
  1126. "speed_mean_cm_s",
  1127. "speed_std_cm_s",
  1128. ]
  1129. for c in tab.columns:
  1130. if c.startswith("lat_displ_dist_"):
  1131. cols_to_norm.append(c)
  1132. for col in cols_to_norm:
  1133. if col in tab.columns:
  1134. tab[f"relative_{col.replace('_cm', '')}"] = tab[col] / body_length
  1135. ### temporal symmetry for duty factor
  1136. stride_types = tab.Stride_type.unique()
  1137. tab["duty_factor_temporal_symmetry"] = np.nan
  1138. for st in stride_types:
  1139. l_dtf, r_dtf = tab.groupby(["Stride_type", "Paw"]).duty_factor.mean()[st]
  1140. temp_symm_per_st = (l_dtf - r_dtf) / (0.5 * (l_dtf + r_dtf))
  1141. tab.loc[tab.Stride_type == st, "duty_factor_temporal_symmetry"] = (
  1142. temp_symm_per_st
  1143. )
  1144. return tab

gait.py at commit 46d106e, under BSD-3-Clause · at the source

Overview

  1. Institute of Science and Technology Austria,Klosterneuburg, Austria
  2. Department of Neuroimmunology, Center for Brain Research, Medical University Vienna,Vienna, Austria
  3. Ludwig Boltzmann Institute for Network Medicine, University of Vienna,Vienna, Austria
  4. Max Perutz Labs, Vienna Biocenter Campus (VBC),Vienna, Austria
  5. Department of Structural and Computational Biology, Center for Molecular Biology, University of Vienna,Vienna, Austria
  6. CeMM Research Center for Molecular Medicine of the Austrian Academy of Sciences,Vienna, Austria
  7. Institute of Artificial Intelligence, Center for Medical Data Science, Medical University of Vienna,Vienna, Austria
  8. Faculty of Mathematics, University of Vienna,Vienna, Austria
  9. Department of Physiology and Pharmacology, Karolinska Institute,Stockholm, Sweden
Journal: Nature, volume 656, issue 8126, pages 159-172
Dates: received 24 July 2024; accepted 18 May 2026; published online 17 June 2026; in print 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1038/s41586-026-10679-1 · PMID 42310454 · PMCID PMC13441911 · OpenAlex W7165022531
Open access: hybrid, a free copy (OpenAlex)
Status: code verified
Categories: genetics / omics (modality), mouse (organism), autism (population), cellular / molecular (subfield)
Methods: Spectral & time-frequency, Connectivity, Statistics, Smoothing, state filtering, decompositions, Machine learning, fMRI & imaging
Keywords: Neuroscience, Molecular biology
MeSH: Autism Spectrum Disorder*, Cerebral Cortex*, Disease Models, Animal*, Animals, Cell Lineage, Female, Gene Expression Regulation, Developmental, Genetic Heterogeneity, Male, Mice, Mutation, Neurodevelopment, Neurons, Sex Characteristics, Synapses (* major topic)
Topic: Autism Spectrum Disorder Research (Cognitive Neuroscience, Neuroscience), according to OpenAlex
Citations: not cited yet (Europe PMC); 112 references in the paper
Research resources: mice with a CMV-Cre line RRID:IMSR_JAX:006054, RRID:IMSR_JAX:032187

Abstract

Despite the functional diversity of over 100 causal genes1–3, phenotypic convergence across models may reveal common neurobiological processes in autism spectrum disorder (ASD). Here we profiled 251 samples from 11 monogenic mouse models of ASD using single-nucleus multi-omic sequencing across three developmental stages, both sexes and two brain regions. Despite genetic heterogeneity, ASD-linked mutations converged on perturbations of the radial glial cell lineage. These alterations reflect a transient developmental delay rather than lasting lineage misspecification and resolve by postnatal stages. Molecularly, the largest transcriptional differences emerged in neurons at early postnatal stages. These changes included downregulation of synaptic and ion channel-related genes, consistent with homeostatic adaptation or delayed maturation. Network analysis showed molecular convergence across models within each developmental stage, suggesting that diverse mutations linked to ASD impinge on common, stage-specific processes. Convergence becomes less pronounced by postnatal day 14, highlighting the dynamic nature of ASD-associated changes. Cross-genotype heterogeneity is superimposed on stage-specific effects. Electrophysiology corroborated this pattern: mutants generally showed altered neuronal excitability and synaptic properties with model-specific nuances. Our study also highlighted sex-specific gene expression alterations, with female mice often displaying larger effect sizes than male mice. Together, our findings provide a comprehensive view of developmental cellular and molecular dynamics across models of ASD.

Reproduced under the paper's license (CC BY), from the paper cited above.

Repositories

Its files are read in the Code ↔ Paper reader above, with 4 matches between paragraphs and lines of code.

modeldb:184162

License: none: the authors keep all their rights
State: the link answers, verified on 27 September 2026
Evidence: the link answers
Software Heritage: not checked
Found in: the text, “Minimal conductance-based model”
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Availability: 1 check, the latest on 27 September 2026: the link answers (HTTP 200)
  • 27 September 2026: the link answers (HTTP 200)
At the source: modeldb.science/184162

git.ista.ac.at/research-sofware/mouseome

License: none: the authors keep all their rights
State: the link answers, verified on 27 September 2026
Evidence: the link answers
Software Heritage: not checked
Found in: “Code availability”
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Availability: 1 check, the latest on 27 September 2026: the link answers (HTTP 200)
  • 27 September 2026: the link answers (HTTP 200)

Zenodo 17532669

License: BSD-3-Clause
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Size: 1 file
Software Heritage: not checked
Found in: the references
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: NumPy (10 files), Matplotlib (8 files), pandas (7 files), SciPy (6 files), scikit-image (5 files), seaborn (4 files), OpenCV (3 files), h5py (2 files), imageio (1 file), scikit-learn (1 file), tifffile (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers (HTTP 200)
  • 27 September 2026: the link answers (HTTP 200)
14 files
At the source:

sommerc/tadpose

License: BSD-3-Clause
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: 46d106ec2a158e06a2d6bbcc06ac2aa151291444, 12 August 2026
Languages: Python (10), Jupyter (2)
Size: 15 files, 12 scripts
Software Heritage: not archived
Found in: the Zenodo archive record
Holds: README, license file, environment (setup.py), 2 notebooks
Not found: CITATION.cff, tests, continuous integration, documentation
Tools: NumPy (10 files), Matplotlib (8 files), pandas (8 files), SciPy (6 files), scikit-image (5 files), seaborn (4 files), OpenCV (3 files), h5py (2 files), imageio (1 file), scikit-learn (1 file), tifffile (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
14 files

Code availability

Scripts and analyses that support the main findings of this study are accessible in a GitHub repository (https://git.ista.ac.at/research-sofware/mouseome).

Reproduced under the paper's license (CC BY), from the paper cited above.

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 4 repositories of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 24 scripts, each with its path and the digest of its content;
  • 4 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

Datasets cited

Data availability

Single-nucleus multiomics data are available from the Gene Expression Omnibus (GSE328363 (https://www.ncbi.nlm.nih.gov/geo/query/acc.cgi?acc=GSE328363)). The mm10 reference genome was used for the alignment (refdata-cellranger-arc-mm10-2020-A-2.0.0, obtained from https://cf.10xgenomics.com/supp/cell-arc/refdata-cellranger-arc-mm10-2020-A-2.0.0.tar.gz). Single-cell data can be accessed and visualized through a CELLxGENE database (https://adameykolab.hifo.meduniwien.ac.at/cellxgene_public/filecrawl/.2026_Nature_Schwarz). Source data are provided with this paper.

Reproduced under the paper's license (CC BY), from the paper cited above.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 2, 28 September 2026

  • Publisher: n/a → Nature Portfolio

Version 1, 27 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 25 authors, 2 keywords, 15 MeSH terms, 109 references, 2 RRIDs.

Cite

This paper

Schwarz, L. A., Dotter, C. P., Isaev, S., Lisi, M., Malzl, D., Büschl, C., Ladstätter, S., Oliveira, B., Barel, M., Basilico, B., Chintaluri, C., Gorkiewicz, S., Goudarzi, M., Belinova, T., Reichl, S., Sendžikaitė, G., Jayaram, S. A., Koppensteiner, P., Sommer, C., . . . Novarino, G. (2026). Cortical development dynamics across autism spectrum disorder mouse models. Nature, 656(8126), 159-172. https://doi.org/10.1038/s41586-026-10679-1

BibTeX

@article{schwarz2026cortical,
author = {Schwarz, Lena A. and Dotter, Christoph P. and Isaev, Sergey and Lisi, Michela and Malzl, Daniel and Büschl, Christoph and Ladstätter, Sabrina and Oliveira, Bárbara and Barel, Matteo and Basilico, Bernadette and Chintaluri, Chaitanya and Gorkiewicz, Sarah and Goudarzi, Mohammad and Belinova, Tereza and Reichl, Stephan and Sendžikaitė, Gintarė and Jayaram, Satish Arcot and Koppensteiner, Peter and Sommer, Christoph and Vogels, Tim P. and Menche, Jörg and Adameyko, Igor and Kharchenko, Peter V. and Bock, Christoph and Novarino, Gaia},
title = {{Cortical development dynamics across autism spectrum disorder mouse models}},
journal = {Nature},
year = {2026},
month = jun,
volume = {656},
number = {8126},
pages = {159--172},
publisher = {Nature Portfolio},
issn = {0028-0836},
doi = {10.1038/s41586-026-10679-1},
url = {https://doi.org/10.1038/s41586-026-10679-1},
pmid = {42310454},
pmcid = {PMC13441911}
}

RIS

TY - JOUR
AU - Schwarz, Lena A.
AU - Dotter, Christoph P.
AU - Isaev, Sergey
AU - Lisi, Michela
AU - Malzl, Daniel
AU - Büschl, Christoph
AU - Ladstätter, Sabrina
AU - Oliveira, Bárbara
AU - Barel, Matteo
AU - Basilico, Bernadette
AU - Chintaluri, Chaitanya
AU - Gorkiewicz, Sarah
AU - Goudarzi, Mohammad
AU - Belinova, Tereza
AU - Reichl, Stephan
AU - Sendžikaitė, Gintarė
AU - Jayaram, Satish Arcot
AU - Koppensteiner, Peter
AU - Sommer, Christoph
AU - Vogels, Tim P.
AU - Menche, Jörg
AU - Adameyko, Igor
AU - Kharchenko, Peter V.
AU - Bock, Christoph
AU - Novarino, Gaia
TI - Cortical development dynamics across autism spectrum disorder mouse models
T2 - Nature
J2 - Nature
PY - 2026
DA - 2026/06/17
VL - 656
IS - 8126
SP - 159
EP - 172
SN - 0028-0836
PB - Nature Portfolio
DO - 10.1038/s41586-026-10679-1
UR - https://doi.org/10.1038/s41586-026-10679-1
LA - en
ER -

CSL-JSON

{
"id": "10.1038/s41586-026-10679-1",
"type": "article-journal",
"title": "Cortical development dynamics across autism spectrum disorder mouse models",
"container-title": "Nature",
"author": [
{
"family": "Schwarz",
"given": "Lena A."
},
{
"family": "Dotter",
"given": "Christoph P."
},
{
"family": "Isaev",
"given": "Sergey"
},
{
"family": "Lisi",
"given": "Michela"
},
{
"family": "Malzl",
"given": "Daniel"
},
{
"family": "Büschl",
"given": "Christoph"
},
{
"family": "Ladstätter",
"given": "Sabrina"
},
{
"family": "Oliveira",
"given": "Bárbara"
},
{
"family": "Barel",
"given": "Matteo"
},
{
"family": "Basilico",
"given": "Bernadette"
},
{
"family": "Chintaluri",
"given": "Chaitanya"
},
{
"family": "Gorkiewicz",
"given": "Sarah"
},
{
"family": "Goudarzi",
"given": "Mohammad"
},
{
"family": "Belinova",
"given": "Tereza"
},
{
"family": "Reichl",
"given": "Stephan"
},
{
"family": "Sendžikaitė",
"given": "Gintarė"
},
{
"family": "Jayaram",
"given": "Satish Arcot"
},
{
"family": "Koppensteiner",
"given": "Peter"
},
{
"family": "Sommer",
"given": "Christoph"
},
{
"family": "Vogels",
"given": "Tim P."
},
{
"family": "Menche",
"given": "Jörg"
},
{
"family": "Adameyko",
"given": "Igor"
},
{
"family": "Kharchenko",
"given": "Peter V."
},
{
"family": "Bock",
"given": "Christoph"
},
{
"family": "Novarino",
"given": "Gaia"
}
],
"container-title-short": "Nature",
"volume": "656",
"issue": "8126",
"page": "159-172",
"DOI": "10.1038/s41586-026-10679-1",
"PMID": "42310454",
"PMCID": "PMC13441911",
"ISSN": "0028-0836",
"publisher": "Nature Portfolio",
"URL": "https://doi.org/10.1038/s41586-026-10679-1",
"language": "en",
"issued": {
"date-parts": [
[
2026,
6,
17
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1038/s41467-026-70375-6 [code]
Ribosomal modifications are associated with mesenchymal fate selection in the neural crest lineage.
Journal: Nature communications
In common: pandas, Matplotlib, NumPy, mouse, 5 references, 2 authors
[2] doi:10.1016/j.xcrm.2026.102766 [code]
A longitudinal single-cell and spatial multiomic atlas of pediatric high-grade glioma.
Journal: Cell reports. Medicine
In common: imageio, tifffile, OpenCV, 8 other tools, genetics / omics, cellular / molecular, 4 references
[3] doi:10.1038/s41467-026-74320-5 [code]
Spatial architecture of autism pathogenesis reveals mosaic structural disarray during early development.
Journal: Nature communications
In common: tifffile, OpenCV, scikit-image, 5 other tools, autism, genetics / omics, 5 references
[4] doi:10.1038/s41467-026-73476-4 [code]
Developmental molecular signatures define de novo cortico-brainstem circuit for skilled forelimb movement.
Journal: Nature communications
In common: tifffile, OpenCV, scikit-image, 3 other tools, mouse, 6 references
[5] doi:10.1016/j.xgen.2026.101278 [code]
Single-cell profiling of DNA methylation in autism spectrum disorder prefrontal cortex reveals distinct regulatory and aging signatures.
Journal: Cell genomics
In common: pandas, NumPy, autism, genetics / omics, 10 references
[6] doi:10.1038/s41586-026-10515-6 [code]
An X-linked long non-coding RNA, PTCHD1-AS, and the core features of autism.
Journal: Nature
In common: pandas, SciPy, Matplotlib, 1 other tool, autism, genetics / omics, mouse, 1 other category, 9 references
[7] doi:10.1038/s41467-026-73373-w [code]
Mapping neuro-vascular unit communications reveals distinct angiogenic programs across developing mouse brain regions.
Journal: Nature communications
In common: imageio, tifffile, OpenCV, 7 other tools, genetics / omics, mouse, 1 reference
[8] doi:10.1038/s41467-026-76956-9 [code]
Innervated human cardiac muscle model reveals sympathetic drivers of KCNH2-associated arrhythmias.
Journal: Nature communications
In common: tifffile, OpenCV, scikit-image, 3 other tools, 7 references
[9] doi:10.1038/s41588-026-02737-1 [code]
Human and mouse adrenal glands are characterized by species-specific steroidogenic states and tissue turnover.
Journal: Nature genetics
In common: pandas, Matplotlib, genetics / omics, mouse, cellular / molecular, 1 reference, 2 authors
[10] doi:10.1186/s11689-026-09713-0 [code]
DRP1 mutations associated with EMPF1 encephalopathy perturb the transcriptional profile and maturation of cortical neurons.
Journal: Journal of neurodevelopmental disorders
In common: tifffile, OpenCV, scikit-image, 5 other tools, 5 references

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.