From 2b7359dbe3172eafde0696e586e51058dda34eda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20Guignard?= Date: Fri, 25 Jul 2025 14:38:08 +0200 Subject: [PATCH 1/5] change version handling --- src/LineageTree/lineageTree.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/LineageTree/lineageTree.py b/src/LineageTree/lineageTree.py index 01a3d26..f9523a9 100644 --- a/src/LineageTree/lineageTree.py +++ b/src/LineageTree/lineageTree.py @@ -29,6 +29,7 @@ from scipy.sparse import dok_array from scipy.spatial import Delaunay, KDTree, distance +from . import __version__ from .tree_approximation import TreeApproximationTemplate, tree_style from .utils import ( convert_style_to_number, @@ -3409,7 +3410,7 @@ def __init__( Supported keyword arguments are dictionaries assigning nodes to any custom property. The property must be specified for every node, and named differently from lineageTree's own attributes. """ - self.__version__ = importlib.metadata.version("LineageTree") + self.__version__ = __version__ self.name = str(name) if name is not None else None if successor is not None and predecessor is not None: raise ValueError( From faaec7f3aeb1837dc9055f3b4812a5f86e953f22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20Guignard?= Date: Fri, 25 Jul 2025 14:38:39 +0200 Subject: [PATCH 2/5] bump version 2.0.2 -> 2.0.3 --- pyproject.toml | 4 ++-- src/LineageTree/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8c868fa..10fdd82 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ maintainers = [ ] name = "LineageTree" description = "Structure for Lineage Trees" -version = "2.0.2" +version = "2.0.3" 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 = "2.0.2" +current_version = "2.0.3" 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 052fdb7..755b647 100644 --- a/src/LineageTree/__init__.py +++ b/src/LineageTree/__init__.py @@ -1,4 +1,4 @@ -__version__ = "2.0.2" +__version__ = "2.0.3" from .lineageTree import lineageTree from .lineageTreeManager import lineageTreeManager from .loaders import ( From 78374052232d46b483633775a0fdafd5f147d94b Mon Sep 17 00:00:00 2001 From: jules-vanaret Date: Mon, 25 Aug 2025 22:07:02 +0200 Subject: [PATCH 3/5] implemented uniform tree --- src/LineageTree/utils.py | 98 ++++++++++++++++++++++++++++------------ 1 file changed, 68 insertions(+), 30 deletions(-) diff --git a/src/LineageTree/utils.py b/src/LineageTree/utils.py index 7928398..ceb39ba 100644 --- a/src/LineageTree/utils.py +++ b/src/LineageTree/utils.py @@ -60,7 +60,7 @@ def create_links_and_chains( def hierarchical_pos( lnks_tms: dict, root, width=1000, vert_gap=2, xcenter=0, ycenter=0 ) -> dict[int, list[float]] | None: - """Calculates the position of each node on the tree graph. + """Calculates the position of each node on the tree graph with uniform leaf spacing. Parameters ---------- @@ -81,42 +81,80 @@ def hierarchical_pos( ------- dict mapping int to list of float Provides a dictionary that contains the id of each node as keys and its 2-d position on the - tree graph as values. + tree graph as values. Leaves are uniformly spaced on the x-axis. If the root requested does not exists, None is then returned """ - to_do = [root] if root not in lnks_tms["times"]: return None - pos_node = {root: [xcenter, ycenter]} - prev_width = {root: width / 2} - while to_do: - curr = to_do.pop() - succ = lnks_tms["links"].get(curr, []) - if len(succ) == 0: - continue - elif len(succ) == 1: - pos_node[succ[0]] = [ - pos_node[curr][0], - pos_node[curr][1] - - lnks_tms["times"].get(curr, 0) - + min(vert_gap, lnks_tms["times"].get(curr, 0)), + + # First pass: find all leaves and calculate y-positions + def find_leaves_and_depths(node, current_depth=0): + """Find all leaves and calculate depths for all nodes.""" + succ = lnks_tms["links"].get(node, []) + node_depth = current_depth + lnks_tms["times"].get(node, 0) + + if not succ: # This is a leaf + return [node], {node: node_depth} + + all_leaves = [] + all_depths = {node: current_depth} + + for child in succ: + child_leaves, child_depths = find_leaves_and_depths(child, node_depth) + all_leaves.extend(child_leaves) + all_depths.update(child_depths) + + return all_leaves, all_depths + + leaves, depths = find_leaves_and_depths(root) + + # Calculate uniform x-positions for leaves + num_leaves = len(leaves) + if num_leaves == 1: + leaf_spacing = 0 + leaf_x_positions = {leaves[0]: xcenter} + else: + leaf_spacing = width / (num_leaves - 1) + leaf_x_positions = { + leaf: xcenter - width/2 + i * leaf_spacing + for i, leaf in enumerate(leaves) + } + + # Second pass: assign positions bottom-up + pos_node = {} + + def assign_positions(node): + """Assign positions working from leaves up to root.""" + succ = lnks_tms["links"].get(node, []) + + if not succ: # This is a leaf + pos_node[node] = [ + leaf_x_positions[node], + ycenter - depths[node] * vert_gap ] - to_do.extend(succ) - prev_width[succ[0]] = prev_width[curr] - elif len(succ) == 2: - pos_node[succ[0]] = [ - pos_node[curr][0] - prev_width[curr] / 2, - pos_node[curr][1] - vert_gap, + return + + # First assign positions to all children + for child in succ: + assign_positions(child) + + # Position this node based on its children + if len(succ) == 1: + # Single child: place directly above + pos_node[node] = [ + pos_node[succ[0]][0], + ycenter - depths[node] * vert_gap ] - pos_node[succ[1]] = [ - pos_node[curr][0] + prev_width[curr] / 2, - pos_node[curr][1] - vert_gap, + else: + # Multiple children: place at the center of children + child_x_positions = [pos_node[child][0] for child in succ] + center_x = sum(child_x_positions) / len(child_x_positions) + pos_node[node] = [ + center_x, + ycenter - depths[node] * vert_gap ] - to_do.extend(succ) - prev_width[succ[0]], prev_width[succ[1]] = ( - prev_width[curr] / 2, - prev_width[curr] / 2, - ) + + assign_positions(root) return pos_node From 3058a9729b29f95f2f3b6f3713cc11c9a9b3f911 Mon Sep 17 00:00:00 2001 From: jules-vanaret Date: Tue, 26 Aug 2025 12:14:36 +0200 Subject: [PATCH 4/5] implemented non-recursive version --- src/LineageTree/utils.py | 184 +++++++++++++++++++++++++-------------- 1 file changed, 120 insertions(+), 64 deletions(-) diff --git a/src/LineageTree/utils.py b/src/LineageTree/utils.py index ceb39ba..0e04098 100644 --- a/src/LineageTree/utils.py +++ b/src/LineageTree/utils.py @@ -57,6 +57,119 @@ def create_links_and_chains( return {"links": links, "times": times, "root": roots} +def _find_leaves_and_depths_iterative(lnks_tms: dict, root: int) -> tuple[list[int], dict[int, int]]: + """Find all leaves and calculate depths for all nodes using iterative approach.""" + leaves = [] + depths = {} + + # Stack for DFS: (node, current_depth, parent_depth) + stack = [(root, 0, 0)] + visited = set() + + while stack: + node, current_depth, parent_depth = stack.pop() + + if node in visited: + continue + visited.add(node) + + node_depth = parent_depth + lnks_tms["times"].get(node, 0) + depths[node] = parent_depth + + succ = lnks_tms["links"].get(node, []) + + if not succ: # This is a leaf + leaves.append(node) + else: + # Add children to stack (reverse order to maintain left-to-right traversal) + for child in reversed(succ): + stack.append((child, current_depth + 1, node_depth)) + + return leaves, depths + + +def _calculate_leaf_positions(leaves: list[int], width: int, xcenter: int) -> dict[int, float]: + """Calculate uniform x-positions for leaves.""" + num_leaves = len(leaves) + if num_leaves == 1: + return {leaves[0]: xcenter} + + leaf_spacing = width / (num_leaves - 1) + return { + leaf: xcenter - width/2 + i * leaf_spacing + for i, leaf in enumerate(leaves) + } + + +def _assign_positions_iterative( + lnks_tms: dict, + root: int, + depths: dict[int, int], + leaf_x_positions: dict[int, float], + vert_gap: int, + ycenter: int +) -> dict[int, list[float]]: + """Assign positions to nodes using iterative post-order traversal.""" + pos_node = {} + + # First pass: build parent-child relationships and find processing order + children_map = {} + all_nodes = set() + stack = [root] + + while stack: + node = stack.pop() + if node in all_nodes: + continue + all_nodes.add(node) + + succ = lnks_tms["links"].get(node, []) + children_map[node] = succ + + for child in succ: + stack.append(child) + + # Post-order traversal using two stacks + stack1 = [root] + stack2 = [] + + while stack1: + node = stack1.pop() + stack2.append(node) + + for child in children_map.get(node, []): + stack1.append(child) + + # Process nodes in post-order (children before parents) + while stack2: + node = stack2.pop() + succ = children_map.get(node, []) + + if not succ: # This is a leaf + pos_node[node] = [ + leaf_x_positions[node], + ycenter - depths[node] * vert_gap + ] + else: + # Position based on children + if len(succ) == 1: + # Single child: place directly above + pos_node[node] = [ + pos_node[succ[0]][0], + ycenter - depths[node] * vert_gap + ] + else: + # Multiple children: place at center of children + child_x_positions = [pos_node[child][0] for child in succ] + center_x = sum(child_x_positions) / len(child_x_positions) + pos_node[node] = [ + center_x, + ycenter - depths[node] * vert_gap + ] + + return pos_node + + def hierarchical_pos( lnks_tms: dict, root, width=1000, vert_gap=2, xcenter=0, ycenter=0 ) -> dict[int, list[float]] | None: @@ -87,74 +200,17 @@ def hierarchical_pos( if root not in lnks_tms["times"]: return None - # First pass: find all leaves and calculate y-positions - def find_leaves_and_depths(node, current_depth=0): - """Find all leaves and calculate depths for all nodes.""" - succ = lnks_tms["links"].get(node, []) - node_depth = current_depth + lnks_tms["times"].get(node, 0) - - if not succ: # This is a leaf - return [node], {node: node_depth} - - all_leaves = [] - all_depths = {node: current_depth} - - for child in succ: - child_leaves, child_depths = find_leaves_and_depths(child, node_depth) - all_leaves.extend(child_leaves) - all_depths.update(child_depths) - - return all_leaves, all_depths - - leaves, depths = find_leaves_and_depths(root) + # Find all leaves and calculate depths + leaves, depths = _find_leaves_and_depths_iterative(lnks_tms, root) # Calculate uniform x-positions for leaves - num_leaves = len(leaves) - if num_leaves == 1: - leaf_spacing = 0 - leaf_x_positions = {leaves[0]: xcenter} - else: - leaf_spacing = width / (num_leaves - 1) - leaf_x_positions = { - leaf: xcenter - width/2 + i * leaf_spacing - for i, leaf in enumerate(leaves) - } - - # Second pass: assign positions bottom-up - pos_node = {} + leaf_x_positions = _calculate_leaf_positions(leaves, width, xcenter) - def assign_positions(node): - """Assign positions working from leaves up to root.""" - succ = lnks_tms["links"].get(node, []) - - if not succ: # This is a leaf - pos_node[node] = [ - leaf_x_positions[node], - ycenter - depths[node] * vert_gap - ] - return - - # First assign positions to all children - for child in succ: - assign_positions(child) - - # Position this node based on its children - if len(succ) == 1: - # Single child: place directly above - pos_node[node] = [ - pos_node[succ[0]][0], - ycenter - depths[node] * vert_gap - ] - else: - # Multiple children: place at the center of children - child_x_positions = [pos_node[child][0] for child in succ] - center_x = sum(child_x_positions) / len(child_x_positions) - pos_node[node] = [ - center_x, - ycenter - depths[node] * vert_gap - ] + # Assign positions using iterative approach + pos_node = _assign_positions_iterative( + lnks_tms, root, depths, leaf_x_positions, vert_gap, ycenter + ) - assign_positions(root) return pos_node From 366d0e0994a46b8a65fcfc367f513b7757ccf301 Mon Sep 17 00:00:00 2001 From: jules-vanaret Date: Wed, 27 Aug 2025 15:19:55 +0200 Subject: [PATCH 5/5] added comments and removed redundancies --- src/lineagetree/_core/utils.py | 87 ++++++++++++++++------------------ 1 file changed, 40 insertions(+), 47 deletions(-) diff --git a/src/lineagetree/_core/utils.py b/src/lineagetree/_core/utils.py index d692e31..cb7510c 100644 --- a/src/lineagetree/_core/utils.py +++ b/src/lineagetree/_core/utils.py @@ -65,21 +65,31 @@ def create_links_and_chains( def _find_leaves_and_depths_iterative(lnks_tms: dict, root: int) -> tuple[list[int], dict[int, int]]: - """Find all leaves and calculate depths for all nodes using iterative approach.""" + """Find all leaves and calculate depths for all nodes using iterative approach. + + Parameters + ---------- + lnks_tms : dict + A dictionary created by create_links_and_chains. + root : int + The id of the root node. + + Returns + ------- + leaves : list of int + List of leaf node ids. + depths : dict mapping int to int + Dictionary mapping node ids to their depth in the tree. + """ leaves = [] depths = {} # Stack for DFS: (node, current_depth, parent_depth) - stack = [(root, 0, 0)] - visited = set() + stack = [(root, 0)] while stack: - node, current_depth, parent_depth = stack.pop() - - if node in visited: - continue - visited.add(node) - + node, parent_depth = stack.pop() + node_depth = parent_depth + lnks_tms["times"].get(node, 0) depths[node] = parent_depth @@ -90,7 +100,7 @@ def _find_leaves_and_depths_iterative(lnks_tms: dict, root: int) -> tuple[list[i else: # Add children to stack (reverse order to maintain left-to-right traversal) for child in reversed(succ): - stack.append((child, current_depth + 1, node_depth)) + stack.append((child, node_depth)) return leaves, depths @@ -120,59 +130,42 @@ def _assign_positions_iterative( pos_node = {} # First pass: build parent-child relationships and find processing order - children_map = {} - all_nodes = set() - stack = [root] - - while stack: - node = stack.pop() - if node in all_nodes: - continue - all_nodes.add(node) - - succ = lnks_tms["links"].get(node, []) - children_map[node] = succ - - for child in succ: - stack.append(child) - - # Post-order traversal using two stacks + children_map = lnks_tms["links"] + + # Reverse-order traversal using two stacks stack1 = [root] stack2 = [] + # This while loop stores nodes in stack2 so that children are processed before parents while stack1: node = stack1.pop() stack2.append(node) - - for child in children_map.get(node, []): - stack1.append(child) + stack1.extend(children_map.get(node, [])) - # Process nodes in post-order (children before parents) + # Process nodes in reverse-order (children before parents) while stack2: node = stack2.pop() succ = children_map.get(node, []) - if not succ: # This is a leaf + if not succ: # This is a leaf pos_node[node] = [ leaf_x_positions[node], ycenter - depths[node] * vert_gap ] + elif len(succ) == 1: + # Single child: place directly above + pos_node[node] = [ + pos_node[succ[0]][0], + ycenter - depths[node] * vert_gap + ] else: - # Position based on children - if len(succ) == 1: - # Single child: place directly above - pos_node[node] = [ - pos_node[succ[0]][0], - ycenter - depths[node] * vert_gap - ] - else: - # Multiple children: place at center of children - child_x_positions = [pos_node[child][0] for child in succ] - center_x = sum(child_x_positions) / len(child_x_positions) - pos_node[node] = [ - center_x, - ycenter - depths[node] * vert_gap - ] + # Multiple children: place at center of children + child_x_positions = [pos_node[child][0] for child in succ] + center_x = sum(child_x_positions) / len(child_x_positions) + pos_node[node] = [ + center_x, + ycenter - depths[node] * vert_gap + ] return pos_node