feat: add compute_polarization() to collective behavior metrics#875
feat: add compute_polarization() to collective behavior metrics#875khan-u wants to merge 18 commits into
Conversation
64435ff to
52d8477
Compare
for more information, see https://pre-commit.ci
…compute_polarization
…tion, edge case handling, and simplified tests
…ts, clarify orientation vs heading terminology
…n for polarization
a04737a to
9313b9d
Compare
Redesigned
|
…ons to resolve potential confusion
for more information, see https://pre-commit.ci
|



Description
What is this PR
Why is this PR needed?
Movement lacks metrics for analyzing collective behavior in multi-individual tracking data. Polarization quantifies group alignment, a foundational measure in collective behavior research used to classify group states such as schooling in fish.
What does this PR do?
Adds
compute_polarization()to thekinematicsmodule. Polarization measures how aligned individuals' direction vectors are, supporting two modes:(tail_base, neck)Output range:
where$\hat{u}_i$ is the unit direction vector for individual $i$ , and $N$ is the number of valid individuals at each time point.
→ Redesigned API – Newer Comment
Usage
Parameters
dataxarray.DataArraytime,space, andindividualsdims. Requiresx,yspace coordinates. Must includekeypointsdim when usingbody_axis_keypoints; for heading polarization, pre-select a keypoint via.sel(keypoints="...")else the first (index 0) will be used.body_axis_keypointstuple[Hashable, Hashable]None(origin, target)keypoint pair defining body axis for orientation polarization. WhenNone, computes heading polarization from displacement.displacement_framesint1body_axis_keypointsis set.return_angleboolFalsein_degreesboolFalseTrue, mean angle is returned in degrees; otherwise radians. Only relevant whenreturn_angle=True.Returns
return_angle=FalseDataArraynamed"polarization", dims("time",), values clipped to [0, 1]return_angle=True(polarization, mean_angle)wheremean_angleis aDataArraynamed"mean_angle", dims("time",)Edge Case Handling
NaNfor that frameNaNNaN(no prior frame for displacement)References
How has this PR been tested?
49 test methods across 5 test classes with comprehensive coverage on
collective.py:TestComputePolarizationValidationTestComputePolarizationBehaviorTestHeadingSourceSelection.sel()selection, z-coord ignored, first-frame validity with angleTestDisplacementFramesTestReturnAnglein_degreesconversionIs this a breaking change?
No.
Does this PR require an update to the documentation?
No — API docs auto-generate from docstrings.
Checklist
Polarization Visualization Demos
Visualization script that produces a multi-row figure showing polarization dynamics over a selected time segment, integrating the AP inference introduced later in PR #945 .
Uses
compute_polarizationacross multiple displacement timescales:Drives a segment scoring pipeline that:
Constructs a composite score by integrating:
Prioritizes segments that:
Resulting visualizations demonstrate that polarization:
A 3-row × N-panel figure (N =
NUM_PANELS, default 5) showing the temporal evolution of collective alignment over a selected segment. Each column corresponds to one frame, spaced 1 second apart (=fpsframes).Row 1: Heading Across Frames (
velocity_node → velocity_node)Each panel overlays two frames using adaptive blending
prev_frames[p]is valid for every panel (including the first)Overlay content:
velocity_nodeRow 2: Orientation Within Frame (
from_node → to_node)Each panel shows the same frame as Row 1 (without ghost overlay), with body-axis annotations.
from_node → to_nodefrom_nodeto_nodeLabeling logic:
If
ap_validated = True:If
ap_validated = False:from_node)to_node)Row 3: Polar Plots
Vectors (origin-centered):
Orientation polarization
p_b(yellow):Heading polarization
p_h(orange):Segment Selection
Sparse vs Continuous
When FPS is known (from user override or video metadata), the script selects the best contiguous segment of
NUM_PANELSframes spaced 1 second apart (frame_interval = fps).If no full segment is found, the script searches for partial continuous segments down to
MIN_CONTINUOUS_PANELS(default 3).If no continuous segment of any valid length is found, it falls back to sparse mode.
In sparse mode, individual frames are selected greedily to maximize variance in polarization, using single-frame displacement for heading.
Frames where the combined polarization is high enough to produce a visible resultant vector on the polar plot (≥
MIN_VISUAL_POL, default 0.15) are prioritized, then lower-polarization frames are added if needed.Coordinate Conventions
dy_cartesian = -(to_y − from_y)for orientation, anddy_cartesian = -(curr_y − prev_y)for heading displacement, converting from image coordinates (y increases downward) to Cartesian (y increases upward).compute_polarizationreturns angles in the input coordinate system (image); the script negates these angles (-theta) when rendering on polar plots.Candidate segments are scored by a composite metric combining:
range × (0.3 + 0.7 × |end−start|/range) + 0.15 × meanorientation_score + heading_score8.0 × mean(|Δθ_orient|/π, |Δθ_heading|/π) × agreement10.0 × mean(heading_pol_window) × mean(min_disp_window) / speed_scalemax(75th percentile of all per-frame min-displacements, 1.0)Together, these scoring components select for segments that best demonstrate the temporal evolution of collective alignment: visible movement, changing group coordination, and concordance between where animals point and where they go.
Visualizations of Other Datasets
2 Mice
4 Gerbils
5 Mice
2 Bees