Source code for movement.kinematics.kinetic_energy

"""Functions for computing kinetic energy."""

import numpy as np
import xarray as xr

from movement.kinematics.kinematics import compute_velocity
from movement.utils.logging import logger
from movement.utils.vector import compute_norm
from movement.validators.arrays import validate_dims_coords


[docs] def compute_kinetic_energy( position: xr.DataArray, keypoints: list | None = None, masses: dict | None = None, decompose: bool = False, ) -> xr.DataArray: r"""Compute kinetic energy per individual. We consider each individual's set of keypoints (pose) as a classical system of particles in physics (see Notes). Parameters ---------- position The input data containing position information, with ``time``, ``space`` and ``keypoints`` as required dimensions. keypoints A list of keypoint names to include in the computation. By default, all are used. masses A dictionary mapping keypoint names to masses, e.g. {"snout": 1.2, "tail": 0.8}. By default, unit mass is assumed for all keypoints. decompose If True, the kinetic energy is decomposed into "translational" and "internal" components (see Notes). This requires at least two keypoints per individual, but more would be desirable for a meaningful decomposition. The default is False, meaning the total kinetic energy is returned. Returns ------- xarray.DataArray A data array containing the kinetic energy per individual, for every time point. Note that the output array lacks ``space`` and ``keypoints`` dimensions. If ``decompose=True`` an extra ``energy`` dimension is added, with coordinates ``translational`` and ``internal``. Notes ----- Considering a given individual at time point :math:`t` as a system of keypoint particles, its total kinetic energy :math:`T_{total}` is given by: .. math:: T_{total} = \sum_{i} \frac{1}{2} m_i \| \mathbf{v}_i(t) \|^2 where :math:`m_i` is the mass of the :math:`i`-th keypoint and :math:`\mathbf{v}_i(t)` is its velocity at time :math:`t`. From Samuel König's second theorem, we can decompose :math:`T_{total}` into: - Translational kinetic energy: the kinetic energy of the individual's total mass :math:`M` moving with the centre of mass velocity; - Internal kinetic energy: the kinetic energy of the keypoints moving relative to the individual's centre of mass. We compute translational kinetic energy :math:`T_{trans}` as follows: .. math:: T_{trans} = \frac{1}{2} M \| \mathbf{v}_{cm}(t) \|^2 where :math:`M = \sum_{i} m_i` is the total mass of the individual and :math:`\mathbf{v}_{cm}(t) = \frac{1}{M} \sum_{i} m_i \mathbf{v}_i(t)` is the velocity of the centre of mass at time :math:`t` (computed as the weighted mean of keypoint velocities). Internal kinetic energy :math:`T_{int}` is derived as the difference between the total and translational components: .. math:: T_{int} = T_{total} - T_{trans} Examples -------- >>> from movement.kinematics import compute_kinetic_energy >>> import numpy as np >>> import xarray as xr Compute total kinetic energy: >>> position = xr.DataArray( ... np.random.rand(3, 2, 4, 2), ... coords={ ... "time": np.arange(3), ... "individuals": ["id0", "id1"], ... "keypoints": ["snout", "spine", "tail_base", "tail_tip"], ... "space": ["x", "y"], ... }, ... dims=["time", "individuals", "keypoints", "space"], ... ) >>> kinetic_energy_total = compute_kinetic_energy(position) >>> kinetic_energy_total <xarray.DataArray (time: 3, individuals: 2)> Size: 48B 0.6579 0.7394 0.1304 0.05152 0.2436 0.5719 Coordinates: * time (time) int64 24B 0 1 2 * individuals (individuals) <U3 24B 'id0' 'id1' Compute kinetic energy decomposed into translational and internal components: >>> kinetic_energy = compute_kinetic_energy(position, decompose=True) >>> kinetic_energy <xarray.DataArray (time: 3, individuals: 2, energy: 2)> Size: 96B 0.0172 1.318 0.02069 0.6498 0.02933 ... 0.1716 0.07829 0.7942 0.06901 0.857 Coordinates: * time (time) int64 24B 0 1 2 * individuals (individuals) <U3 24B 'id0' 'id1' * energy (energy) <U13 104B 'translational' 'internal' Select the 'internal' component: >>> kinetic_energy_internal = kinetic_energy.sel(energy="internal") Use unequal keypoint masses and exclude an unreliable keypoint (e.g. "tail_tip"): >>> masses = {"snout": 1.2, "spine": 0.8, "tail_base": 1.0} >>> kinetic_energy = compute_kinetic_energy( ... position, ... keypoints=["snout", "spine", "tail_base"], ... masses=masses, ... decompose=True, ... ) """ # Validate required dimensions and coordinate labels validate_dims_coords( position, {"time": [], "space": ["x", "y"], "keypoints": []} ) # Subset keypoints if requested if keypoints is not None: position = position.sel(keypoints=keypoints) # Validate that at least 2 keypoints exist for decomposition if decompose and position.sizes["keypoints"] < 2: raise logger.error( ValueError( "At least 2 keypoints are required to decompose " "kinetic energy into translational and internal components." ) ) # Compute velocity from position velocity = compute_velocity(position) # Initialise unit weights weights = xr.DataArray( np.ones(position.sizes["keypoints"]), dims=["keypoints"], coords={"keypoints": position.coords["keypoints"]}, ) # Update weights with keypoint masses, if provided if masses: for keypoint, mass in masses.items(): weights.loc[keypoint] = mass # Compute total KE weighted_ke = 0.5 * weights * (compute_norm(velocity) ** 2) ke_total = weighted_ke.sum(dim="keypoints") if not decompose: return ke_total else: # Compute translational KE based on centre of mass velocity v_cm = (velocity * weights.expand_dims(space=["x", "y"])).sum( dim="keypoints" ) / weights.sum() ke_trans = 0.5 * weights.sum() * compute_norm(v_cm) ** 2 # Internal KE ke_int = ke_total - ke_trans # Format output ke = xr.concat([ke_trans, ke_int], dim="energy") ke = ke.assign_coords(energy=["translational", "internal"]) ke = ke.transpose("time", ..., "energy") return ke