From 01b5246b8c96226dbbf1ab928fcea1f58eed5803 Mon Sep 17 00:00:00 2001 From: BadPrograms Date: Wed, 7 Jan 2026 14:23:05 +0100 Subject: [PATCH 1/3] fix for all timepoints --- src/lineagetree/measure/spatial.py | 21 +++++++++++++++++---- tests/test_lineageTree.py | 5 +++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/lineagetree/measure/spatial.py b/src/lineagetree/measure/spatial.py index c74b55c..f10e64d 100644 --- a/src/lineagetree/measure/spatial.py +++ b/src/lineagetree/measure/spatial.py @@ -1,4 +1,5 @@ from __future__ import annotations +from warnings import warn, catch_warnings, simplefilter from itertools import combinations from typing import TYPE_CHECKING @@ -49,7 +50,9 @@ def get_idx3d(lT: LineageTree, t: int) -> tuple[KDTree, np.ndarray]: return idx3d, np.array(to_check_lT) -def get_gabriel_graph(lT: LineageTree, t: int) -> dict[int, set[int]]: +def get_gabriel_graph( + lT: LineageTree, t: int | None = None +) -> dict[int, set[int]]: """Build the Gabriel graph of the given graph for time point `t`. The Garbiel graph is then stored in `lT.Gabriel_graph` and returned. @@ -59,8 +62,8 @@ def get_gabriel_graph(lT: LineageTree, t: int) -> dict[int, set[int]]: ---------- lT : LineageTree The LineageTree instance. - t : int - time + t : int or None + time, if time is set to 'None' the gabriel graph will be calculated for all timepoints, defaults to None. Returns ------- @@ -70,7 +73,17 @@ def get_gabriel_graph(lT: LineageTree, t: int) -> dict[int, set[int]]: if not hasattr(lT, "Gabriel_graph"): lT.Gabriel_graph = {} - if lT.time_nodes[t] - lT.Gabriel_graph.keys(): + if t and len(lT.time_nodes[t]) < 5: + warn("Need more than 5 timepoints") + return lT.Gabriel_graph + + if t is None: + with catch_warnings(): + simplefilter("ignore") + for time in lT.time_nodes: + get_gabriel_graph(lT, time) + + elif lT.time_nodes[t] - lT.Gabriel_graph.keys(): _, nodes = lT.get_idx3d(t) data_corres = {} diff --git a/tests/test_lineageTree.py b/tests/test_lineageTree.py index 50bec7f..b8df55f 100644 --- a/tests/test_lineageTree.py +++ b/tests/test_lineageTree.py @@ -585,6 +585,11 @@ def test_idx3d(): def test_gabriel_graph(): gg = lT1.get_gabriel_graph(0) assert gg[173618] == {110832, 168322} + gg_all = lT1.get_gabriel_graph() + gg_all_2 = {} + for t in lT1.time_nodes: + gg_all_2.update(lT1.get_gabriel_graph(t)) + assert gg_all == gg_all_2 def test_get_chain_of_node(): From a6cf65b2649549d2d619b4621c4e48951bec98db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20Guignard?= Date: Wed, 7 Jan 2026 16:47:14 +0100 Subject: [PATCH 2/3] Better handling of multiple time points and small number of points --- src/lineagetree/measure/spatial.py | 106 +++++++++++++++-------------- 1 file changed, 55 insertions(+), 51 deletions(-) diff --git a/src/lineagetree/measure/spatial.py b/src/lineagetree/measure/spatial.py index f10e64d..c7f4d8b 100644 --- a/src/lineagetree/measure/spatial.py +++ b/src/lineagetree/measure/spatial.py @@ -2,7 +2,7 @@ from warnings import warn, catch_warnings, simplefilter from itertools import combinations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Iterable import numpy as np from scipy.spatial import Delaunay, KDTree @@ -51,7 +51,7 @@ def get_idx3d(lT: LineageTree, t: int) -> tuple[KDTree, np.ndarray]: def get_gabriel_graph( - lT: LineageTree, t: int | None = None + lT: LineageTree, time: int | Iterable[int] | None = None ) -> dict[int, set[int]]: """Build the Gabriel graph of the given graph for time point `t`. The Garbiel graph is then stored in `lT.Gabriel_graph` and returned. @@ -62,8 +62,9 @@ def get_gabriel_graph( ---------- lT : LineageTree The LineageTree instance. - t : int or None - time, if time is set to 'None' the gabriel graph will be calculated for all timepoints, defaults to None. + time : int or Iterable of int, optional + time or iterable of times. + If not given the gabriel graph will be calculated for all timepoints. Returns ------- @@ -73,53 +74,56 @@ def get_gabriel_graph( if not hasattr(lT, "Gabriel_graph"): lT.Gabriel_graph = {} - if t and len(lT.time_nodes[t]) < 5: - warn("Need more than 5 timepoints") - return lT.Gabriel_graph - - if t is None: - with catch_warnings(): - simplefilter("ignore") - for time in lT.time_nodes: - get_gabriel_graph(lT, time) - - elif lT.time_nodes[t] - lT.Gabriel_graph.keys(): - _, nodes = lT.get_idx3d(t) - - data_corres = {} - data = [] - for i, C in enumerate(nodes): - data.append(lT.pos[C]) - data_corres[i] = C - - tmp = Delaunay(data) - - delaunay_graph = {} - - for N in tmp.simplices: - for e1, e2 in combinations(np.sort(N), 2): - delaunay_graph.setdefault(e1, set()).add(e2) - delaunay_graph.setdefault(e2, set()).add(e1) - - Gabriel_graph = {} - - for e1, neighbs in delaunay_graph.items(): - for ni in neighbs: - if not any( - np.linalg.norm((data[ni] + data[e1]) / 2 - data[i]) - < np.linalg.norm(data[ni] - data[e1]) / 2 - for i in delaunay_graph[e1].intersection( - delaunay_graph[ni] - ) - ): - Gabriel_graph.setdefault(data_corres[e1], set()).add( - data_corres[ni] - ) - Gabriel_graph.setdefault(data_corres[ni], set()).add( - data_corres[e1] - ) - - lT.Gabriel_graph.update(Gabriel_graph) + if time is None: + time = lT.time_nodes.keys() + elif not isinstance(time, Iterable): + time = [time] + + for t in time: + if lT.time_nodes[t] - lT.Gabriel_graph.keys(): + nodes = np.fromiter(list(lT.time_nodes[t]), dtype=int) + + data_corres = {} + data = [] + for i, C in enumerate(nodes): + data.append(lT.pos[C]) + data_corres[i] = C + + delaunay_graph = {} + + # The delaunay triangulation is only usefult to compute + # when the number of points is higher than the spatial dimension + 1 + if len(data[0]) + 1 < len(data): + tmp = Delaunay(data) + for N in tmp.simplices: + for e1, e2 in combinations(np.sort(N), 2): + delaunay_graph.setdefault(e1, set()).add(e2) + delaunay_graph.setdefault(e2, set()).add(e1) + # When there are fewer nodes than the number of dimensions + 2 + # The Delaunay is the complete graph + else: + for e1, e2 in combinations(data_corres, 2): + delaunay_graph.setdefault(e1, set()).add(e2) + delaunay_graph.setdefault(e2, set()).add(e1) + + Gabriel_graph = {} + + for e1, neighbs in delaunay_graph.items(): + for ni in neighbs: + if not any( + np.linalg.norm((data[ni] + data[e1]) / 2 - data[i]) + < np.linalg.norm(data[ni] - data[e1]) / 2 + for i in delaunay_graph[e1].intersection( + delaunay_graph[ni] + ) + ): + Gabriel_graph.setdefault(data_corres[e1], set()).add( + data_corres[ni] + ) + Gabriel_graph.setdefault(data_corres[ni], set()).add( + data_corres[e1] + ) + lT.Gabriel_graph.update(Gabriel_graph) return lT.Gabriel_graph From 17920ffbf527a132363abc5f8701b9c42e4f044f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20Guignard?= Date: Wed, 7 Jan 2026 16:51:05 +0100 Subject: [PATCH 3/3] bump version 3.0.4 -> 3.1.0 --- pyproject.toml | 4 ++-- src/lineagetree/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 470af73..4ed603d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ maintainers = [ ] name = "lineagetree" description = "Structure for Lineage Trees" -version = "3.0.4" +version = "3.1.0" license = "MIT" license-files = [ "LICENSE" ] readme = {file = "README.md", content-type = "text/markdown"} @@ -76,7 +76,7 @@ profile = "black" line_length = 79 [tool.bumpver] -current_version = "3.0.4" +current_version = "3.1.0" version_pattern = "MAJOR.MINOR.PATCH[-TAG]" commit_message = "bump version {old_version} -> {new_version}" commit = true diff --git a/src/lineagetree/__init__.py b/src/lineagetree/__init__.py index 8c53c0e..b72ddcd 100644 --- a/src/lineagetree/__init__.py +++ b/src/lineagetree/__init__.py @@ -1,4 +1,4 @@ -__version__ = "3.0.4" +__version__ = "3.1.0" from .lineage_tree import LineageTree from ._io._loaders import ( read_from_ASTEC,