animetric (development version)
Removed
-
mean_angle()andmedian_angle()are removed. Useanicore::circ_mean()andanicore::circ_median(), which are attached bylibrary(animovement)(animovement/anicore#147). The circular statistics live in one place now, and these were the two that had drifted:mean_angle()duplicatedcirc_mean()exactly, andmedian_angle()was not a circular median at all.median_angle()took the median of the sine and cosine components, which is not rotation-equivariant — rotating every angle in a sample by the same amount moved its answer by a different amount, so the result depended on where the circle was cut.anicore::circ_median()is Fisher’s circular median and does not have that defect, so it is a replacement that returns different numbers. Any stored values computed withmedian_angle()were frame-dependent.
Fixed
The windowed straightness of
add_tortuosity()isNAfor a window that holds a missing position (#108). The distance travelled left out the steps to and from the missing position while the displacement still spanned the gap, so the straightness could exceed 1.calculate_tortuosity(), which is deprecated, keeps its old output.calculate_kinematics()gives correct results for polar, cylindrical and spherical input stored in degrees. It converts such input to Cartesian with anispace’smap_to_*(), which read every angle as radians until animovement/anispace#47, sophi = 90was taken as 90 radians. animetric now requires anispace 0.3.0.9005, which has the fix.headingfromcalculate_kinematics()ispifor movement along-x, where it used to be rewritten to0— the opposite direction (#69). The rewrite fired wheneverv_ywas exactly zero, which integer-pixel tracking produces routinely, so a trajectory along-xpicked up 180-degree jumps that showed as spikes inangular_velocityandangular_accelerationand as turning inangular_path_lengththat never happened.headingis nowNAwhere speed is zero, since a stationary animal has no direction of travel; before, it read as0, or aspiwhen the velocity was a negative zero.angular_path_lengthfromcalculate_kinematics()starts at0(#68). When the first heading was negative it started at2 * abs(heading)and carried that offset along the whole trajectory, so a straight line at a heading of-1reported two radians of turning.summarise_tortuosity()takes the difference between the last and first values, so itstotal_angular_path_lengthwas not affected. Turning made while the animal is stopped (whereheadingisNA) is counted when it moves off again.calculate_kinematics(),calculate_tortuosity(),summarise_kinematics()andsummarise_tortuosity()read the axes and the index from the frame’s declared variables, so they work on frames whose columns are not calledx,y,zandtime(#81). They used to stop with “Columnxnot found”. Velocity and acceleration components are named by axis role (v_x,a_y, …) whatever the input columns are called. 1D frames get translational kinematics and tortuosity too;calculate_kinematics()used to return them unchanged. The 2D and 3D code paths are now one implementation, with turning angles fromanicore::angle_between(). The only change in output is that 3D windowed tortuosity isNAat the first row where the window is incomplete, as it already was in 2D; the old 3D code filled the missing start of the window with the first position.Angular measures are returned in the frame’s declared
unit_angle(#80). A frame declared in degrees used to getheading,angular_velocity,angular_acceleration,angular_path_lengthand the heading summaries in radians, still labelled as degrees, so anything that trusted the metadata was off by a factor of 180/pi.calculate_kinematics(),summarise_kinematics()andsummarise_tortuosity()now report them in degrees for such frames, with no extra step. Radian frames are unchanged. The documentation now also says which way signed angles run: they follow the frame’s own axes, counter-clockwise whenypoints up, as aniread leaves image data.calculate_nnd()keeps the input’s metadata, such assampling_rate, and works on frames whose axis columns have custom names. It used to rebuild its result by re-detecting the columns, which dropped both.
Added
add_kinematics()andsummarise_path()takemin_step, the shortest step whose direction counts (#104). A point that barely moves takes its direction from tracking noise, which swings at random from frame to frame and was all counted as turning. Belowmin_step,courseisNAand the row adds nothing toturning_speed,turning_rate,turning_acceleration,cumulative_turningortotal_turning; a turn made in the meantime is counted once, when the animal moves off. The default,"auto", is three times the positional noise estimated for each trajectory, and at most half its median step.min_step = 0counts every direction, ascalculate_kinematics()did. On the SLEAP sample, cleaned and smoothed as in the Get Started guides, the centre’stotal_turningfalls from about 356,000 to 243,000 degrees.compute_min_step()returns the threshold thatmin_step = "auto"chooses for each trajectory, with the positional noise it is estimated from, in the frame’s spatial unit (#111). It is computed by the same code as inadd_kinematics(), so it can be reported, compared with a known tracking precision, or passed back asmin_step, for example to use one threshold for every trajectory. It is experimental, likemin_step.summarise_path()andadd_tortuosity()computesinuosityande_maxfrom the path rediscretised to a constant step length, as Benhamou (2004) defines sinuosity (#104). Jitter while an animal is still gives no steps, so it no longer dominates them. A window ofadd_tortuosity()in which the animal moved less than a step hasNA.summarise_path()andadd_tortuosity()takestep_length, the step the path is rediscretised at forsinuosityande_max, in the frame’s spatial unit (#114). The default,"auto", is each trajectory’s own step as before; a number rediscretises every trajectory at that step, so that their sinuosity can be compared at one scale. It is experimental.-
add_orientation()declares which way a body faces from where its points are (#97):-
2D:
heading, the direction fromfromtoto. -
3D: a unit quaternion (
qw,qx,qy,qz), with a third point,plane, to fix the roll. Any point off thefrom-toline will do. -
Across the body:
perpendicular = TRUEhandles axes that run across the body, such as right eye to left eye. -
Where it goes:
attach_toputs the orientation on chosen members, so a head and a body orientation can share the one declared column.
Once declared, the orientation is a proper
wherevariable, which anispace’s egocentric transform can align by. Needs anispace 0.3.0.9006 (quat_from_vectors()). -
2D:
Changed
-
add_nnd()replacescalculate_nnd()(#109). It returns the frame with nearest-neighbour columns added, so it joins theadd_*()family.compute_nnd()keeps its name, as it returns values. On the way:calculate_nnd()add_nnd()nnd_<across>(e.g.nnd_individual)nnd_<n>_<across>(e.g.nnd_1_individual)nnd_<variable>(e.g.nnd_keypoint)nnd_<n>_<variable>(e.g.nnd_1_keypoint)nnd_distancennd_<n>_distance- Every column carries the neighbour rank
n,1included, so the nearest and second-nearest neighbours sit side by side:add_nnd(n = 1) |> add_nnd(n = 2). It used to stop with “Names must be unique”. The names split unambiguously with^nnd_(\d+)_(.+)$, even when the column after the rank has underscores in it. Calling it again with the samensays which columns are already there. -
nmust be a single whole number of 1 or more. - The deprecated
keypoint_neighbourargument is not carried over; useneighbour = list(keypoint = ...).
- Every column carries the neighbour rank
-
add_kinematics()andadd_tortuosity()replacecalculate_kinematics()andcalculate_tortuosity(), with the same arguments (#103). Functions that return the frame with something added now all start withadd_. Some columns are renamed on the way:Function Old New add_kinematics()path_lengthcumulative_distanceadd_tortuosity()straightnessstraightness_11add_tortuosity()sinuositysinuosity_11add_tortuosity()emaxe_max_11-
cumulative_distanceis the running total of distance travelled, and pairs withcumulative_turning.accelerationkeeps its name, and its documentation now says what it is: the signed rate of change of speed along the path, not the size of the acceleration vector (a_x,a_y). -
add_tortuosity()names its columns with the window width,11being the default, and adds only those three.calculate_tortuosity()also added every kinematic column when the frame had none. The width in the name keeps the windowed measures apart from the whole-path ones ofsummarise_path(), and lets several widths sit side by side. -
summarise_aniframe()summarises the windowed measures at every width present (median_straightness_11, …), as well as the unsuffixed columns ofcalculate_tortuosity().
-
summarise_aniframe(),add_orientation()and theverticalargument ofcalculate_kinematics()are marked experimental (animovement/.github#46). They are new designs that have not been used in anger yet, so they may still change without a deprecation cycle; anything without a badge is stable, and changes only through one.summarise_aniframe()has open design questions (#91, #92, #22), the first uses ofadd_orientation()are still being designed (#25, #85), andverticalmay come to default to a vertical declared in the frame’s metadata (animovement/anicore#172).-
The summaries are reorganised into two functions, by what they summarise (#58). Sliding windows stay in
add_tortuosity(); both summaries cover each group’s whole time range.-
summarise_aniframe()summarises the distribution of per-row measures, with one row per group and any grouping allowed. It is now an S3 generic:-
anipoints: speed, acceleration, the turning measures, course and elevation, the windowed measures of
add_tortuosity(),confidence, and a declaredyawas*_heading. -
anisegments:
length. -
anijoints:
angle.
course,yaw, joint angles) get circular statistics, reported in the frame’sunit_angle.cols =picks the measures. It no longer needsadd_kinematics()to have been run: it summarises whichever measures the frame has. -
anipoints: speed, acceleration, the turning measures, course and elevation, the windowed measures of
-
summarise_path()measures each trajectory as a whole:total_distance,total_turning,net_displacement,straightness,sinuosityande_max. It works on any anipoint in any coordinate system, computing what it needs from the positions, and needs one trajectory per group.
median_straightness_11fromsummarise_aniframe()is the typical straightness over windows;straightnessfromsummarise_path()is how direct the whole route was. -
-
Deprecated:
-
summarise_kinematics()→summarise_aniframe(). -
summarise_tortuosity()→summarise_path(), which returns the same measures, withtotal_path_lengthandemaxnamedtotal_distanceande_max. -
summarise_aniframe(type = )keeps its old combined output, with a warning.
Each deprecated call returns exactly what it did before. They will be removed after the next release.
-
-
add_point()andcompute_point()replaceadd_centroid()andcompute_centroid()(#94). They derive a new member of an identity level at each moment, as before, andmethodnow chooses how:-
"centroid", the mean (the default, and what the old functions did); -
"median", robust to a stray keypoint; -
"weighted", weighted byconfidence; - a function of your own, e.g.
\(x) mean(x, trim = 0.1).
A declared orientation is derived for the new member too: the circular mean of
yaw, or the mean quaternion in 3D. It used to be leftNA.add_centroid()andcompute_centroid()are deprecated, and return exactly what they did. -
The
aniframe_kinclass is retired (#58).calculate_kinematics()returns a plain anipoint. The class only labelled a frame as having been throughcalculate_kinematics(), recorded nothing about its columns, and survivedselect(-speed).is_aniframe_kin()is deprecated; check for the columns you need instead. Declaring derived columns in anicore’s metadata is the planned replacement (animovement/anicore#174).calculate_kinematics()gives turning measures for 3D data too (#63). Every 3D frame getsturning_speed(how fast the direction of travel turns, in any direction) andcumulative_turning, andcalculate_kinematics(data, vertical = "z")addscourseandcourse_unwrappedin the horizontal plane,course_elevation(the angle of travel above it), and the signed horizontalturning_rateandturning_acceleration. The newverticalargument names the axis that points up in the world, with a minus sign when it points down ("-y"); the frame’saxis_directionscannot supply it, since they are relative to the camera (animovement/anicore#172 proposes declaring it). Course counts about the vertical by the right-hand rule. The 3D turning speed is the angle between the velocities either side of each row over the time between them, so it has no wrap at +/-pi and no singularity when travel is vertical; 2D results are unchanged.summarise_kinematics()andsummarise_tortuosity()summarise the new columns, andtotal_turningis now reported for 3D.-
Breaking: the velocity-derived angular columns are renamed to say they describe the path, not the body (#70). Course is the direction of travel and turning rate is how fast it changes, while heading and angular velocity are where the animal faces and how fast that turns. They differ for any animal that does not move nose-first, and the orientation names are kept free for body orientation once anicore records it (animovement/anicore#46).
Old New headingcourseheading_unwrappedcourse_unwrappedangular_velocityturning_rateangular_speedturning_speedangular_accelerationturning_accelerationangular_path_lengthcumulative_turningThe summaries follow:
median_heading,mad_heading,mean_headingandsd_headingbecome*_course,*_angular_speed,*_angular_velocityand*_angular_accelerationbecome*_turning_speed,*_turning_rateand*_turning_acceleration, andsummarise_tortuosity()’stotal_angular_path_lengthbecomestotal_turning. Values are unchanged. data.table is now a hard dependency (#27).
calculate_tortuosity()cannot work without it, so the first call on a fresh install used to stop and offer to install it.Works with anicore’s
anipointclass and rebuilt accessor API (animovement/anicore#154).calculate_kinematics()returns a frame of classc("aniframe_kin", "anipoint", "aniframe", ...).The circular summaries in
summarise_kinematics()come from anicore, and thecircularpackage is no longer needed at all (animovement/anicore#147). It was a soft dependency behind acheck_installed()prompt, so the first call tosummarise_kinematics()on a fresh install used to stop and ask to install a package — for two columns of the summary table.mean_headingis reported in[0, 2*pi), likemedian_headingalready was. It previously came back in(-pi, pi], so the two summaries of the same column disagreed about where the circle starts; near+/-pithat showed up as a mean of-3.13beside a median of3.15. Both now use the rangeanicore::wrap_angle()gives by default. The direction is unchanged — only how it is written down.
Deprecated
-
calculate_nnd()→add_nnd()(#109).Each returns exactly what it did, with the old column names (
path_length;straightness,sinuosityandemax;nnd_<across>andnnd_distance),calculate_tortuosity()still adds the kinematic columns, andcalculate_nnd()still takeskeypoint_neighbour. They will be removed after the next release.
Fixed
-
median_headingcould be 180 degrees wrong, and is now correct (animovement/anicore#147). Where two directions tie for the circular median, the old implementation averaged them arithmetically — it read the tied pair out of an undocumented attribute ofcircular::median.circular()’s return value and calledmean()on it. When the tie straddles zero, the arithmetic mean of the two is their antipode. Headings tied at 0.1 and 5.8 radians gave 2.95 radians, or 169 degrees, where the answer is 349 degrees:# the two tied directions, and what each way of averaging them gives # arithmetic: (0.1 + 5.8) / 2 = 2.95 -> 169 degrees, the antipode # circular: anicore::circ_median() -> 349 degreesNothing signalled it: no
NA, no warning, and a plausible direction. Heading distributions straddle zero routinely, so this was not a corner case.anicore::circ_median()averages tied directions on the circle, and a grid search over the definition — the direction minimising the summed angular distance — confirms which of the two is the median. Any storedmedian_headingmay need recomputing. sd_headingis0rather thanNaNwhen the heading never changes.circular::sd.circular()returnsNaNthere, because the resultant length of a constant sample can land above 1 in floating point; anicore’scirc_sd()clamps it. A keypoint that does not move produces exactly this (animovement/anicore#147).
animetric 0.5.0 (2026-08-28)
Changed
The minimum
anicoreis 0.8.0, the first version published under that name — the dependency was renamed without a version constraint, so nothing recorded that a pre-renameaniframewill not do.The core data structures come from
anicore, which is what theaniframepackage was renamed to in its 0.8.0 (animovement/anicore#84). Theaniframeclass keeps its name; only the package providing it changed.The
calculate_kinematics(),calculate_nnd()andcalculate_tortuosity()examples run rather than sitting in\dontrun{}. Each builds its own frame withanicore::example_aniframe(); they were wrapped because they referred to an undefineddata, so they had never been checked against the functions they document.-
calculate_nnd()reads the columns it needs from the aniframe’s metadata, and takes the identity roles explicitly (#37). It previously hard-codedindividual,c("session", "trial", "time"),keypointandx/y/z, consulting the metadata for none of them.That was already producing wrong numbers:
observationjoinedvariables_whenin aniframe 0.6.0, but the hard-coded context list never picked it up, so multi-clip data was pooled and each animal could be matched to one in a different clip — silently, and with a plausible-looking distance.acrossnames the column whose value must differ (required — nothing is inferred),withinnames columns that must match on top of the temporal context, andfocal/neighbourrestrict which points are measured from and to. Being independent, they express asymmetric questions:# whose neck is my head nearest to? data |> calculate_nnd( across = "individual", focal = list(keypoint = "head"), neighbour = list(keypoint = "neck") ) # nearest keypoint within each animal data |> calculate_nnd(across = "keypoint", within = "individual")Existing calls need
across = "individual"added.keypoint_neighbourstill works, with a deprecation warning, and maps toneighbour = list(keypoint = ...). Frames identified by
trackorsubjectrather thanindividualnow work, as do multi-observation frames. Polar, cylindrical and spherical frames error with a pointer toanispace::map_to_cartesian()rather than silently measuring in mixed units.compute_nnd()takesacross,is_focal,is_candidateandlabelsin place ofindividual,keypointandkeypoint_neighbour, mirroring the generalisation above. Its result names the ranked column (nnd_across) whichcalculate_nnd()renames.-
summarise_keypoints()is renamedadd_centroid(), and takesacrossto choose the level it collapses (#47). The old name is gone rather than deprecated — it has not been in a release, so nothing can be depending on it. The old name saidsummarise_, which in this package means collapsing a frame to summary rows — this appends them. It also named the keypoint level, which is only one of the levels a frame can be summarised across.acrossnames the identity variables to collapse, so the same question can be asked at any scale. On pose data for a team:add_centroid(af, across = "keypoint") # each player's own centre add_centroid(af, across = "individual") # one centre per keypoint, across players add_centroid(af, across = c("individual", "keypoint")) # the point the whole team occupiesIt is not guessed.
variables_whatis documented coarse to fine, but nothing enforces that and attributes likesexortreatmentdo not nest at all, so a frame declaring more than one identity variable has to be told which to collapse (animovement/anicore#140, animovement/anicore#141). A frame declaring one is unambiguous and needs no argument.A collapsed level that did not actually vary keeps its value rather than taking the summary’s name — an individual’s strain is still its strain, since nothing was averaged over it.
Only identity variables can be collapsed. Collapsing the index or a temporal variable averages over time, which is what the
summarise_*()family does. compute_centroid()’sinclude_keypoints,exclude_keypointsandcentroid_nameare renamedinclude,excludeandname, and it takes the sameacross(#47).add_areais gone from the summary function: it was never implemented, and area is a different shape of answer that will arrive as its own function rather than a flag.
Fixed
-
calculate_kinematics(),calculate_tortuosity()andsummarise_tortuosity()refuse a frame whose grouping pools several trajectories, rather than answering wrongly (#54). Speed and path length come from successive rows within a group, so a group has to hold one position per moment. Regrouping an aniframe coarsely — every keypoint of an animal together, say — made the distance between keypoints count as movement:# two keypoints 100 apart, each drifting 1 per frame calculate_kinematics(af) # mean speed 1, correct calculate_kinematics(regrouped_by_individual) # mean speed 2.7, silentlyPath length accumulates the same way, and
summarise_tortuosity()takes its last value minus its first — across concatenated trajectories that is a number describing nothing, and it was inflating totals about threefold.Regrouping itself is still allowed;
anicorealready warns that a frame’s grouping and its declaration then disagree. This is narrower and firmer: these computations have a precondition, and the error says how to summarise more coarsely — summarise at the declared grouping first, then combine those results. compute_centroid()carries the source frame’s metadata into its result (#38, #47). Sampling rate, units and the rest were dropped, so a centroid arrived claiming to know nothing about the recording it came from.A centroid no longer gains a
confidencecolumn on frames that do not track one (#47).
Removed
- The re-exports of
as_aniframe(),is_aniframe(),ensure_is_aniframe(),deg_to_rad(),rad_to_deg(),wrap_angle(),unwrap_angle(),calculate_angular_difference()anddiff_angle(). Calls to these throughanimetric::need repointing atanicore::oranispace::. animetric still uses them internally — it just has no reason to publish another package’s interface as its own, which left the same function documented in two places and animetric’s exports growing whenever anicore’s did.
animetric 0.4.0 (2026-08-18)
Changed
- Removed the
aniframe_kin2dandaniframe_kin3dclasses.calculate_kinematics()set them, but nothing ever read them — no predicate, no method, no test in any package — and the dimensionality they encoded already lives in thecoordinate_systemmetadata field. Kinematics output still carriesaniframe_kin, which is what thesummarise_*()functions dispatch on.
Fixed
-
calculate_nnd()checks that theindividualandkeypointcolumns exist before reading them. An absent column was reported as an all-NAone, and every call warned on the way. Surfaced by aniframe 0.7.0, which no longer adds akeypointcolumn beside an existing identity.
animetric 0.3.1
Added
-
summarise_aniframe()andsummarise_tortuosity(), alongsidesummarize_*()spellings forsummarise_aniframe(),summarise_kinematics()andsummarise_tortuosity().
Fixed
-
calculate_tortuosity()preserves the classes of the frame it was given. -
calculate_nnd()keeps the incoming classes rather than returning a plain frame.
animetric 0.3.0 (2025-12-04)
The calculate_*() and summarise_*() families are reworked, tortuosity metrics arrive, and with calculate_nnd() the package gains its first social metric.
Added
-
calculate_nnd()andcompute_nnd()compute nearest-neighbour distance through a time series — the first collective metric in the package.
Removed
-
calculate_kinematics_2d(),calculate_kinematics_3d(),calculate_tortuosity_2d()andcalculate_tortuosity_3d().calculate_kinematics()andcalculate_tortuosity()branch on dimensionality themselves, from the frame’scoordinate_system.
animetric 0.2.0
The package takes its present shape: kinematics, path complexity, angles and summaries.
Added
- Kinematics:
calculate_kinematics(), withcalculate_kinematics_2d()and_3d()behind it, anddifferentiate(). - Path complexity:
calculate_tortuosity()with_2d()and_3d()variants,compute_sinuosity(),compute_straightness()andcompute_emax(). - Spatial summaries:
compute_centroid(). - Circular statistics:
mean_angle()andmedian_angle(). - Summaries:
summarise_kinematics()andsummarise_keypoints(). -
is_aniframe_kin()to test whether a frame carries kinematics. - Angle helpers re-exported from aniframe:
deg_to_rad(),rad_to_deg(),wrap_angle(),unwrap_angle(),diff_angle()andcalculate_angular_difference().