From 78b156a2240a03418816ad2966f8f686bd1f6f05 Mon Sep 17 00:00:00 2001 From: BadPrograms Date: Tue, 5 May 2026 23:49:23 +0200 Subject: [PATCH 1/6] initial --- src/lineagetree/_mixins/spatial_mixin.py | 38 +++ src/lineagetree/measure/spatial.py | 288 ++++++++++++++++++++++- tests/test_lineageTree.py | 55 +++++ 3 files changed, 380 insertions(+), 1 deletion(-) diff --git a/src/lineagetree/_mixins/spatial_mixin.py b/src/lineagetree/_mixins/spatial_mixin.py index e2d581c..8eee981 100644 --- a/src/lineagetree/_mixins/spatial_mixin.py +++ b/src/lineagetree/_mixins/spatial_mixin.py @@ -5,6 +5,19 @@ get_gabriel_graph, get_idx3d, compute_neighbours_in_radius, + get_angles, + get_asphericity, + get_displacement, + get_displacement_ratio, + get_duration, + get_max_displacement, + get_mean_squared_displacement, + get_outreach_ratio, + get_overall_angle, + get_speed, + get_straightness, + get_track_length, + get_velocity, ) from ._methodize import AutoMethodizeMeta @@ -19,3 +32,28 @@ class SpatialMixin(metaclass=AutoMethodizeMeta): compute_spatial_edges = compute_spatial_edges compute_spatial_density = compute_spatial_density compute_neighbours_in_radius = compute_neighbours_in_radius + get_angles = get_angles + + get_asphericity = get_asphericity + + get_displacement = get_displacement + + get_displacement_ratio = get_displacement_ratio + + get_duration = get_duration + + get_max_displacement = get_max_displacement + + get_mean_squared_displacement = get_mean_squared_displacement + + get_outreach_ratio = get_outreach_ratio + + get_overall_angle = get_overall_angle + + get_speed = get_speed + + get_straightness = get_straightness + + get_track_length = get_track_length + + get_velocity = get_velocity diff --git a/src/lineagetree/measure/spatial.py b/src/lineagetree/measure/spatial.py index c7f4d8b..2483eaa 100644 --- a/src/lineagetree/measure/spatial.py +++ b/src/lineagetree/measure/spatial.py @@ -1,11 +1,11 @@ from __future__ import annotations -from warnings import warn, catch_warnings, simplefilter from itertools import combinations from typing import TYPE_CHECKING, Iterable import numpy as np from scipy.spatial import Delaunay, KDTree +from .._core._modifier import anchored_gaussian_smooth if TYPE_CHECKING: from ..lineage_tree import LineageTree @@ -277,3 +277,289 @@ def compute_spatial_edges( out = dict(zip(nodes, [set(nodes[ni]) for ni in neighbs], strict=True)) lT.th_edges.update({k: v.difference([k]) for k, v in out.items()}) return lT.th_edges + + +def _track_length(chain: np.ndarray, sigma: int = 1.5) -> float: + """Computes the anchored gaussian smooth of a chain and returns the distance travelled for a given chain. + + Parameters + ---------- + chain : np.ndarray + 3D numpy array with the positions of all nodes in a chain. + sigma : int + Standard deviation of the Gaussian kernel used for smoothing. + Higher values produce stronger smoothing. Default is 1.5. + Returns + ------- + float + The sum of all the distances from the start to the end of the chain, after the data has been smoothed or not. + """ + + chain = np.array(chain) + smoothed_chain = np.zeros_like(chain, dtype=float) + if sigma != 0: + smoothed_chain[:, 0] = anchored_gaussian_smooth(chain[:, 0], sigma) + smoothed_chain[:, 1] = anchored_gaussian_smooth(chain[:, 1], sigma) + smoothed_chain[:, 2] = anchored_gaussian_smooth(chain[:, 2], sigma) + else: + smoothed_chain = chain # Not smoothed + distance_travelled = np.linalg.norm( + smoothed_chain[:-1] - smoothed_chain[1:], axis=0 + ) + return sum(distance_travelled) + + +def get_track_length(lT: LineageTree, sigma: float = 1.5) -> dict[int, float]: + """Returns the distance travelled for each chain. + + Parameters + ---------- + lT : LineageTree + The LineageTree object. + sigma : float, optional + Standard deviation of the Gaussian kernel used for smoothing. + Higher values produce stronger smoothing, by default is 1.5. + + Returns + ------- + dict[int, float] + The distance travelled for each chain assigned to all of its nodes. + """ + track_length = {} + for track in lT.all_chains: + data = np.array([lT.pos[c] for c in track]) + dist = _track_length(data, sigma) + track_length.update({node: dist for node in track}) + lT.track_length = track_length + return lT.track_length + + +def get_duration(lT: LineageTree) -> dict[int, float]: + """The duration of each chain. + + Parameters + ---------- + lT : LineageTree + The LineageTree object. + + Returns + ------- + dict[int,float] + The duration of each chain assigned to all of its nodes. + """ + lT.duration = { + node: len(lT.get_chain_of_node(node)) * lT.time_resolution + for node in lT.nodes + } + return lT.duration + + +def get_max_displacement(lT: LineageTree): + lT.max_displacement = {} + for chain in lT.all_chains: + root = lT.get_ancestor_at_t(chain[0]) + if root not in lT.nodes: + continue + root_pos = lT.pos[root] + positions = np.array([lT.pos[c] for c in chain]) + displacements = np.cumsum(np.linalg.norm((positions - root_pos), axis=1)) + lT.max_displacement.update({node:disp for node, disp in zip(chain,displacements)}) + return lT.max_displacement + + +def get_speed(lT: LineageTree, sigma: int) -> dict[int, float]: + + track_length = get_track_length(lT, sigma) + lT.speed = { + node: dist / lT.time_resolution for node, dist in track_length.items() + } + return lT.speed + + +def get_displacement(lT: LineageTree): + displacement = {} + for chain in lT.all_chains: + displacement.update( + { + node: np.linalg.norm(lT.pos[chain[0]] - lT.pos[chain[-1]]) + for node in chain + } + ) + lT.displacement = displacement + return lT.displacement + + +def get_velocity(lT: LineageTree): + disp = get_displacement(lT) + lT.velocity = { + node: dist / lT.time_resolution for node, dist in disp.items() + } + return lT.velocity + + +def get_mean_squared_displacement( + lT: LineageTree, +): + lT.msd = {} + for chain in lT.all_chains: + positions = np.array([lT.pos[c] for c in chain]) + MSD = np.cumsum( + np.linalg.norm((positions - positions[0]) ** 2, axis=1) + ) / np.arange(1, len(chain) + 1) + lT.msd.update({node: msd for node, msd in zip(chain, MSD)}) + return lT.msd + + +def get_displacement_ratio(lT: LineageTree, sigma: float = 1.5): + disp = get_displacement(lT) + max_disp = get_max_displacement(lT) + lT.displacement_ratio = { + node: disp[node] / max_disp[node] for node in disp if max_disp[node] + } + return lT.displacement_ratio + + +def get_outreach_ratio(lT: LineageTree, sigma: float = 1.5): + max_disp = get_max_displacement(lT) + track_length = get_track_length(lT, sigma) + lT.displacement_ratio = { + node: max_disp[node] / track_length[node] for node in track_length if track_length[node] + } + return lT.displacement_ratio + + +def get_straightness(lT: LineageTree, sigma: float = 1.5): + displacement = get_displacement(lT) + track_length = get_track_length(lT, sigma) + lT.straightness = { + node: displacement[node] / track_length[node] for node in displacement + } + return lT.straightness + + +def _inertia_matrix(chain, sigma: float = 1.5): + """Calculates the inertia of a given point cloud. + + Parameters + ---------- + lT : LineageTree + The lineageTree object + sigma : float, optional + The smoothing factor, by default 1.5 + """ + smoothed_chain = np.zeros_like(chain, dtype = float) + if sigma != 0: + smoothed_chain[:, 0] = anchored_gaussian_smooth(chain[:, 0], sigma) + smoothed_chain[:, 1] = anchored_gaussian_smooth(chain[:, 1], sigma) + smoothed_chain[:, 2] = anchored_gaussian_smooth(chain[:, 2], sigma) + else: + smoothed_chain = chain # Not smoothed + cloud = smoothed_chain - np.mean(smoothed_chain, axis=0) # Centering + Ixx = np.sum(cloud[:, 1] ** 2 + cloud[:, 2] ** 2) + Iyy = np.sum(cloud[:, 2] ** 2 + cloud[:, 0] ** 2) + Izz = np.sum(cloud[:, 0] ** 2 + cloud[:, 1] ** 2) + + Ixy = -np.sum(cloud[:, 0]* cloud[:, 1]) + Ixz = -np.sum(cloud[:, 0]* cloud[:, 2]) + Iyz = -np.sum(cloud[:, 1]* cloud[:, 2]) + + return np.array([[Ixx, Ixy, Ixz], [Ixy, Iyy, Iyz], [Ixz, Iyz, Izz]]) + + +def get_asphericity(lT: LineageTree, sigma: float = 1.5): + """Calculate the asphericity of a track + Adapted from : J Rudnick and G Gaspari 1986 J. Phys. A: Math. Gen. 19 L191 + + Parameters + ---------- + lT : LineageTree + The LineageTree object + sigma : _type_ + Smoothing factor. + """ + lT.asphericity = {} + + for chain in lT.all_chains: + chain_pos = np.array([lT.pos[c] for c in chain]) + if len(chain)<4: + continue + else: + inertia = _inertia_matrix(chain_pos, sigma) + eig_vals = np.linalg.eigvals(inertia) + tr = eig_vals[0] ** 2 + eig_vals[1] ** 2 + eig_vals[2] ** 2 + M = ( + eig_vals[0] ** 2 * eig_vals[1] ** 2 + + eig_vals[1] ** 2 * eig_vals[2] ** 2 + + eig_vals[0] ** 2 * eig_vals[2] ** 2 + ) + asphericity = (tr**2 - 3 * M) / (tr**2) + lT.asphericity.update({node: asphericity for node in chain}) + return lT.asphericity + + +def get_angles(lT: LineageTree, sigma: float = 1.5): + lT.angles = {} + for chain in lT.all_chains: + if len(chain)<3: + continue + else: + positions = np.array([lT.pos[c] for c in chain]) + smoothed_positions = np.zeros_like(positions, dtype=float) + if sigma != 0: + smoothed_positions[:, 0] = anchored_gaussian_smooth( + positions[:, 0], sigma + ) + smoothed_positions[:, 1] = anchored_gaussian_smooth( + positions[:, 1], sigma + ) + smoothed_positions[:, 2] = anchored_gaussian_smooth( + positions[:, 2], sigma + ) + else: + smoothed_positions = positions # Not smoothed + + vectors1 = smoothed_positions[:-1] - smoothed_positions[1:] + vectors2 = vectors1[1:] + angles = [ + (v1 @ v2) / (np.linalg.norm(v1) * np.linalg.norm(v2)) + for v1, v2 in zip(vectors1, vectors2) + ] + angles = [0] + angles + [0] # first and last node have no angle + lT.angles.update({node: angle for node, angle in zip(chain, angles)}) + return lT.angles + + +def get_overall_angle(lT: LineageTree): + lT.overall_angles = {} + for chain in lT.all_chains: + if len(chain)<3: + continue + else: + vector1 = lT.pos[chain[1]] - lT.pos[chain[0]] + vector2 = lT.pos[chain[-1]] - lT.pos[chain[-2]] + overangle = (vector1 @ vector2) / ( + np.linalg.norm(vector1) * np.linalg.norm(vector2) + ) + lT.overall_angles.update({node: overangle for node in chain}) + return lT.overall_angles + + +# TODO + +# * **trackLength**: sums up the distances between subsequent positions; in other words, it estimates the length of the underlying subtrack by linear interpolation (usually an underestimation) +# * **duration**: time elapsed between first and last positions of the subtrack +# * **maxDisplacement**: maximal Euclidean distance of any position on the subtrack from the first position +# * **speed**: trackLength/duration +# * **displacement**: Euclidean distance between the track starting and endpoints +# * **squareDisplacement**: squared Euclidean distance between the track starting and endpoints +# * **displacementRatio**: displacement/maxDisplacement (values between 0 and 1, where 1 means a perfectly straight track) +# * **outreachRatio**: maxDisplacement/trackLength (values between 0 and 1, where 1 means a perfectly straight track) +# * **straightness**: displacement/trackLength (values between 0 and 1, where 1 means a perfectly straight track) +# * **asphericity**: a different appraoch to measure straightness, that computes the asphericity of the set of positions on the subtrack _via_ the length of its principal components (number between 0 and 1, with higher values indicating straighter tracks). Unlike straightness, however, asphericity ignores back-and-forth motion of the object, so something that bounces between two positions will have low straightness but high asphericity. We define the asphericity of every track with two or fewer positions to be 1. +# * **overallAngle**: angle (degrees) between the first and the last segment of the given track. Angles are measured symmetrically, thus the return values range from 0 to pi; for instance, both a 90 degrees left and right turns yield the same value pi/2 radians. +# * **meanTurningAngle**: averages the overallAngle over all adjacent segments of a given track; a low meanTurningAngle indicates high persistence of orientation, whereas for an uncorrelated random walk we expect 90 degrees. Note that angle measurements will yield NA values for tracks in which two subsequent positions are identical. +# * **overallDot**: computes the dot product between the first and the last segment of the given track. +# * **overallNormDot**: computes the dot product between the unit vectors along the first and the last segment of the given track. These two functions may be useful to generate autocovariance plots. +# * **fractalDimension**: fractal dimension is a mathematical concept used to describe the complexity of self-similar patterns or structures, such as fractals. It is a measure of how much detail or irregularity is present in a pattern or structure. This function estimates the fractal dimension of a track using the function fd.estim.boxcount, which involves dividing the cell trajectories into smaller and smaller boxes of a given size, counting the number of boxes that contain part of the trajectory, and then using this information to estimate the fractal dimension. In general, a higher fractal dimension can indicate that the cell track is more complex or irregular, and may be more invasive or aggressive. Conversely, a lower fractal dimension may indicate a more regular or uniform track shape, and may be associated with less invasive or aggressive behavior. + +# NOTE: With increasing window sizes, the number of available timepoints per cell decrease, since we can not create subtracks of length w starting in the last w timepoints. diff --git a/tests/test_lineageTree.py b/tests/test_lineageTree.py index 284ffa6..b6f2bd9 100644 --- a/tests/test_lineageTree.py +++ b/tests/test_lineageTree.py @@ -797,3 +797,58 @@ def test_smoothing(): new_pos[1552], np.array([462.15385069, 907.17562352, 419.54303692]) ).all() lt.pos = lt.old_pos + +def test_get_track_length(): + result_0 = lt.get_track_length(0) + result_1 = lt.get_track_length(1) + + + +def test_get_duration(): + result = lt.get_duration() + + + +def test_get_max_displacement(): + result = lt.get_max_displacement() + + +def test_get_speed(): + speed_0 = lt.get_speed(0) + speed_1 = lt.get_speed(1) + + +def test_get_displacement(): + result = lt.get_displacement() + + +def test_get_velocity(): + result = lt.get_velocity() + + +def test_get_mean_squared_displacement(): + result = lt.get_mean_squared_displacement() + + +def test_get_displacement_ratio(): + ratio_0 = lt.get_displacement_ratio(0) + ratio_1 = lt.get_displacement_ratio(1) + + +def test_get_straightness(): + s0 = lt.get_straightness(0) + s1 = lt.get_straightness(1) + + +def test_get_asphericity(): + a1 = lt.get_asphericity(1) + a0 = lt.get_asphericity(0) + + +def test_get_angles(): + angles_0 = lt.get_angles(0) + angles_1 = lt.get_angles(1) + + +def test_get_overall_angle(): + result = lt.get_overall_angle() \ No newline at end of file From d4e03dff463c0da1d0ded8816a36b235242c1030 Mon Sep 17 00:00:00 2001 From: BadPrograms Date: Fri, 8 May 2026 09:38:25 +0200 Subject: [PATCH 2/6] docstrings added --- .../approximation/approximations.py | 44 +++++ src/lineagetree/measure/spatial.py | 153 +++++++++++++++--- 2 files changed, 179 insertions(+), 18 deletions(-) create mode 100644 src/lineagetree/approximation/approximations.py diff --git a/src/lineagetree/approximation/approximations.py b/src/lineagetree/approximation/approximations.py new file mode 100644 index 0000000..31b6aef --- /dev/null +++ b/src/lineagetree/approximation/approximations.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from dataclasses import dataclass +from abc import ABC, abstractmethod +from collections import deque +from typing import TYPE_CHECKING, Iterable, Callable +import numpy as np +from scipy.interpolate import InterpolatedUnivariateSpline +from hashlib import sha256 +import pickle + +from edist import uted + +from .delta_functions import ( + delta_normalized_difference, + delta_nd_norm, + delta_difference, +) + +if TYPE_CHECKING: + from ..lineage_tree import LineageTree + from edist.alignment import Alignment + +class TreeApproximator(ABC): + + def build_approximated_tree(self): + """This functionm will create the nod list and the adjacency_list + """ + + def get_norm(self): + """Return the distance of the approximated tree to the null tree. + """ + def __init__(self, lT:LineageTree, starting_node:int, ending_timepoint:int|None=None) -> None: + """_summary_ + + Parameters + ---------- + lT : LineageTree + _description_ + starting_node : int + _description_ + ending_timepoint : int | None, optional + _description_, by default None + """ diff --git a/src/lineagetree/measure/spatial.py b/src/lineagetree/measure/spatial.py index 2483eaa..6271f79 100644 --- a/src/lineagetree/measure/spatial.py +++ b/src/lineagetree/measure/spatial.py @@ -279,7 +279,7 @@ def compute_spatial_edges( return lT.th_edges -def _track_length(chain: np.ndarray, sigma: int = 1.5) -> float: +def _track_length(chain: np.ndarray, sigma: float = 1.5) -> float: """Computes the anchored gaussian smooth of a chain and returns the distance travelled for a given chain. Parameters @@ -310,7 +310,7 @@ def _track_length(chain: np.ndarray, sigma: int = 1.5) -> float: def get_track_length(lT: LineageTree, sigma: float = 1.5) -> dict[int, float]: - """Returns the distance travelled for each chain. + """Returns the distance travelled for each timepoint. Parameters ---------- @@ -323,7 +323,7 @@ def get_track_length(lT: LineageTree, sigma: float = 1.5) -> dict[int, float]: Returns ------- dict[int, float] - The distance travelled for each chain assigned to all of its nodes. + dictionary that maps a node id to its track length """ track_length = {} for track in lT.all_chains: @@ -345,7 +345,7 @@ def get_duration(lT: LineageTree) -> dict[int, float]: Returns ------- dict[int,float] - The duration of each chain assigned to all of its nodes. + dictionary that maps a node id to the temporal duration of its chain. """ lT.duration = { node: len(lT.get_chain_of_node(node)) * lT.time_resolution @@ -354,7 +354,21 @@ def get_duration(lT: LineageTree) -> dict[int, float]: return lT.duration -def get_max_displacement(lT: LineageTree): +def get_max_displacement(lT: LineageTree) -> dict[int, float]: + """Maximal Euclidean distance of any position on the subtrack from the first position. + Each node in a given chain has the same max displacement. + + Parameters + ---------- + lT : LineageTree + The LineageTree object. + + Returns + ------- + dict[int, float] + a dictionary that maps each node to the max displacement of its chain. + """ + lT.max_displacement = {} for chain in lT.all_chains: root = lT.get_ancestor_at_t(chain[0]) @@ -367,7 +381,21 @@ def get_max_displacement(lT: LineageTree): return lT.max_displacement -def get_speed(lT: LineageTree, sigma: int) -> dict[int, float]: +def get_speed(lT: LineageTree, sigma: float) -> dict[int, float]: + """The speed of each node. + + Parameters + ---------- + lT : LineageTree + The LineageTree object. + sigma : float + The smoothing factor + + Returns + ------- + dict[int, float] + dictionary that maps each node to its speed ( distance travelled/time_resolution) + """ track_length = get_track_length(lT, sigma) lT.speed = { @@ -376,7 +404,19 @@ def get_speed(lT: LineageTree, sigma: int) -> dict[int, float]: return lT.speed -def get_displacement(lT: LineageTree): +def get_displacement(lT: LineageTree) -> dict[int, float]: + """Displacement between a the start and the end of each chain. + + Parameters + ---------- + lT : LineageTree + The LineageTree object. + + Returns + ------- + dict[int, float] + dictionary that maps each node to the displacement of its chain (start to end) + """ displacement = {} for chain in lT.all_chains: displacement.update( @@ -389,7 +429,19 @@ def get_displacement(lT: LineageTree): return lT.displacement -def get_velocity(lT: LineageTree): +def get_velocity(lT: LineageTree) -> dict[int, float]: + """The velocity of each node. + + Parameters + ---------- + lT : LineageTree + The LineageTree object. + + Returns + ------- + dict[int, float] + dictionary that maps each node ti its velocity + """ disp = get_displacement(lT) lT.velocity = { node: dist / lT.time_resolution for node, dist in disp.items() @@ -399,7 +451,19 @@ def get_velocity(lT: LineageTree): def get_mean_squared_displacement( lT: LineageTree, -): +) -> dict[int, float]: + """The Mean Squared Euclidean distance between the track starting and endpoints + + Parameters + ---------- + lT : LineageTree + The LineageTree object. + + Returns + ------- + dict[int, float] + dictionary that maps each node to the msd of its respective chain. + """ lT.msd = {} for chain in lT.all_chains: positions = np.array([lT.pos[c] for c in chain]) @@ -410,7 +474,20 @@ def get_mean_squared_displacement( return lT.msd -def get_displacement_ratio(lT: LineageTree, sigma: float = 1.5): +def get_displacement_ratio(lT: LineageTree) -> dict[int, float]: + """The displacement ratio, displacement/max_displacement, displacement of each chain divided by + the displacement from the root node. + + Parameters + ---------- + lT : LineageTree + The LineageTree object. + + Returns + ------- + dict[int, float] + dictionary that maps each node to its displacement ratio. + """ disp = get_displacement(lT) max_disp = get_max_displacement(lT) lT.displacement_ratio = { @@ -419,7 +496,7 @@ def get_displacement_ratio(lT: LineageTree, sigma: float = 1.5): return lT.displacement_ratio -def get_outreach_ratio(lT: LineageTree, sigma: float = 1.5): +def get_outreach_ratio(lT: LineageTree, sigma: float = 1.5) -> dict[int, float]: max_disp = get_max_displacement(lT) track_length = get_track_length(lT, sigma) lT.displacement_ratio = { @@ -428,7 +505,7 @@ def get_outreach_ratio(lT: LineageTree, sigma: float = 1.5): return lT.displacement_ratio -def get_straightness(lT: LineageTree, sigma: float = 1.5): +def get_straightness(lT: LineageTree, sigma: float = 1.5) -> dict[int, float]: displacement = get_displacement(lT) track_length = get_track_length(lT, sigma) lT.straightness = { @@ -437,7 +514,7 @@ def get_straightness(lT: LineageTree, sigma: float = 1.5): return lT.straightness -def _inertia_matrix(chain, sigma: float = 1.5): +def _inertia_matrix(chain, sigma: float = 1.5)-> np.ndarray: """Calculates the inertia of a given point cloud. Parameters @@ -466,8 +543,8 @@ def _inertia_matrix(chain, sigma: float = 1.5): return np.array([[Ixx, Ixy, Ixz], [Ixy, Iyy, Iyz], [Ixz, Iyz, Izz]]) -def get_asphericity(lT: LineageTree, sigma: float = 1.5): - """Calculate the asphericity of a track +def get_asphericity(lT: LineageTree, sigma: float = 1.5) -> dict[int, float]: + """Calculate the asphericity of a track. For 0 it is perfectly spherical, while for 1 it is not spherical. Adapted from : J Rudnick and G Gaspari 1986 J. Phys. A: Math. Gen. 19 L191 Parameters @@ -476,6 +553,12 @@ def get_asphericity(lT: LineageTree, sigma: float = 1.5): The LineageTree object sigma : _type_ Smoothing factor. + + Returns + ------- + dict[int, float]: + dictionary that maps each node to the sphericity of each respective track. + """ lT.asphericity = {} @@ -494,10 +577,32 @@ def get_asphericity(lT: LineageTree, sigma: float = 1.5): ) asphericity = (tr**2 - 3 * M) / (tr**2) lT.asphericity.update({node: asphericity for node in chain}) - return lT.asphericity + return lT.asphericity + +def get_angles(lT: LineageTree, sigma: float = 1.5) -> dict[int, float]: + """Angles between adjacent edges. -def get_angles(lT: LineageTree, sigma: float = 1.5): + Parameters + ---------- + lT : LineageTree + The LineageTree object. + sigma : float, optional + The smoothing factor, by default 1.5 + + Returns + ------- + dict[int, float] + dictionary that maps each node to the angle between its diplacement and the next ones displacement. + o + / + i + | + e + \\ + a + for i its the angle of o->i to i->e + """ lT.angles = {} for chain in lT.all_chains: if len(chain)<3: @@ -529,7 +634,19 @@ def get_angles(lT: LineageTree, sigma: float = 1.5): return lT.angles -def get_overall_angle(lT: LineageTree): +def get_overall_angle(lT: LineageTree) -> dict[int, float]: + """_summary_ + + Parameters + ---------- + lT : LineageTree + The LineageTree object. + + Returns + ------- + dict[int, float] + _description_ + """ lT.overall_angles = {} for chain in lT.all_chains: if len(chain)<3: From 9518331487f359f4250d11a122c97877a73e6ed7 Mon Sep 17 00:00:00 2001 From: BadPrograms Date: Tue, 19 May 2026 14:03:01 +0200 Subject: [PATCH 3/6] progress --- src/lineagetree/measure/spatial.py | 68 ++++++++++++++++++------------ tests/test_lineageTree.py | 12 +++--- 2 files changed, 45 insertions(+), 35 deletions(-) diff --git a/src/lineagetree/measure/spatial.py b/src/lineagetree/measure/spatial.py index 6271f79..384fe91 100644 --- a/src/lineagetree/measure/spatial.py +++ b/src/lineagetree/measure/spatial.py @@ -355,8 +355,8 @@ def get_duration(lT: LineageTree) -> dict[int, float]: def get_max_displacement(lT: LineageTree) -> dict[int, float]: - """Maximal Euclidean distance of any position on the subtrack from the first position. - Each node in a given chain has the same max displacement. + """Maximal Euclidean distance of any position on the subtrack from the first position. + Each node in a given chain has the same max displacement. Parameters ---------- @@ -368,7 +368,7 @@ def get_max_displacement(lT: LineageTree) -> dict[int, float]: dict[int, float] a dictionary that maps each node to the max displacement of its chain. """ - + lT.max_displacement = {} for chain in lT.all_chains: root = lT.get_ancestor_at_t(chain[0]) @@ -376,8 +376,12 @@ def get_max_displacement(lT: LineageTree) -> dict[int, float]: continue root_pos = lT.pos[root] positions = np.array([lT.pos[c] for c in chain]) - displacements = np.cumsum(np.linalg.norm((positions - root_pos), axis=1)) - lT.max_displacement.update({node:disp for node, disp in zip(chain,displacements)}) + displacements = np.cumsum( + np.linalg.norm((positions - root_pos), axis=1) + ) + lT.max_displacement.update( + {node: disp for node, disp in zip(chain, displacements)} + ) return lT.max_displacement @@ -496,11 +500,15 @@ def get_displacement_ratio(lT: LineageTree) -> dict[int, float]: return lT.displacement_ratio -def get_outreach_ratio(lT: LineageTree, sigma: float = 1.5) -> dict[int, float]: +def get_outreach_ratio( + lT: LineageTree, sigma: float = 1.5 +) -> dict[int, float]: max_disp = get_max_displacement(lT) track_length = get_track_length(lT, sigma) lT.displacement_ratio = { - node: max_disp[node] / track_length[node] for node in track_length if track_length[node] + node: max_disp[node] / track_length[node] + for node in track_length + if track_length[node] } return lT.displacement_ratio @@ -514,7 +522,7 @@ def get_straightness(lT: LineageTree, sigma: float = 1.5) -> dict[int, float]: return lT.straightness -def _inertia_matrix(chain, sigma: float = 1.5)-> np.ndarray: +def _inertia_matrix(chain, sigma: float = 1.5) -> np.ndarray: """Calculates the inertia of a given point cloud. Parameters @@ -523,8 +531,14 @@ def _inertia_matrix(chain, sigma: float = 1.5)-> np.ndarray: The lineageTree object sigma : float, optional The smoothing factor, by default 1.5 + + Returns + ------- + np.ndarray + The 3x3 inertia array of a point cloud. + """ - smoothed_chain = np.zeros_like(chain, dtype = float) + smoothed_chain = np.zeros_like(chain, dtype=float) if sigma != 0: smoothed_chain[:, 0] = anchored_gaussian_smooth(chain[:, 0], sigma) smoothed_chain[:, 1] = anchored_gaussian_smooth(chain[:, 1], sigma) @@ -536,9 +550,9 @@ def _inertia_matrix(chain, sigma: float = 1.5)-> np.ndarray: Iyy = np.sum(cloud[:, 2] ** 2 + cloud[:, 0] ** 2) Izz = np.sum(cloud[:, 0] ** 2 + cloud[:, 1] ** 2) - Ixy = -np.sum(cloud[:, 0]* cloud[:, 1]) - Ixz = -np.sum(cloud[:, 0]* cloud[:, 2]) - Iyz = -np.sum(cloud[:, 1]* cloud[:, 2]) + Ixy = -np.sum(cloud[:, 0] * cloud[:, 1]) + Ixz = -np.sum(cloud[:, 0] * cloud[:, 2]) + Iyz = -np.sum(cloud[:, 1] * cloud[:, 2]) return np.array([[Ixx, Ixy, Ixz], [Ixy, Iyy, Iyz], [Ixz, Iyz, Izz]]) @@ -553,7 +567,7 @@ def get_asphericity(lT: LineageTree, sigma: float = 1.5) -> dict[int, float]: The LineageTree object sigma : _type_ Smoothing factor. - + Returns ------- dict[int, float]: @@ -564,7 +578,7 @@ def get_asphericity(lT: LineageTree, sigma: float = 1.5) -> dict[int, float]: for chain in lT.all_chains: chain_pos = np.array([lT.pos[c] for c in chain]) - if len(chain)<4: + if len(chain) < 4: continue else: inertia = _inertia_matrix(chain_pos, sigma) @@ -594,18 +608,12 @@ def get_angles(lT: LineageTree, sigma: float = 1.5) -> dict[int, float]: ------- dict[int, float] dictionary that maps each node to the angle between its diplacement and the next ones displacement. - o - / - i - | - e - \\ - a - for i its the angle of o->i to i->e + For example for and edge containing the nodes o,i,e,a: o -> i -> e -> a + for i its the angle of o->i to i->e, for e is the angle of i->e to e->a """ lT.angles = {} for chain in lT.all_chains: - if len(chain)<3: + if len(chain) < 3: continue else: positions = np.array([lT.pos[c] for c in chain]) @@ -622,7 +630,7 @@ def get_angles(lT: LineageTree, sigma: float = 1.5) -> dict[int, float]: ) else: smoothed_positions = positions # Not smoothed - + vectors1 = smoothed_positions[:-1] - smoothed_positions[1:] vectors2 = vectors1[1:] angles = [ @@ -630,12 +638,16 @@ def get_angles(lT: LineageTree, sigma: float = 1.5) -> dict[int, float]: for v1, v2 in zip(vectors1, vectors2) ] angles = [0] + angles + [0] # first and last node have no angle - lT.angles.update({node: angle for node, angle in zip(chain, angles)}) + lT.angles.update( + {node: angle for node, angle in zip(chain, angles)} + ) return lT.angles def get_overall_angle(lT: LineageTree) -> dict[int, float]: - """_summary_ + """The angle (degrees) between the first and the last segment of the given track. + Angles are measured symmetrically, thus the return values range from 0 to pi; + for instance, both a 90 degrees left and right turns yield the same value pi/2 radians. Parameters ---------- @@ -645,11 +657,11 @@ def get_overall_angle(lT: LineageTree) -> dict[int, float]: Returns ------- dict[int, float] - _description_ + dictionary that maps the overall angle of a given chain to every node of its chain """ lT.overall_angles = {} for chain in lT.all_chains: - if len(chain)<3: + if len(chain) < 3: continue else: vector1 = lT.pos[chain[1]] - lT.pos[chain[0]] diff --git a/tests/test_lineageTree.py b/tests/test_lineageTree.py index b6f2bd9..1e220a3 100644 --- a/tests/test_lineageTree.py +++ b/tests/test_lineageTree.py @@ -798,25 +798,24 @@ def test_smoothing(): ).all() lt.pos = lt.old_pos + def test_get_track_length(): result_0 = lt.get_track_length(0) result_1 = lt.get_track_length(1) - def test_get_duration(): result = lt.get_duration() - def test_get_max_displacement(): result = lt.get_max_displacement() - + def test_get_speed(): speed_0 = lt.get_speed(0) speed_1 = lt.get_speed(1) - + def test_get_displacement(): result = lt.get_displacement() @@ -831,8 +830,7 @@ def test_get_mean_squared_displacement(): def test_get_displacement_ratio(): - ratio_0 = lt.get_displacement_ratio(0) - ratio_1 = lt.get_displacement_ratio(1) + ratio_1 = lt.get_displacement_ratio() def test_get_straightness(): @@ -851,4 +849,4 @@ def test_get_angles(): def test_get_overall_angle(): - result = lt.get_overall_angle() \ No newline at end of file + result = lt.get_overall_angle() From b0965621eb380f72444cce790a22767d20d3a89b Mon Sep 17 00:00:00 2001 From: BadPrograms Date: Fri, 22 May 2026 16:35:15 +0200 Subject: [PATCH 4/6] changing names --- src/lineagetree/_mixins/spatial_mixin.py | 52 ++--- .../approximation/approximations.py | 36 ++-- src/lineagetree/measure/spatial.py | 179 ++++++++++-------- tests/test_lineageTree.py | 58 +++--- 4 files changed, 169 insertions(+), 156 deletions(-) diff --git a/src/lineagetree/_mixins/spatial_mixin.py b/src/lineagetree/_mixins/spatial_mixin.py index c836e20..e77fbdf 100644 --- a/src/lineagetree/_mixins/spatial_mixin.py +++ b/src/lineagetree/_mixins/spatial_mixin.py @@ -5,19 +5,19 @@ gabriel_graph, idx3d, neighbours_in_radius, - get_angles, - get_asphericity, - get_displacement, - get_displacement_ratio, - get_duration, - get_max_displacement, - get_mean_squared_displacement, - get_outreach_ratio, - get_overall_angle, - get_speed, - get_straightness, - get_track_length, - get_velocity, + angles, + asphericity, + displacement, + displacement_ratio, + duration, + max_displacement, + mean_squared_displacement, + outreach_ratio, + overall_angle, + speed, + straightness, + track_length, + velocity, ) from ._methodize import AutoMethodizeMeta @@ -32,28 +32,28 @@ class SpatialMixin(metaclass=AutoMethodizeMeta): spatial_edges = spatial_edges spatial_density = spatial_density neighbours_in_radius = neighbours_in_radius - get_angles = get_angles + angles = angles - get_asphericity = get_asphericity + asphericity = asphericity - get_displacement = get_displacement + displacement = displacement - get_displacement_ratio = get_displacement_ratio + displacement_ratio = displacement_ratio - get_duration = get_duration + duration = duration - get_max_displacement = get_max_displacement + max_displacement = max_displacement - get_mean_squared_displacement = get_mean_squared_displacement + mean_squared_displacement = mean_squared_displacement - get_outreach_ratio = get_outreach_ratio + outreach_ratio = outreach_ratio - get_overall_angle = get_overall_angle + overall_angle = overall_angle - get_speed = get_speed + speed = speed - get_straightness = get_straightness + straightness = straightness - get_track_length = get_track_length + track_length = track_length - get_velocity = get_velocity + velocity = velocity diff --git a/src/lineagetree/approximation/approximations.py b/src/lineagetree/approximation/approximations.py index 31b6aef..da36f2c 100644 --- a/src/lineagetree/approximation/approximations.py +++ b/src/lineagetree/approximation/approximations.py @@ -1,36 +1,28 @@ from __future__ import annotations -from dataclasses import dataclass -from abc import ABC, abstractmethod -from collections import deque -from typing import TYPE_CHECKING, Iterable, Callable -import numpy as np -from scipy.interpolate import InterpolatedUnivariateSpline -from hashlib import sha256 -import pickle - -from edist import uted - -from .delta_functions import ( - delta_normalized_difference, - delta_nd_norm, - delta_difference, -) +from abc import ABC +from typing import TYPE_CHECKING + + if TYPE_CHECKING: from ..lineage_tree import LineageTree - from edist.alignment import Alignment + class TreeApproximator(ABC): def build_approximated_tree(self): - """This functionm will create the nod list and the adjacency_list - """ + """This functionm will create the nod list and the adjacency_list""" def get_norm(self): - """Return the distance of the approximated tree to the null tree. - """ - def __init__(self, lT:LineageTree, starting_node:int, ending_timepoint:int|None=None) -> None: + """Return the distance of the approximated tree to the null tree.""" + + def __init__( + self, + lT: LineageTree, + starting_node: int, + ending_timepoint: int | None = None, + ) -> None: """_summary_ Parameters diff --git a/src/lineagetree/measure/spatial.py b/src/lineagetree/measure/spatial.py index ee17a42..4b748bc 100644 --- a/src/lineagetree/measure/spatial.py +++ b/src/lineagetree/measure/spatial.py @@ -305,7 +305,7 @@ def _track_length(chain: np.ndarray, sigma: float = 1.5) -> float: return sum(distance_travelled) -def get_track_length(lT: LineageTree, sigma: float = 1.5) -> dict[int, float]: +def track_length(lT: LineageTree, sigma: float = 1.5) -> dict[int, float]: """Returns the distance travelled for each timepoint. Parameters @@ -321,16 +321,16 @@ def get_track_length(lT: LineageTree, sigma: float = 1.5) -> dict[int, float]: dict[int, float] dictionary that maps a node id to its track length """ - track_length = {} + tr_length = {} for track in lT.all_chains: data = np.array([lT.pos[c] for c in track]) dist = _track_length(data, sigma) - track_length.update({node: dist for node in track}) - lT.track_length = track_length - return lT.track_length + tr_length.update({node: dist for node in track}) + lT.Track_length = tr_length + return lT.Track_length -def get_duration(lT: LineageTree) -> dict[int, float]: +def duration(lT: LineageTree) -> dict[int, float]: """The duration of each chain. Parameters @@ -343,14 +343,14 @@ def get_duration(lT: LineageTree) -> dict[int, float]: dict[int,float] dictionary that maps a node id to the temporal duration of its chain. """ - lT.duration = { + lT.Duration = { node: len(lT.get_chain_of_node(node)) * lT.time_resolution for node in lT.nodes } - return lT.duration + return lT.Duration -def get_max_displacement(lT: LineageTree) -> dict[int, float]: +def max_displacement(lT: LineageTree) -> dict[int, float]: """Maximal Euclidean distance of any position on the subtrack from the first position. Each node in a given chain has the same max displacement. @@ -365,7 +365,7 @@ def get_max_displacement(lT: LineageTree) -> dict[int, float]: a dictionary that maps each node to the max displacement of its chain. """ - lT.max_displacement = {} + lT.Max_displacement = {} for chain in lT.all_chains: root = lT.get_ancestor_at_t(chain[0]) if root not in lT.nodes: @@ -375,36 +375,37 @@ def get_max_displacement(lT: LineageTree) -> dict[int, float]: displacements = np.cumsum( np.linalg.norm((positions - root_pos), axis=1) ) - lT.max_displacement.update( + lT.Max_displacement.update( {node: disp for node, disp in zip(chain, displacements)} ) - return lT.max_displacement + return lT.Max_displacement -def get_speed(lT: LineageTree, sigma: float) -> dict[int, float]: +def speed(lT: LineageTree, sigma: float = 1.5) -> dict[int, float]: """The speed of each node. Parameters ---------- lT : LineageTree The LineageTree object. - sigma : float - The smoothing factor + sigma : float, optional + Standard deviation of the Gaussian kernel used for smoothing. + Higher values produce stronger smoothing, by default is 1.5. Returns ------- dict[int, float] - dictionary that maps each node to its speed ( distance travelled/time_resolution) + dictionary that maps each node to its speed (distance travelled/time_resolution) """ - track_length = get_track_length(lT, sigma) - lT.speed = { - node: dist / lT.time_resolution for node, dist in track_length.items() + tr_length = track_length(lT, sigma) + lT.Speed = { + node: dist / lT.time_resolution for node, dist in tr_length.items() } - return lT.speed + return lT.Speed -def get_displacement(lT: LineageTree) -> dict[int, float]: +def displacement(lT: LineageTree) -> dict[int, float]: """Displacement between a the start and the end of each chain. Parameters @@ -425,11 +426,11 @@ def get_displacement(lT: LineageTree) -> dict[int, float]: for node in chain } ) - lT.displacement = displacement - return lT.displacement + lT.Displacement = displacement + return lT.Displacement -def get_velocity(lT: LineageTree) -> dict[int, float]: +def velocity(lT: LineageTree) -> dict[int, float]: """The velocity of each node. Parameters @@ -440,16 +441,16 @@ def get_velocity(lT: LineageTree) -> dict[int, float]: Returns ------- dict[int, float] - dictionary that maps each node ti its velocity + dictionary that maps each node to its velocity """ - disp = get_displacement(lT) - lT.velocity = { + disp = displacement(lT) + lT.Velocity = { node: dist / lT.time_resolution for node, dist in disp.items() } - return lT.velocity + return lT.Velocity -def get_mean_squared_displacement( +def mean_squared_displacement( lT: LineageTree, ) -> dict[int, float]: """The Mean Squared Euclidean distance between the track starting and endpoints @@ -474,7 +475,7 @@ def get_mean_squared_displacement( return lT.msd -def get_displacement_ratio(lT: LineageTree) -> dict[int, float]: +def displacement_ratio(lT: LineageTree) -> dict[int, float]: """The displacement ratio, displacement/max_displacement, displacement of each chain divided by the displacement from the root node. @@ -488,34 +489,62 @@ def get_displacement_ratio(lT: LineageTree) -> dict[int, float]: dict[int, float] dictionary that maps each node to its displacement ratio. """ - disp = get_displacement(lT) - max_disp = get_max_displacement(lT) - lT.displacement_ratio = { + disp = displacement(lT) + max_disp = max_displacement(lT) + lT.Displacement_ratio = { node: disp[node] / max_disp[node] for node in disp if max_disp[node] } - return lT.displacement_ratio + return lT.Displacement_ratio -def get_outreach_ratio( - lT: LineageTree, sigma: float = 1.5 -) -> dict[int, float]: - max_disp = get_max_displacement(lT) - track_length = get_track_length(lT, sigma) - lT.displacement_ratio = { - node: max_disp[node] / track_length[node] - for node in track_length - if track_length[node] - } - return lT.displacement_ratio +def outreach_ratio(lT: LineageTree, sigma: float = 1.5) -> dict[int, float]: + """maxDisplacement/trackLength + (values between 0 and 1, where 1 means a perfectly straight track) + Parameters + ---------- + lT : LineageTree + The LineageTree object. + sigma : float, optional + Standard deviation of the Gaussian kernel used for smoothing. + Higher values produce stronger smoothing, by default is 1.5. -def get_straightness(lT: LineageTree, sigma: float = 1.5) -> dict[int, float]: - displacement = get_displacement(lT) - track_length = get_track_length(lT, sigma) - lT.straightness = { - node: displacement[node] / track_length[node] for node in displacement + Returns + ------- + dict[int, float] + dictionary that maps erach node to the outreach ratio of its chain + """ + max_disp = max_displacement(lT) + tr_length = track_length(lT, sigma) + lT.Outreach_ratio = { + node: max_disp[node] / tr_length[node] + for node in tr_length + if tr_length[node] } - return lT.straightness + return lT.Outreach_ratio + + +def straightness(lT: LineageTree, sigma: float = 1.5) -> dict[int, float]: + """displacement/trackLength + (values between 0 and 1, where 1 means a perfectly straight track) + + Parameters + ---------- + lT : LineageTree + The LineageTree object. + sigma : float, optional + Standard deviation of the Gaussian kernel used for smoothing. + Higher values produce stronger smoothing, by default is 1.5. + + Returns + ------- + dict[int, float] + dictionary that maps each node to the straightness of its chain + """ + displ = displacement(lT) + tr_length = track_length(lT, sigma) + lT.Straightness = {node: displ[node] / tr_length[node] for node in displ} + return lT.Straightness def _inertia_matrix(chain, sigma: float = 1.5) -> np.ndarray: @@ -526,7 +555,8 @@ def _inertia_matrix(chain, sigma: float = 1.5) -> np.ndarray: lT : LineageTree The lineageTree object sigma : float, optional - The smoothing factor, by default 1.5 + Standard deviation of the Gaussian kernel used for smoothing. + Higher values produce stronger smoothing, by default is 1.5. Returns ------- @@ -553,7 +583,7 @@ def _inertia_matrix(chain, sigma: float = 1.5) -> np.ndarray: return np.array([[Ixx, Ixy, Ixz], [Ixy, Iyy, Iyz], [Ixz, Iyz, Izz]]) -def get_asphericity(lT: LineageTree, sigma: float = 1.5) -> dict[int, float]: +def asphericity(lT: LineageTree, sigma: float = 1.5) -> dict[int, float]: """Calculate the asphericity of a track. For 0 it is perfectly spherical, while for 1 it is not spherical. Adapted from : J Rudnick and G Gaspari 1986 J. Phys. A: Math. Gen. 19 L191 @@ -561,8 +591,9 @@ def get_asphericity(lT: LineageTree, sigma: float = 1.5) -> dict[int, float]: ---------- lT : LineageTree The LineageTree object - sigma : _type_ - Smoothing factor. + sigma : float, by degault 1.5 + Standard deviation of the Gaussian kernel used for smoothing. + Higher values produce stronger smoothing, by default is 1.5. Returns ------- @@ -570,7 +601,7 @@ def get_asphericity(lT: LineageTree, sigma: float = 1.5) -> dict[int, float]: dictionary that maps each node to the sphericity of each respective track. """ - lT.asphericity = {} + lT.Asphericity = {} for chain in lT.all_chains: chain_pos = np.array([lT.pos[c] for c in chain]) @@ -586,19 +617,20 @@ def get_asphericity(lT: LineageTree, sigma: float = 1.5) -> dict[int, float]: + eig_vals[0] ** 2 * eig_vals[2] ** 2 ) asphericity = (tr**2 - 3 * M) / (tr**2) - lT.asphericity.update({node: asphericity for node in chain}) - return lT.asphericity + lT.Asphericity.update({node: asphericity for node in chain}) + return lT.Asphericity -def get_angles(lT: LineageTree, sigma: float = 1.5) -> dict[int, float]: +def angles(lT: LineageTree, sigma: float = 1.5) -> dict[int, float]: """Angles between adjacent edges. Parameters ---------- lT : LineageTree The LineageTree object. - sigma : float, optional - The smoothing factor, by default 1.5 + sigma : float, by degault 1.5 + Standard deviation of the Gaussian kernel used for smoothing. + Higher values produce stronger smoothing, by default is 1.5. Returns ------- @@ -607,7 +639,7 @@ def get_angles(lT: LineageTree, sigma: float = 1.5) -> dict[int, float]: For example for and edge containing the nodes o,i,e,a: o -> i -> e -> a for i its the angle of o->i to i->e, for e is the angle of i->e to e->a """ - lT.angles = {} + lT.Angles = {} for chain in lT.all_chains: if len(chain) < 3: continue @@ -634,13 +666,13 @@ def get_angles(lT: LineageTree, sigma: float = 1.5) -> dict[int, float]: for v1, v2 in zip(vectors1, vectors2) ] angles = [0] + angles + [0] # first and last node have no angle - lT.angles.update( + lT.Angles.update( {node: angle for node, angle in zip(chain, angles)} ) - return lT.angles + return lT.Angles -def get_overall_angle(lT: LineageTree) -> dict[int, float]: +def overall_angle(lT: LineageTree) -> dict[int, float]: """The angle (degrees) between the first and the last segment of the given track. Angles are measured symmetrically, thus the return values range from 0 to pi; for instance, both a 90 degrees left and right turns yield the same value pi/2 radians. @@ -655,7 +687,7 @@ def get_overall_angle(lT: LineageTree) -> dict[int, float]: dict[int, float] dictionary that maps the overall angle of a given chain to every node of its chain """ - lT.overall_angles = {} + lT.Overall_angles = {} for chain in lT.all_chains: if len(chain) < 3: continue @@ -665,23 +697,12 @@ def get_overall_angle(lT: LineageTree) -> dict[int, float]: overangle = (vector1 @ vector2) / ( np.linalg.norm(vector1) * np.linalg.norm(vector2) ) - lT.overall_angles.update({node: overangle for node in chain}) - return lT.overall_angles + lT.Overall_angles.update({node: overangle for node in chain}) + return lT.Overall_angles # TODO -# * **trackLength**: sums up the distances between subsequent positions; in other words, it estimates the length of the underlying subtrack by linear interpolation (usually an underestimation) -# * **duration**: time elapsed between first and last positions of the subtrack -# * **maxDisplacement**: maximal Euclidean distance of any position on the subtrack from the first position -# * **speed**: trackLength/duration -# * **displacement**: Euclidean distance between the track starting and endpoints -# * **squareDisplacement**: squared Euclidean distance between the track starting and endpoints -# * **displacementRatio**: displacement/maxDisplacement (values between 0 and 1, where 1 means a perfectly straight track) -# * **outreachRatio**: maxDisplacement/trackLength (values between 0 and 1, where 1 means a perfectly straight track) -# * **straightness**: displacement/trackLength (values between 0 and 1, where 1 means a perfectly straight track) -# * **asphericity**: a different appraoch to measure straightness, that computes the asphericity of the set of positions on the subtrack _via_ the length of its principal components (number between 0 and 1, with higher values indicating straighter tracks). Unlike straightness, however, asphericity ignores back-and-forth motion of the object, so something that bounces between two positions will have low straightness but high asphericity. We define the asphericity of every track with two or fewer positions to be 1. -# * **overallAngle**: angle (degrees) between the first and the last segment of the given track. Angles are measured symmetrically, thus the return values range from 0 to pi; for instance, both a 90 degrees left and right turns yield the same value pi/2 radians. # * **meanTurningAngle**: averages the overallAngle over all adjacent segments of a given track; a low meanTurningAngle indicates high persistence of orientation, whereas for an uncorrelated random walk we expect 90 degrees. Note that angle measurements will yield NA values for tracks in which two subsequent positions are identical. # * **overallDot**: computes the dot product between the first and the last segment of the given track. # * **overallNormDot**: computes the dot product between the unit vectors along the first and the last segment of the given track. These two functions may be useful to generate autocovariance plots. diff --git a/tests/test_lineageTree.py b/tests/test_lineageTree.py index 238ca04..8d21c73 100644 --- a/tests/test_lineageTree.py +++ b/tests/test_lineageTree.py @@ -799,54 +799,54 @@ def test_smoothing(): lt.pos = lt.old_pos -def test_get_track_length(): - result_0 = lt.get_track_length(0) - result_1 = lt.get_track_length(1) +def test_track_length(): + lt.track_length(0) + lt.track_length(1) -def test_get_duration(): - result = lt.get_duration() +def test_duration(): + lt.duration() -def test_get_max_displacement(): - result = lt.get_max_displacement() +def test_max_displacement(): + lt.max_displacement() -def test_get_speed(): - speed_0 = lt.get_speed(0) - speed_1 = lt.get_speed(1) +def test_speed(): + lt.speed(0) + lt.speed(1) -def test_get_displacement(): - result = lt.get_displacement() +def test_displacement(): + lt.displacement() -def test_get_velocity(): - result = lt.get_velocity() +def test_velocity(): + lt.velocity() -def test_get_mean_squared_displacement(): - result = lt.get_mean_squared_displacement() +def test_mean_squared_displacement(): + lt.mean_squared_displacement() -def test_get_displacement_ratio(): - ratio_1 = lt.get_displacement_ratio() +def test_displacement_ratio(): + lt.displacement_ratio() -def test_get_straightness(): - s0 = lt.get_straightness(0) - s1 = lt.get_straightness(1) +def test_straightness(): + lt.straightness(0) + lt.straightness(1) -def test_get_asphericity(): - a1 = lt.get_asphericity(1) - a0 = lt.get_asphericity(0) +def test_asphericity(): + lt.asphericity(1) + lt.asphericity(0) -def test_get_angles(): - angles_0 = lt.get_angles(0) - angles_1 = lt.get_angles(1) +def test_angles(): + lt.angles(0) + lt.angles(1) -def test_get_overall_angle(): - result = lt.get_overall_angle() +def test_overall_angle(): + lt.overall_angle() From 20d1e79f4feca4c6f6d86976f89d2f3b5e4b2be9 Mon Sep 17 00:00:00 2001 From: BadPrograms Date: Wed, 17 Jun 2026 12:49:30 +0200 Subject: [PATCH 5/6] not done --- src/lineagetree/measure/spatial.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/lineagetree/measure/spatial.py b/src/lineagetree/measure/spatial.py index 4b748bc..47cbe4b 100644 --- a/src/lineagetree/measure/spatial.py +++ b/src/lineagetree/measure/spatial.py @@ -6,6 +6,8 @@ import numpy as np from scipy.spatial import Delaunay, KDTree from .._core._modifier import anchored_gaussian_smooth +from scipy.interpolate import InterpolatedUnivariateSpline +from scipy.optimize import curve_fit if TYPE_CHECKING: from ..lineage_tree import LineageTree @@ -701,6 +703,25 @@ def overall_angle(lT: LineageTree) -> dict[int, float]: return lT.Overall_angles +def alpha_score( + lT: LineageTree, +): + def _model(x, a, b): + return b * (x**a) + + alphas = {} + for chain in lT.all_chains: + if len(chain) < 5 or chain[0] in lT.roots or chain[-1] in lT.leaves: + continue + positions = np.array([lT.pos[c] for c in chain]) + MSD = np.cumsum( + np.linalg.norm((positions - positions[0]) ** 2, axis=1) + ) / np.arange(1, len(chain) + 1) + pars, *_ = curve_fit(_model, np.linspace(0, len(MSD), 50), MSD) + alphas.update({node: pars[0] for node in chain}) + return alphas + + # TODO # * **meanTurningAngle**: averages the overallAngle over all adjacent segments of a given track; a low meanTurningAngle indicates high persistence of orientation, whereas for an uncorrelated random walk we expect 90 degrees. Note that angle measurements will yield NA values for tracks in which two subsequent positions are identical. From 455d9b3f9e8f8b984105be206a8fb86c715e5ea0 Mon Sep 17 00:00:00 2001 From: BadPrograms Date: Mon, 22 Jun 2026 11:18:08 +0200 Subject: [PATCH 6/6] . --- src/lineagetree/measure/spatial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lineagetree/measure/spatial.py b/src/lineagetree/measure/spatial.py index 47cbe4b..f09b470 100644 --- a/src/lineagetree/measure/spatial.py +++ b/src/lineagetree/measure/spatial.py @@ -712,7 +712,7 @@ def _model(x, a, b): alphas = {} for chain in lT.all_chains: if len(chain) < 5 or chain[0] in lT.roots or chain[-1] in lT.leaves: - continue + alphas.update({node: np.nan for node in chain}) positions = np.array([lT.pos[c] for c in chain]) MSD = np.cumsum( np.linalg.norm((positions - positions[0]) ** 2, axis=1)