From 7ba43be165df441ac5eb3fba98d2e81db0c05180 Mon Sep 17 00:00:00 2001 From: Benjamin Gallusser Date: Fri, 22 Sep 2023 18:07:43 +0200 Subject: [PATCH 1/5] Speed up CTC edge errors When calculating edge errors, there is a 1-to-1 mapping between computed graph nodes and GT graph nodes, see details below. It is faster to use a dictionary for doing the node mapping with O(n log(n)) runtime, compared to repeatedly iterating over a list with np.where (O(n^2)). Details: Potential 1-to-many matches, namely computed nodes that match multiple GT nodes (called non-split), are not part of the induced graph. Therefore, all edges incident to such nodes are also not part of the induced graph, leaving us with the desired 1-to-1 mapping for all nodes that are incident to existing edges in the induced graph, which we iterate over in the loop for finding FP edges. Conversely, each GT node is only matched to at most one computed node, directly yielding the 1-1 matching for finding FN edges. --- src/traccuracy/track_errors/_ctc.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/traccuracy/track_errors/_ctc.py b/src/traccuracy/track_errors/_ctc.py index 7acba9b0..f80449be 100644 --- a/src/traccuracy/track_errors/_ctc.py +++ b/src/traccuracy/track_errors/_ctc.py @@ -2,7 +2,6 @@ from collections import defaultdict from typing import TYPE_CHECKING -import numpy as np from tqdm import tqdm from traccuracy import EdgeAttr, NodeAttr @@ -91,15 +90,15 @@ def get_edge_errors(matched_data: "Matched"): ) gt_graph.set_edge_attribute(list(gt_graph.edges()), EdgeAttr.FALSE_NEG, False) - node_mapping_first = np.array([mp[0] for mp in node_mapping]) - node_mapping_second = np.array([mp[1] for mp in node_mapping]) + node_mapping_first = {mp[0]: mp[1] for mp in node_mapping} + node_mapping_second = {mp[1]: mp[0] for mp in node_mapping} # fp edges - edges in induced_graph that aren't in gt_graph for edge in tqdm(induced_graph.edges, "Evaluating FP edges"): source, target = edge[0], edge[1] - source_gt_id = node_mapping[np.where(node_mapping_second == source)[0][0]][0] - target_gt_id = node_mapping[np.where(node_mapping_second == target)[0][0]][0] + source_gt_id = node_mapping_second[source] + target_gt_id = node_mapping_second[target] expected_gt_edge = (source_gt_id, target_gt_id) if expected_gt_edge not in gt_graph.edges(): @@ -124,8 +123,8 @@ def get_edge_errors(matched_data: "Matched"): gt_graph.set_edge_attribute(edge, EdgeAttr.FALSE_NEG, True) continue - source_comp_id = node_mapping[np.where(node_mapping_first == source)[0][0]][1] - target_comp_id = node_mapping[np.where(node_mapping_first == target)[0][0]][1] + source_comp_id = node_mapping_first[source] + target_comp_id = node_mapping_first[target] expected_comp_edge = (source_comp_id, target_comp_id) if expected_comp_edge not in induced_graph.edges: From ad520845a6ba5d6bde72bb7d0acee84fb394781d Mon Sep 17 00:00:00 2001 From: Benjamin Gallusser Date: Mon, 25 Sep 2023 11:04:39 +0200 Subject: [PATCH 2/5] Remove TrackingGraph nodes/edges type cast --- src/traccuracy/_tracking_graph.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/traccuracy/_tracking_graph.py b/src/traccuracy/_tracking_graph.py index 62701b68..7ea977b9 100644 --- a/src/traccuracy/_tracking_graph.py +++ b/src/traccuracy/_tracking_graph.py @@ -183,11 +183,12 @@ def nodes(self, limit_to=None): Returns: dict[hashable, dict]: A dictionary from node ids to node attributes """ - nodes = self.graph.nodes.items() if limit_to is None: - return dict(nodes) + return self.graph.nodes else: - limited_nodes = {_id: data for _id, data in nodes if _id in limit_to} + limited_nodes = { + _id: data for _id, data in self.graph.nodes if _id in limit_to + } return limited_nodes def edges(self, limit_to=None): @@ -201,11 +202,12 @@ def edges(self, limit_to=None): Returns: dict[tuple[hashable], dict]: A dictionary from edge ids to edge attributes """ - edges = self.graph.edges.items() if limit_to is None: - return dict(edges) + return self.graph.edges else: - limited_edges = {_id: data for _id, data in edges if _id in limit_to} + limited_edges = { + _id: data for _id, data in self.graph.edges if _id in limit_to + } return limited_edges def get_nodes_in_frame(self, frame): From 2c7a8813083c93ea2dca6bd0a629a7b9b598a5fe Mon Sep 17 00:00:00 2001 From: Benjamin Gallusser Date: Mon, 25 Sep 2023 11:37:04 +0200 Subject: [PATCH 3/5] Improve CTC edge errors clarity --- src/traccuracy/track_errors/_ctc.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/traccuracy/track_errors/_ctc.py b/src/traccuracy/track_errors/_ctc.py index f80449be..a211dc15 100644 --- a/src/traccuracy/track_errors/_ctc.py +++ b/src/traccuracy/track_errors/_ctc.py @@ -90,8 +90,12 @@ def get_edge_errors(matched_data: "Matched"): ) gt_graph.set_edge_attribute(list(gt_graph.edges()), EdgeAttr.FALSE_NEG, False) - node_mapping_first = {mp[0]: mp[1] for mp in node_mapping} - node_mapping_second = {mp[1]: mp[0] for mp in node_mapping} + node_mapping_first = { + gt: comp for gt, comp in node_mapping if comp in induced_graph + } + node_mapping_second = { + comp: gt for gt, comp in node_mapping if comp in induced_graph + } # fp edges - edges in induced_graph that aren't in gt_graph for edge in tqdm(induced_graph.edges, "Evaluating FP edges"): From 7e72be379e50e52cf3bf33b6b90d59d5d1c33f2b Mon Sep 17 00:00:00 2001 From: Benjamin Gallusser Date: Thu, 28 Sep 2023 14:55:25 +0200 Subject: [PATCH 4/5] Standardize TrackingGraph nodes/edges output dtype --- src/traccuracy/_tracking_graph.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/traccuracy/_tracking_graph.py b/src/traccuracy/_tracking_graph.py index 7ea977b9..9af082b0 100644 --- a/src/traccuracy/_tracking_graph.py +++ b/src/traccuracy/_tracking_graph.py @@ -181,15 +181,15 @@ def nodes(self, limit_to=None): Will raise KeyError if any of these node_ids are not present. Returns: - dict[hashable, dict]: A dictionary from node ids to node attributes + NodeView: Provides set-like operations on the nodes as well as node attribute lookup. """ if limit_to is None: return self.graph.nodes else: - limited_nodes = { - _id: data for _id, data in self.graph.nodes if _id in limit_to - } - return limited_nodes + for node in limit_to: + if not self.graph.has_node(node): + raise KeyError(f"Queried node {node} not present in graph.") + return self.graph.subgraph(limit_to).nodes def edges(self, limit_to=None): """Get all the edges in the graph, along with their attributes. @@ -200,15 +200,16 @@ def edges(self, limit_to=None): Will raise KeyError if any of these edge ids are not present. Returns: - dict[tuple[hashable], dict]: A dictionary from edge ids to edge attributes + OutEdgeView: Provides set-like operations on the edge-tuples as well as edge attribute + lookup. """ if limit_to is None: return self.graph.edges else: - limited_edges = { - _id: data for _id, data in self.graph.edges if _id in limit_to - } - return limited_edges + for edge in limit_to: + if not self.graph.has_edge(*edge): + raise KeyError(f"Queried edge {edge} not present in graph.") + return self.graph.edge_subgraph(limit_to).edges def get_nodes_in_frame(self, frame): """Get the node ids of all nodes in the given frame. From b7cd3c5fde8a1bcf12d5b62b325b427c4e7d4c66 Mon Sep 17 00:00:00 2001 From: Benjamin Gallusser Date: Thu, 28 Sep 2023 15:04:26 +0200 Subject: [PATCH 5/5] Improve naming in CTC edge errors --- src/traccuracy/track_errors/_ctc.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/traccuracy/track_errors/_ctc.py b/src/traccuracy/track_errors/_ctc.py index a211dc15..f1bf3a38 100644 --- a/src/traccuracy/track_errors/_ctc.py +++ b/src/traccuracy/track_errors/_ctc.py @@ -90,19 +90,15 @@ def get_edge_errors(matched_data: "Matched"): ) gt_graph.set_edge_attribute(list(gt_graph.edges()), EdgeAttr.FALSE_NEG, False) - node_mapping_first = { - gt: comp for gt, comp in node_mapping if comp in induced_graph - } - node_mapping_second = { - comp: gt for gt, comp in node_mapping if comp in induced_graph - } + gt_comp_mapping = {gt: comp for gt, comp in node_mapping if comp in induced_graph} + comp_gt_mapping = {comp: gt for gt, comp in node_mapping if comp in induced_graph} # fp edges - edges in induced_graph that aren't in gt_graph for edge in tqdm(induced_graph.edges, "Evaluating FP edges"): source, target = edge[0], edge[1] - source_gt_id = node_mapping_second[source] - target_gt_id = node_mapping_second[target] + source_gt_id = comp_gt_mapping[source] + target_gt_id = comp_gt_mapping[target] expected_gt_edge = (source_gt_id, target_gt_id) if expected_gt_edge not in gt_graph.edges(): @@ -127,8 +123,8 @@ def get_edge_errors(matched_data: "Matched"): gt_graph.set_edge_attribute(edge, EdgeAttr.FALSE_NEG, True) continue - source_comp_id = node_mapping_first[source] - target_comp_id = node_mapping_first[target] + source_comp_id = gt_comp_mapping[source] + target_comp_id = gt_comp_mapping[target] expected_comp_edge = (source_comp_id, target_comp_id) if expected_comp_edge not in induced_graph.edges: