From cb62a2d506257160a1091ba84b98afe355d0bb93 Mon Sep 17 00:00:00 2001 From: eberrigan Date: Tue, 3 Sep 2024 15:21:32 -0700 Subject: [PATCH 01/18] comment in `to_json` to start --- sleap/skeleton.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sleap/skeleton.py b/sleap/skeleton.py index eca393b8e..3e58bf9e8 100644 --- a/sleap/skeleton.py +++ b/sleap/skeleton.py @@ -999,6 +999,7 @@ def to_json(self, node_to_idx: Optional[Dict[Node, int]] = None) -> str: Returns: A string containing the JSON representation of the skeleton. """ + # TODO: Replace jsonpickle with a custom encoder from sleap-io. jsonpickle.set_encoder_options("simplejson", sort_keys=True, indent=4) if node_to_idx is not None: indexed_node_graph = nx.relabel_nodes( From 4c03ca2b46ced2a381c7700d7c42307e1bd3511c Mon Sep 17 00:00:00 2001 From: eberrigan Date: Tue, 3 Sep 2024 16:11:07 -0700 Subject: [PATCH 02/18] fix confusing typo --- sleap/skeleton.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sleap/skeleton.py b/sleap/skeleton.py index 3e58bf9e8..adbcd01e3 100644 --- a/sleap/skeleton.py +++ b/sleap/skeleton.py @@ -987,7 +987,7 @@ def to_json(self, node_to_idx: Optional[Dict[Node, int]] = None) -> str: """Convert the :class:`Skeleton` to a JSON representation. Args: - node_to_idx: optional dict which maps :class:`Node`sto index + node_to_idx: optional dict which maps :class:`Nodes`to index in some list. This is used when saving :class:`Labels`where we want to serialize the :class:`Nodes` outside the :class:`Skeleton` object. From f352d03420a9b15997df1e3fe7f43e1b42885b5b Mon Sep 17 00:00:00 2001 From: eberrigan Date: Tue, 3 Sep 2024 17:00:11 -0700 Subject: [PATCH 03/18] add logic from sleap-io `serialize_skeletons` to `Skeleton.to_json` and use `json.dumps` instead of `jsonpickle` --- sleap/skeleton.py | 126 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 108 insertions(+), 18 deletions(-) diff --git a/sleap/skeleton.py b/sleap/skeleton.py index adbcd01e3..c9fc3f0ee 100644 --- a/sleap/skeleton.py +++ b/sleap/skeleton.py @@ -1000,7 +1000,7 @@ def to_json(self, node_to_idx: Optional[Dict[Node, int]] = None) -> str: A string containing the JSON representation of the skeleton. """ # TODO: Replace jsonpickle with a custom encoder from sleap-io. - jsonpickle.set_encoder_options("simplejson", sort_keys=True, indent=4) + if node_to_idx is not None: indexed_node_graph = nx.relabel_nodes( G=self._graph, mapping=node_to_idx @@ -1008,25 +1008,115 @@ def to_json(self, node_to_idx: Optional[Dict[Node, int]] = None) -> str: else: indexed_node_graph = self._graph - # Encode to JSON - graph = json_graph.node_link_data(indexed_node_graph) - - # SLEAP v1.3.0 added `description` and `preview_image` to `Skeleton`, but saving - # these fields breaks data format compatibility. Currently, these are only - # added in our custom template skeletons. To ensure backwards data format - # compatibilty of user data, we only save these fields if they are not None. - if self.is_template: - data = { - "nx_graph": graph, - "description": self.description, - "preview_image": self.preview_image, - } - else: - data = graph + # Create a dictionary to store node data + # Taken from sleap-io: https://github.com/talmolab/sleap-io/blob/2bc3d5210c46bdb25413d25970c4bdc7adb6e8cc/sleap_io/io/slp.py#L633C1-L644C71 + nodes_dicts = [] + node_to_id = {} + for node in self.nodes: + if node not in node_to_id: + # Note: This ID is not the same as the node index in the skeleton in + # legacy SLEAP, but we do not retain this information in the labels, so + # IDs will be different. + # + # The weight is also kept fixed here, but technically this is not + # modified or used in legacy SLEAP either. + # + # TODO: Store legacy metadata in labels to get byte-level compatibility? + node_to_id[node] = len(node_to_id) + nodes_dicts.append({"name": node.name, "weight": 1.0}) + + # Create a dictionary to store edge data + # Taken from sleap-io: https://github.com/talmolab/sleap-io/blob/2bc3d5210c46bdb25413d25970c4bdc7adb6e8cc/sleap_io/io/slp.py#L649-L693 + # Build links dicts for normal edges. + edges_dicts = [] + for edge_ind, edge in enumerate(self.edges): + if edge_ind == 0: + edge_type = { + "py/reduce": [ + {"py/type": "sleap.skeleton.EdgeType"}, + {"py/tuple": [1]}, # 1 = real edge, 2 = symmetry edge + ] + } + else: + edge_type = {"py/id": 1} + + edges_dicts.append( + { + # Note: Insert idx is not the same as the edge index in the skeleton + # in legacy SLEAP. + "edge_insert_idx": edge_ind, + "key": 0, # Always 0. + "source": node_to_id[edge.source], + "target": node_to_id[edge.destination], + "type": edge_type, + } + ) - json_str = jsonpickle.encode(data) + # Build links dicts for symmetry edges. + for symmetry_ind, symmetry in enumerate(self.symmetries): + if symmetry_ind == 0: + edge_type = { + "py/reduce": [ + {"py/type": "sleap.skeleton.EdgeType"}, + {"py/tuple": [2]}, # 1 = real edge, 2 = symmetry edge + ] + } + else: + edge_type = {"py/id": 2} + + src, dst = tuple(symmetry.nodes) + edges_dicts.append( + { + "key": 0, + "source": node_to_id[src], + "target": node_to_id[dst], + "type": edge_type, + } + ) - return json_str + # Create skeleton dict. + # Taken from sleap-io: https://github.com/talmolab/sleap-io/blob/2bc3d5210c46bdb25413d25970c4bdc7adb6e8cc/sleap_io/io/slp.py#L695C1-L708C10 + skeleton_dicts = [] + skeleton_dicts.append( + { + "directed": True, + "graph": { + "name": self.name, + "num_edges_inserted": len(self.edges), + }, + "links": edges_dicts, + "multigraph": True, + # In the order in Skeleton.nodes and must match up with nodes_dicts. + "nodes": [{"id": node_to_id[node]} for node in self.nodes], + } + ) + # jsonpickle.set_encoder_options("simplejson", sort_keys=True, indent=4) + # if node_to_idx is not None: + # indexed_node_graph = nx.relabel_nodes( + # G=self._graph, mapping=node_to_idx + # ) # map nodes to int + # else: + # indexed_node_graph = self._graph + + # # Encode to JSON + # graph = json_graph.node_link_data(indexed_node_graph) + + # # SLEAP v1.3.0 added `description` and `preview_image` to `Skeleton`, but saving + # # these fields breaks data format compatibility. Currently, these are only + # # added in our custom template skeletons. To ensure backwards data format + # # compatibilty of user data, we only save these fields if they are not None. + # if self.is_template: + # data = { + # "nx_graph": graph, + # "description": self.description, + # "preview_image": self.preview_image, + # } + # else: + # data = graph + + # json_str = jsonpickle.encode(data) + + # return json_str def save_json(self, filename: str, node_to_idx: Optional[Dict[Node, int]] = None): """ From 94dccb0b4f6c4d0fd0252c24fe11266faaf56780 Mon Sep 17 00:00:00 2001 From: eberrigan Date: Tue, 3 Sep 2024 17:02:13 -0700 Subject: [PATCH 04/18] clean docstring --- sleap/skeleton.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sleap/skeleton.py b/sleap/skeleton.py index c9fc3f0ee..f7cb8747f 100644 --- a/sleap/skeleton.py +++ b/sleap/skeleton.py @@ -987,9 +987,9 @@ def to_json(self, node_to_idx: Optional[Dict[Node, int]] = None) -> str: """Convert the :class:`Skeleton` to a JSON representation. Args: - node_to_idx: optional dict which maps :class:`Nodes`to index + node_to_idx: optional dict which maps :class:`Nodes` to index in some list. This is used when saving - :class:`Labels`where we want to serialize the + :class:`Labels` where we want to serialize the :class:`Nodes` outside the :class:`Skeleton` object. If given, then we replace each :class:`Node` with specified index before converting :class:`Skeleton`. From 4febed0011273f0566324fec8ccd8cab84f9cf8b Mon Sep 17 00:00:00 2001 From: eberrigan Date: Tue, 3 Sep 2024 17:04:42 -0700 Subject: [PATCH 05/18] replace relabel nodes logic using networkx with custom indexing logic --- sleap/skeleton.py | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/sleap/skeleton.py b/sleap/skeleton.py index f7cb8747f..08457e299 100644 --- a/sleap/skeleton.py +++ b/sleap/skeleton.py @@ -999,35 +999,17 @@ def to_json(self, node_to_idx: Optional[Dict[Node, int]] = None) -> str: Returns: A string containing the JSON representation of the skeleton. """ - # TODO: Replace jsonpickle with a custom encoder from sleap-io. - - if node_to_idx is not None: - indexed_node_graph = nx.relabel_nodes( - G=self._graph, mapping=node_to_idx - ) # map nodes to int - else: - indexed_node_graph = self._graph - + # Logic taken from github.com/talmolab/sleap-io/io/slp.py::serialize_skeletons + # https://github.com/talmolab/sleap-io/blob/main/sleap_io/io/slp.py#L606 # Create a dictionary to store node data - # Taken from sleap-io: https://github.com/talmolab/sleap-io/blob/2bc3d5210c46bdb25413d25970c4bdc7adb6e8cc/sleap_io/io/slp.py#L633C1-L644C71 nodes_dicts = [] node_to_id = {} for node in self.nodes: if node not in node_to_id: - # Note: This ID is not the same as the node index in the skeleton in - # legacy SLEAP, but we do not retain this information in the labels, so - # IDs will be different. - # - # The weight is also kept fixed here, but technically this is not - # modified or used in legacy SLEAP either. - # - # TODO: Store legacy metadata in labels to get byte-level compatibility? - node_to_id[node] = len(node_to_id) + node_to_id[node] = node_to_idx[node] if node_to_idx is not None else len(node_to_id) nodes_dicts.append({"name": node.name, "weight": 1.0}) # Create a dictionary to store edge data - # Taken from sleap-io: https://github.com/talmolab/sleap-io/blob/2bc3d5210c46bdb25413d25970c4bdc7adb6e8cc/sleap_io/io/slp.py#L649-L693 - # Build links dicts for normal edges. edges_dicts = [] for edge_ind, edge in enumerate(self.edges): if edge_ind == 0: From c3448f4a4f974acf64d6bac19d5eddbddfe37b0e Mon Sep 17 00:00:00 2001 From: eberrigan Date: Tue, 3 Sep 2024 17:05:41 -0700 Subject: [PATCH 06/18] make dictionary directly instead of a list with a dictionary and save json string --- sleap/skeleton.py | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/sleap/skeleton.py b/sleap/skeleton.py index 08457e299..692dd6ad0 100644 --- a/sleap/skeleton.py +++ b/sleap/skeleton.py @@ -1024,8 +1024,6 @@ def to_json(self, node_to_idx: Optional[Dict[Node, int]] = None) -> str: edges_dicts.append( { - # Note: Insert idx is not the same as the edge index in the skeleton - # in legacy SLEAP. "edge_insert_idx": edge_ind, "key": 0, # Always 0. "source": node_to_id[edge.source], @@ -1057,21 +1055,25 @@ def to_json(self, node_to_idx: Optional[Dict[Node, int]] = None) -> str: ) # Create skeleton dict. - # Taken from sleap-io: https://github.com/talmolab/sleap-io/blob/2bc3d5210c46bdb25413d25970c4bdc7adb6e8cc/sleap_io/io/slp.py#L695C1-L708C10 - skeleton_dicts = [] - skeleton_dicts.append( - { - "directed": True, - "graph": { - "name": self.name, - "num_edges_inserted": len(self.edges), - }, - "links": edges_dicts, - "multigraph": True, - # In the order in Skeleton.nodes and must match up with nodes_dicts. - "nodes": [{"id": node_to_id[node]} for node in self.nodes], - } - ) + skeleton_dict = { + "directed": True, + "graph": { + "name": self.name, + "num_edges_inserted": len(self.edges), + }, + "links": edges_dicts, + "multigraph": True, + # In the order in Skeleton.nodes and must match up with nodes_dicts. + "nodes": [{"id": node_to_id[node]} for node in self.nodes], + } + + # Convert the skeleton dict to a JSON string using the standard json module + json_str = json.dumps(skeleton_dict, indent=4, sort_keys=True) + + return json_str + + + # jsonpickle.set_encoder_options("simplejson", sort_keys=True, indent=4) # if node_to_idx is not None: # indexed_node_graph = nx.relabel_nodes( From 3a7e65a973da6537a15f11d3d37cc727a0826bb0 Mon Sep 17 00:00:00 2001 From: eberrigan Date: Thu, 5 Sep 2024 16:16:52 -0700 Subject: [PATCH 07/18] no attributes edge.source and edge.destination in SLEAP --- sleap/skeleton.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/sleap/skeleton.py b/sleap/skeleton.py index 692dd6ad0..a9ef1d062 100644 --- a/sleap/skeleton.py +++ b/sleap/skeleton.py @@ -1005,13 +1005,17 @@ def to_json(self, node_to_idx: Optional[Dict[Node, int]] = None) -> str: nodes_dicts = [] node_to_id = {} for node in self.nodes: + print(f'node: {node}') if node not in node_to_id: node_to_id[node] = node_to_idx[node] if node_to_idx is not None else len(node_to_id) nodes_dicts.append({"name": node.name, "weight": 1.0}) + print(f'node_to_id: {node_to_id}') + print(f'nodes_dicts: {nodes_dicts}') # Create a dictionary to store edge data edges_dicts = [] for edge_ind, edge in enumerate(self.edges): + print(f'edge: {edge}') if edge_ind == 0: edge_type = { "py/reduce": [ @@ -1019,19 +1023,20 @@ def to_json(self, node_to_idx: Optional[Dict[Node, int]] = None) -> str: {"py/tuple": [1]}, # 1 = real edge, 2 = symmetry edge ] } + print(f'edge_type: {edge_type}') else: edge_type = {"py/id": 1} - + print(f'edge_type: {edge_type}') edges_dicts.append( { "edge_insert_idx": edge_ind, "key": 0, # Always 0. - "source": node_to_id[edge.source], - "target": node_to_id[edge.destination], + "source": node_to_id[edge[0]], + "target": node_to_id[edge[1]], "type": edge_type, } ) - + print(f'edges_dicts: {edges_dicts}') # Build links dicts for symmetry edges. for symmetry_ind, symmetry in enumerate(self.symmetries): if symmetry_ind == 0: From 5decb55a415b469814ce9109ab7765dfdd1d02a9 Mon Sep 17 00:00:00 2001 From: eberrigan Date: Tue, 10 Sep 2024 16:22:44 -0700 Subject: [PATCH 08/18] add to_json functions using sleap-io logic --- sleap/skeleton.py | 101 ++++++++++++++++++++++++++++++---------------- 1 file changed, 66 insertions(+), 35 deletions(-) diff --git a/sleap/skeleton.py b/sleap/skeleton.py index a9ef1d062..ec213c73b 100644 --- a/sleap/skeleton.py +++ b/sleap/skeleton.py @@ -987,9 +987,9 @@ def to_json(self, node_to_idx: Optional[Dict[Node, int]] = None) -> str: """Convert the :class:`Skeleton` to a JSON representation. Args: - node_to_idx: optional dict which maps :class:`Nodes` to index + node_to_idx: optional dict which maps :class:`Nodes`to index in some list. This is used when saving - :class:`Labels` where we want to serialize the + :class:`Labels`where we want to serialize the :class:`Nodes` outside the :class:`Skeleton` object. If given, then we replace each :class:`Node` with specified index before converting :class:`Skeleton`. @@ -999,22 +999,29 @@ def to_json(self, node_to_idx: Optional[Dict[Node, int]] = None) -> str: Returns: A string containing the JSON representation of the skeleton. """ - # Logic taken from github.com/talmolab/sleap-io/io/slp.py::serialize_skeletons - # https://github.com/talmolab/sleap-io/blob/main/sleap_io/io/slp.py#L606 - # Create a dictionary to store node data + # Create global list of nodes with all nodes from all skeletons. nodes_dicts = [] node_to_id = {} for node in self.nodes: - print(f'node: {node}') if node not in node_to_id: - node_to_id[node] = node_to_idx[node] if node_to_idx is not None else len(node_to_id) - nodes_dicts.append({"name": node.name, "weight": 1.0}) + print(f'node: {node}') + # Note: This ID is not the same as the node index in the skeleton in + # legacy SLEAP, but we do not retain this information in the labels, so + # IDs will be different. + # + # The weight is also kept fixed here, but technically this is not + # modified or used in legacy SLEAP either. + # + # TODO: Store legacy metadata in labels to get byte-level compatibility? + node_to_id[node] = len(node_to_id) print(f'node_to_id: {node_to_id}') - print(f'nodes_dicts: {nodes_dicts}') - - # Create a dictionary to store edge data + nodes_dicts.append({"name": node.name, "weight": 1.0}) + print(f'nodes_dicts: {nodes_dicts}') + + # Build links dicts for normal edges. edges_dicts = [] for edge_ind, edge in enumerate(self.edges): + print(f'edge_ind: {edge_ind}') print(f'edge: {edge}') if edge_ind == 0: edge_type = { @@ -1027,18 +1034,33 @@ def to_json(self, node_to_idx: Optional[Dict[Node, int]] = None) -> str: else: edge_type = {"py/id": 1} print(f'edge_type: {edge_type}') + + # Edges are stored as a list of tuples of nodes + # The source and target are the nodes in the tuple (edge) are the first and + # second nodes respectively + source = edge[0] + print(f'source: {source}') + print(f'node_to_id[source]: {node_to_id[source]}') + target = edge[1] + print(f'target: {target}') + print(f'node_to_id[target]: {node_to_id[target]}') edges_dicts.append( { + # Note: Insert idx is not the same as the edge index in the skeleton + # in legacy SLEAP. "edge_insert_idx": edge_ind, "key": 0, # Always 0. - "source": node_to_id[edge[0]], - "target": node_to_id[edge[1]], + "source": {"py/id": node_to_id[source]}, + "target": {"py/id": node_to_id[target]}, "type": edge_type, } ) - print(f'edges_dicts: {edges_dicts}') + print(f'edges_dicts: {edges_dicts}') + # Build links dicts for symmetry edges. for symmetry_ind, symmetry in enumerate(self.symmetries): + print(f'symmetry_ind: {symmetry_ind}') + print(f'symmetry: {symmetry}') if symmetry_ind == 0: edge_type = { "py/reduce": [ @@ -1050,35 +1072,44 @@ def to_json(self, node_to_idx: Optional[Dict[Node, int]] = None) -> str: edge_type = {"py/id": 2} src, dst = tuple(symmetry.nodes) + print(f'src: {src}') + print(f'dst: {dst}') edges_dicts.append( { "key": 0, - "source": node_to_id[src], - "target": node_to_id[dst], + "source": {"py/id": node_to_id[src]}, + "target": {"py/id": node_to_id[dst]}, "type": edge_type, } ) # Create skeleton dict. - skeleton_dict = { - "directed": True, - "graph": { - "name": self.name, - "num_edges_inserted": len(self.edges), - }, - "links": edges_dicts, - "multigraph": True, - # In the order in Skeleton.nodes and must match up with nodes_dicts. - "nodes": [{"id": node_to_id[node]} for node in self.nodes], - } - - # Convert the skeleton dict to a JSON string using the standard json module - json_str = json.dumps(skeleton_dict, indent=4, sort_keys=True) - - return json_str - + if self.is_template: + skeleton_dict = { + "directed": True, + "graph": { + "name": self.name, + "num_edges_inserted": len(self.edges), + }, + "links": edges_dicts, + "multigraph": True, + # In the order in Skeleton.nodes and must match up with nodes_dicts. + "nodes": [{"id": {"py/id": node_to_id[node]}} for node in self.nodes], + "description": self.description, + "preview_image": self.preview_image, + } + else: + skeleton_dict ={ + "directed": True, + "graph": { + "name": self.name, + "num_edges_inserted": len(self.edges), + }, + "links": edges_dicts, + "multigraph": True, + # In the order in Skeleton.nodes and must match up with nodes_dicts. + "nodes": [{"id": {"py/id": node_to_id[node]}} for node in self.nodes],} - # jsonpickle.set_encoder_options("simplejson", sort_keys=True, indent=4) # if node_to_idx is not None: # indexed_node_graph = nx.relabel_nodes( @@ -1360,4 +1391,4 @@ def __hash__(self): cattr.register_unstructure_hook(Skeleton, lambda skeleton: Skeleton.to_dict(skeleton)) -cattr.register_structure_hook(Skeleton, lambda dicts, cls: Skeleton.from_dict(dicts)) +cattr.register_structure_hook(Skeleton, lambda dicts, cls: Skeleton.from_dict(dicts)) \ No newline at end of file From 622853cc6cf770e61be41dd71a8e7857b28063fe Mon Sep 17 00:00:00 2001 From: eberrigan Date: Wed, 11 Sep 2024 13:41:53 -0700 Subject: [PATCH 09/18] save changes to serialize skeletons --- sleap/skeleton.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/sleap/skeleton.py b/sleap/skeleton.py index ec213c73b..d236be492 100644 --- a/sleap/skeleton.py +++ b/sleap/skeleton.py @@ -1082,15 +1082,16 @@ def to_json(self, node_to_idx: Optional[Dict[Node, int]] = None) -> str: "type": edge_type, } ) - + # Create graph field + graph = { + "name": self.name, + "num_edges_inserted": len(self.edges), + } # Create skeleton dict. if self.is_template: skeleton_dict = { "directed": True, - "graph": { - "name": self.name, - "num_edges_inserted": len(self.edges), - }, + "nx_graph": graph, "links": edges_dicts, "multigraph": True, # In the order in Skeleton.nodes and must match up with nodes_dicts. @@ -1101,14 +1102,16 @@ def to_json(self, node_to_idx: Optional[Dict[Node, int]] = None) -> str: else: skeleton_dict ={ "directed": True, - "graph": { - "name": self.name, - "num_edges_inserted": len(self.edges), - }, + "nx_graph": graph, "links": edges_dicts, "multigraph": True, # In the order in Skeleton.nodes and must match up with nodes_dicts. "nodes": [{"id": {"py/id": node_to_id[node]}} for node in self.nodes],} + + print(f'skeleton_dict: {skeleton_dict}') + json_str = json.dumps(skeleton_dict) + print(f'json_str: {json_str}') + return json_str # jsonpickle.set_encoder_options("simplejson", sort_keys=True, indent=4) # if node_to_idx is not None: From 884da47ff676910cf23df4b5d6024b72afe032f3 Mon Sep 17 00:00:00 2001 From: eberrigan Date: Wed, 11 Sep 2024 16:41:38 -0700 Subject: [PATCH 10/18] match everything except indexing to legacy sleap --- sleap/skeleton.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/sleap/skeleton.py b/sleap/skeleton.py index d236be492..71e35fe7d 100644 --- a/sleap/skeleton.py +++ b/sleap/skeleton.py @@ -987,7 +987,7 @@ def to_json(self, node_to_idx: Optional[Dict[Node, int]] = None) -> str: """Convert the :class:`Skeleton` to a JSON representation. Args: - node_to_idx: optional dict which maps :class:`Nodes`to index + node_to_idx: optional dict which maps :class:`Node`s to index in some list. This is used when saving :class:`Labels`where we want to serialize the :class:`Nodes` outside the :class:`Skeleton` object. @@ -1050,8 +1050,9 @@ def to_json(self, node_to_idx: Optional[Dict[Node, int]] = None) -> str: # in legacy SLEAP. "edge_insert_idx": edge_ind, "key": 0, # Always 0. - "source": {"py/id": node_to_id[source]}, - "target": {"py/id": node_to_id[target]}, + "source": {"py/object": "sleap.skeleton.Node", "py/state": {"name": source.name, "weight": 1.0}}, + "target": {"py/object": "sleap.skeleton.Node", "py/state": {"name": target.name, "weight": 1.0}}, + # "target": {"py/id": node_to_id[target]}, "type": edge_type, } ) @@ -1087,22 +1088,26 @@ def to_json(self, node_to_idx: Optional[Dict[Node, int]] = None) -> str: "name": self.name, "num_edges_inserted": len(self.edges), } - # Create skeleton dict. + # Create skeleton dict if self.is_template: - skeleton_dict = { + # Template skeletons have additional fields + nx_graph = { "directed": True, - "nx_graph": graph, + "graph": graph, "links": edges_dicts, "multigraph": True, # In the order in Skeleton.nodes and must match up with nodes_dicts. "nodes": [{"id": {"py/id": node_to_id[node]}} for node in self.nodes], + } + skeleton_dict = { "description": self.description, + "nx_graph": nx_graph, "preview_image": self.preview_image, } else: skeleton_dict ={ "directed": True, - "nx_graph": graph, + "graph": graph, "links": edges_dicts, "multigraph": True, # In the order in Skeleton.nodes and must match up with nodes_dicts. From 6e37ca9b3b6a0c0f338494b3c1b392ede267a9fe Mon Sep 17 00:00:00 2001 From: Elizabeth Berrigan Date: Thu, 12 Sep 2024 18:26:01 -0700 Subject: [PATCH 11/18] move nodes list before links --- sleap/skeleton.py | 45 ++++++++++++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/sleap/skeleton.py b/sleap/skeleton.py index 71e35fe7d..b61907d13 100644 --- a/sleap/skeleton.py +++ b/sleap/skeleton.py @@ -1093,29 +1093,44 @@ def to_json(self, node_to_idx: Optional[Dict[Node, int]] = None) -> str: # Template skeletons have additional fields nx_graph = { "directed": True, - "graph": graph, + "graph": graph, + "nodes": [ + { + "id": { + "py/object": "sleap.skeleton.Node", + "py/state": {"name": node.name, "weight": node.weight}, + } + } + for node in self.nodes + ], "links": edges_dicts, "multigraph": True, - # In the order in Skeleton.nodes and must match up with nodes_dicts. - "nodes": [{"id": {"py/id": node_to_id[node]}} for node in self.nodes], } skeleton_dict = { "description": self.description, - "nx_graph": nx_graph, + "nx_graph": nx_graph, "preview_image": self.preview_image, } else: - skeleton_dict ={ - "directed": True, - "graph": graph, - "links": edges_dicts, - "multigraph": True, - # In the order in Skeleton.nodes and must match up with nodes_dicts. - "nodes": [{"id": {"py/id": node_to_id[node]}} for node in self.nodes],} - - print(f'skeleton_dict: {skeleton_dict}') - json_str = json.dumps(skeleton_dict) - print(f'json_str: {json_str}') + skeleton_dict = { + "directed": True, + "graph": graph, + "nodes": [ + { + "id": { + "py/object": "sleap.skeleton.Node", + "py/state": {"name": node.name, "weight": node.weight}, + } + } + for node in self.nodes + ], + "links": edges_dicts, + "multigraph": True, + } + + print(f"skeleton_dict: {skeleton_dict}") + json_str = json.dumps(skeleton_dict, indent=4, sort_keys=True) + print(f"json_str: {json_str}") return json_str # jsonpickle.set_encoder_options("simplejson", sort_keys=True, indent=4) From 01d02559472bb12f61c06bb36bc12cb4a1cbd089 Mon Sep 17 00:00:00 2001 From: Elizabeth Berrigan Date: Thu, 12 Sep 2024 18:26:08 -0700 Subject: [PATCH 12/18] format --- sleap/skeleton.py | 46 ++++++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/sleap/skeleton.py b/sleap/skeleton.py index b61907d13..f85576724 100644 --- a/sleap/skeleton.py +++ b/sleap/skeleton.py @@ -1004,7 +1004,7 @@ def to_json(self, node_to_idx: Optional[Dict[Node, int]] = None) -> str: node_to_id = {} for node in self.nodes: if node not in node_to_id: - print(f'node: {node}') + print(f"node: {node}") # Note: This ID is not the same as the node index in the skeleton in # legacy SLEAP, but we do not retain this information in the labels, so # IDs will be different. @@ -1014,15 +1014,15 @@ def to_json(self, node_to_idx: Optional[Dict[Node, int]] = None) -> str: # # TODO: Store legacy metadata in labels to get byte-level compatibility? node_to_id[node] = len(node_to_id) - print(f'node_to_id: {node_to_id}') + print(f"node_to_id: {node_to_id}") nodes_dicts.append({"name": node.name, "weight": 1.0}) - print(f'nodes_dicts: {nodes_dicts}') + print(f"nodes_dicts: {nodes_dicts}") # Build links dicts for normal edges. edges_dicts = [] for edge_ind, edge in enumerate(self.edges): - print(f'edge_ind: {edge_ind}') - print(f'edge: {edge}') + print(f"edge_ind: {edge_ind}") + print(f"edge: {edge}") if edge_ind == 0: edge_type = { "py/reduce": [ @@ -1030,38 +1030,44 @@ def to_json(self, node_to_idx: Optional[Dict[Node, int]] = None) -> str: {"py/tuple": [1]}, # 1 = real edge, 2 = symmetry edge ] } - print(f'edge_type: {edge_type}') + print(f"edge_type: {edge_type}") else: edge_type = {"py/id": 1} - print(f'edge_type: {edge_type}') + print(f"edge_type: {edge_type}") # Edges are stored as a list of tuples of nodes - # The source and target are the nodes in the tuple (edge) are the first and + # The source and target are the nodes in the tuple (edge) are the first and # second nodes respectively source = edge[0] - print(f'source: {source}') - print(f'node_to_id[source]: {node_to_id[source]}') + print(f"source: {source}") + print(f"node_to_id[source]: {node_to_id[source]}") target = edge[1] - print(f'target: {target}') - print(f'node_to_id[target]: {node_to_id[target]}') + print(f"target: {target}") + print(f"node_to_id[target]: {node_to_id[target]}") edges_dicts.append( { # Note: Insert idx is not the same as the edge index in the skeleton # in legacy SLEAP. "edge_insert_idx": edge_ind, "key": 0, # Always 0. - "source": {"py/object": "sleap.skeleton.Node", "py/state": {"name": source.name, "weight": 1.0}}, - "target": {"py/object": "sleap.skeleton.Node", "py/state": {"name": target.name, "weight": 1.0}}, + "source": { + "py/object": "sleap.skeleton.Node", + "py/state": {"name": source.name, "weight": 1.0}, + }, + "target": { + "py/object": "sleap.skeleton.Node", + "py/state": {"name": target.name, "weight": 1.0}, + }, # "target": {"py/id": node_to_id[target]}, "type": edge_type, } ) - print(f'edges_dicts: {edges_dicts}') + print(f"edges_dicts: {edges_dicts}") # Build links dicts for symmetry edges. for symmetry_ind, symmetry in enumerate(self.symmetries): - print(f'symmetry_ind: {symmetry_ind}') - print(f'symmetry: {symmetry}') + print(f"symmetry_ind: {symmetry_ind}") + print(f"symmetry: {symmetry}") if symmetry_ind == 0: edge_type = { "py/reduce": [ @@ -1073,8 +1079,8 @@ def to_json(self, node_to_idx: Optional[Dict[Node, int]] = None) -> str: edge_type = {"py/id": 2} src, dst = tuple(symmetry.nodes) - print(f'src: {src}') - print(f'dst: {dst}') + print(f"src: {src}") + print(f"dst: {dst}") edges_dicts.append( { "key": 0, @@ -1414,4 +1420,4 @@ def __hash__(self): cattr.register_unstructure_hook(Skeleton, lambda skeleton: Skeleton.to_dict(skeleton)) -cattr.register_structure_hook(Skeleton, lambda dicts, cls: Skeleton.from_dict(dicts)) \ No newline at end of file +cattr.register_structure_hook(Skeleton, lambda dicts, cls: Skeleton.from_dict(dicts)) From 71b0307009393eeba9712e8f8cdb54e2a4882e43 Mon Sep 17 00:00:00 2001 From: Elizabeth Berrigan Date: Thu, 12 Sep 2024 18:39:45 -0700 Subject: [PATCH 13/18] symmetry is a tuple of nodes --- sleap/skeleton.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sleap/skeleton.py b/sleap/skeleton.py index f85576724..ed6988fe1 100644 --- a/sleap/skeleton.py +++ b/sleap/skeleton.py @@ -1078,14 +1078,14 @@ def to_json(self, node_to_idx: Optional[Dict[Node, int]] = None) -> str: else: edge_type = {"py/id": 2} - src, dst = tuple(symmetry.nodes) - print(f"src: {src}") - print(f"dst: {dst}") + symmetry_src, symmetry_dst = symmetry + print(f"src: {symmetry_src}") + print(f"dst: {symmetry_dst}") edges_dicts.append( { "key": 0, - "source": {"py/id": node_to_id[src]}, - "target": {"py/id": node_to_id[dst]}, + "source": {"py/id": node_to_id[symmetry_src]}, + "target": {"py/id": node_to_id[symmetry_dst]}, "type": edge_type, } ) From 69db671c4f52334ac7414ffce0763f872e14da59 Mon Sep 17 00:00:00 2001 From: Elizabeth Berrigan Date: Thu, 12 Sep 2024 19:09:48 -0700 Subject: [PATCH 14/18] source and target should be tuples in edge dict --- sleap/skeleton.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/sleap/skeleton.py b/sleap/skeleton.py index ed6988fe1..f4339803b 100644 --- a/sleap/skeleton.py +++ b/sleap/skeleton.py @@ -1052,13 +1052,12 @@ def to_json(self, node_to_idx: Optional[Dict[Node, int]] = None) -> str: "key": 0, # Always 0. "source": { "py/object": "sleap.skeleton.Node", - "py/state": {"name": source.name, "weight": 1.0}, + "py/state": {"py/tuple": [source.name, source.weight]}, }, "target": { "py/object": "sleap.skeleton.Node", - "py/state": {"name": target.name, "weight": 1.0}, + "py/state": {"py/tuple": [target.name, target.weight]}, }, - # "target": {"py/id": node_to_id[target]}, "type": edge_type, } ) From 0e2f9b53224a5c12277beb4f96a6321104ea8177 Mon Sep 17 00:00:00 2001 From: eberrigan Date: Mon, 16 Sep 2024 19:23:24 -0700 Subject: [PATCH 15/18] check-in notebook for testing --- test_Skeleton_to_json.v006.ipynb | 313 +++++++++++++++++++++++++++++++ 1 file changed, 313 insertions(+) create mode 100644 test_Skeleton_to_json.v006.ipynb diff --git a/test_Skeleton_to_json.v006.ipynb b/test_Skeleton_to_json.v006.ipynb new file mode 100644 index 000000000..2ddfa54ab --- /dev/null +++ b/test_Skeleton_to_json.v006.ipynb @@ -0,0 +1,313 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import sleap\n", + "\n", + "import json\n", + "import jsonpickle\n", + "import attrs\n", + "\n", + "from sleap.skeleton import Skeleton\n", + "from typing import List, Dict, Any, Tuple, Union, Optional\n", + "from networkx.readwrite import json_graph\n", + "from sleap.io.dataset import Labels" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "sleap: 1.4.1a2\n", + "jsonpickle: 1.2\n", + "attrs: 24.2.0\n" + ] + } + ], + "source": [ + "print(f\"sleap: {sleap.__version__}\")\n", + "print(f\"jsonpickle: {jsonpickle.__version__}\")\n", + "print(f\"attrs: {attrs.__version__}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# tuples vs. dicts\n", + "# saving to individual skeleton JSON files\n", + "# input Skeleton from labels file \n", + "# from_json_data get rid of cattrs" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# old code from SLEAP\n", + " # def to_json(self, node_to_idx: Optional[Dict[Node, int]] = None) -> str:\n", + " # \"\"\"Convert the :class:`Skeleton` to a JSON representation.\n", + "\n", + " # Args:\n", + " # node_to_idx: optional dict which maps :class:`Nodes`to index\n", + " # in some list. This is used when saving\n", + " # :class:`Labels`where we want to serialize the\n", + " # :class:`Nodes` outside the :class:`Skeleton` object.\n", + " # If given, then we replace each :class:`Node` with\n", + " # specified index before converting :class:`Skeleton`.\n", + " # Otherwise, we convert :class:`Node` objects with the rest of\n", + " # the :class:`Skeleton`.\n", + "\n", + " # Returns:\n", + " # A string containing the JSON representation of the skeleton.\n", + " # \"\"\"\n", + " # jsonpickle.set_encoder_options(\"simplejson\", sort_keys=True, indent=4)\n", + " # if node_to_idx is not None:\n", + " # indexed_node_graph = nx.relabel_nodes(\n", + " # G=self._graph, mapping=node_to_idx\n", + " # ) # map nodes to int\n", + " # else:\n", + " # indexed_node_graph = self._graph\n", + "\n", + " # # Encode to JSON\n", + " # graph = json_graph.node_link_data(indexed_node_graph)\n", + "\n", + " # # SLEAP v1.3.0 added `description` and `preview_image` to `Skeleton`, but saving\n", + " # # these fields breaks data format compatibility. Currently, these are only\n", + " # # added in our custom template skeletons. To ensure backwards data format\n", + " # # compatibilty of user data, we only save these fields if they are not None.\n", + " # if self.is_template:\n", + " # data = {\n", + " # \"nx_graph\": graph,\n", + " # \"description\": self.description,\n", + " # \"preview_image\": self.preview_image,\n", + " # }\n", + " # else:\n", + " # data = graph\n", + "\n", + " # json_str = jsonpickle.encode(data)\n", + "\n", + " # return json_str" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "# # Test Skeleton from labels file made with sleap-io\n", + "\n", + "# labels_path = r\"C:\\repos\\sleap-io\\minimal_instance_from_sio.slp\"\n", + "\n", + "# # Load labels\n", + "# labels = sleap.load_file(labels_path)\n", + "\n", + "# labels.skeletons" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "test_skeleton: Skeleton(description=None, nodes=[A, B], edges=[A->B], symmetries=[])\n" + ] + } + ], + "source": [ + "# Test with labels\n", + "\n", + "# Load the labels\n", + "test_labels = sleap.load_file(r'tests\\data\\slp_hdf5\\minimal_instance.slp')\n", + "\n", + "# Get the first skeleton\n", + "test_skeleton = test_labels.skeletons[0]\n", + "\n", + "# Print the skeleton\n", + "print(f'test_skeleton: {test_skeleton}')" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "node: Node(name='A', weight=1.0)\n", + "node_to_id: {Node(name='A', weight=1.0): 0}\n", + "node: Node(name='B', weight=1.0)\n", + "node_to_id: {Node(name='A', weight=1.0): 0, Node(name='B', weight=1.0): 1}\n", + "nodes_dicts: [{'name': 'A', 'weight': 1.0}, {'name': 'B', 'weight': 1.0}]\n", + "edge_ind: 0\n", + "edge: (Node(name='A', weight=1.0), Node(name='B', weight=1.0))\n", + "edge_type: {'py/reduce': [{'py/type': 'sleap.skeleton.EdgeType'}, {'py/tuple': [1]}]}\n", + "source: Node(name='A', weight=1.0)\n", + "node_to_id[source]: 0\n", + "target: Node(name='B', weight=1.0)\n", + "node_to_id[target]: 1\n", + "edges_dicts: [{'edge_insert_idx': 0, 'key': 0, 'source': {'py/object': 'sleap.skeleton.Node', 'py/state': {'name': 'A', 'weight': 1.0}}, 'target': {'py/object': 'sleap.skeleton.Node', 'py/state': {'name': 'B', 'weight': 1.0}}, 'type': {'py/reduce': [{'py/type': 'sleap.skeleton.EdgeType'}, {'py/tuple': [1]}]}}]\n", + "skeleton_dict: {'directed': True, 'graph': {'name': 'Skeleton-0', 'num_edges_inserted': 1}, 'links': [{'edge_insert_idx': 0, 'key': 0, 'source': {'py/object': 'sleap.skeleton.Node', 'py/state': {'name': 'A', 'weight': 1.0}}, 'target': {'py/object': 'sleap.skeleton.Node', 'py/state': {'name': 'B', 'weight': 1.0}}, 'type': {'py/reduce': [{'py/type': 'sleap.skeleton.EdgeType'}, {'py/tuple': [1]}]}}], 'multigraph': True, 'nodes': [{'id': {'py/id': 0}}, {'id': {'py/id': 1}}]}\n", + "json_str: {\"directed\": true, \"graph\": {\"name\": \"Skeleton-0\", \"num_edges_inserted\": 1}, \"links\": [{\"edge_insert_idx\": 0, \"key\": 0, \"source\": {\"py/object\": \"sleap.skeleton.Node\", \"py/state\": {\"name\": \"A\", \"weight\": 1.0}}, \"target\": {\"py/object\": \"sleap.skeleton.Node\", \"py/state\": {\"name\": \"B\", \"weight\": 1.0}}, \"type\": {\"py/reduce\": [{\"py/type\": \"sleap.skeleton.EdgeType\"}, {\"py/tuple\": [1]}]}}], \"multigraph\": true, \"nodes\": [{\"id\": {\"py/id\": 0}}, {\"id\": {\"py/id\": 1}}]}\n", + "test_skeleton_json: {\"directed\": true, \"graph\": {\"name\": \"Skeleton-0\", \"num_edges_inserted\": 1}, \"links\": [{\"edge_insert_idx\": 0, \"key\": 0, \"source\": {\"py/object\": \"sleap.skeleton.Node\", \"py/state\": {\"name\": \"A\", \"weight\": 1.0}}, \"target\": {\"py/object\": \"sleap.skeleton.Node\", \"py/state\": {\"name\": \"B\", \"weight\": 1.0}}, \"type\": {\"py/reduce\": [{\"py/type\": \"sleap.skeleton.EdgeType\"}, {\"py/tuple\": [1]}]}}], \"multigraph\": true, \"nodes\": [{\"id\": {\"py/id\": 0}}, {\"id\": {\"py/id\": 1}}]}\n", + "Wrote test_skeleton_json to file\n" + ] + } + ], + "source": [ + "# Save to json using the old to_json method (deprecated)\n", + "test_skeleton_json = test_skeleton.to_json()\n", + "print(f'test_skeleton_json: {test_skeleton_json}')\n", + "\n", + "# Save json string to file\n", + "with open(r'test_skeleton_serialization.json', 'w') as f:\n", + " f.write(test_skeleton_json)\n", + " print(f'Wrote test_skeleton_json to file')" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "ename": "TypeError", + "evalue": "unhashable type: 'dict'", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mTypeError\u001b[0m Traceback (most recent call last)", + "\u001b[1;32m~\\AppData\\Local\\Temp\\ipykernel_21480\\3098081962.py\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[0;32m 1\u001b[0m \u001b[1;31m# Load the json file\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 2\u001b[1;33m \u001b[0mtest_skeleton_from_json\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mSkeleton\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mfrom_json\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mtest_skeleton_json\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 3\u001b[0m \u001b[0mprint\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34mf'test_skeleton_from_json: {test_skeleton_from_json}'\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;32mc:\\repos\\sleap\\sleap\\skeleton.py\u001b[0m in \u001b[0;36mfrom_json\u001b[1;34m(cls, json_str, idx_to_node)\u001b[0m\n\u001b[0;32m 1193\u001b[0m \u001b[0mdicts\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mjsonpickle\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mdecode\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mjson_str\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 1194\u001b[0m \u001b[0mnx_graph\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mdicts\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mget\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m\"nx_graph\"\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mdicts\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m-> 1195\u001b[1;33m \u001b[0mgraph\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mjson_graph\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mnode_link_graph\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mnx_graph\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 1196\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 1197\u001b[0m \u001b[1;31m# Replace graph node indices with corresponding nodes from node_map\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;32mc:\\miniforge3\\envs\\sleap_dev_latest\\lib\\site-packages\\networkx\\readwrite\\json_graph\\node_link.py\u001b[0m in \u001b[0;36mnode_link_graph\u001b[1;34m(data, directed, multigraph, attrs)\u001b[0m\n\u001b[0;32m 167\u001b[0m \u001b[0mnode\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mto_tuple\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0md\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mget\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mname\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mnext\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mc\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 168\u001b[0m \u001b[0mnodedata\u001b[0m \u001b[1;33m=\u001b[0m \u001b[1;33m{\u001b[0m\u001b[0mstr\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mk\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m \u001b[0mv\u001b[0m \u001b[1;32mfor\u001b[0m \u001b[0mk\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mv\u001b[0m \u001b[1;32min\u001b[0m \u001b[0md\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mitems\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mk\u001b[0m \u001b[1;33m!=\u001b[0m \u001b[0mname\u001b[0m\u001b[1;33m}\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 169\u001b[1;33m \u001b[0mgraph\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0madd_node\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mnode\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m**\u001b[0m\u001b[0mnodedata\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 170\u001b[0m \u001b[1;32mfor\u001b[0m \u001b[0md\u001b[0m \u001b[1;32min\u001b[0m \u001b[0mdata\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0mlinks\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 171\u001b[0m \u001b[0msrc\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mtuple\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0md\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0msource\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0misinstance\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0md\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0msource\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mlist\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;32melse\u001b[0m \u001b[0md\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0msource\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;32mc:\\miniforge3\\envs\\sleap_dev_latest\\lib\\site-packages\\networkx\\classes\\digraph.py\u001b[0m in \u001b[0;36madd_node\u001b[1;34m(self, node_for_adding, **attr)\u001b[0m\n\u001b[0;32m 416\u001b[0m \u001b[0mdoesn\u001b[0m\u001b[0;31m'\u001b[0m\u001b[0mt\u001b[0m \u001b[0mchange\u001b[0m \u001b[0mon\u001b[0m \u001b[0mmutables\u001b[0m\u001b[1;33m.\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 417\u001b[0m \"\"\"\n\u001b[1;32m--> 418\u001b[1;33m \u001b[1;32mif\u001b[0m \u001b[0mnode_for_adding\u001b[0m \u001b[1;32mnot\u001b[0m \u001b[1;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_succ\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 419\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mnode_for_adding\u001b[0m \u001b[1;32mis\u001b[0m \u001b[1;32mNone\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 420\u001b[0m \u001b[1;32mraise\u001b[0m \u001b[0mValueError\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m\"None cannot be a node\"\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;31mTypeError\u001b[0m: unhashable type: 'dict'" + ] + } + ], + "source": [ + "# Load the json file\n", + "test_skeleton_from_json = Skeleton.from_json(test_skeleton_json)\n", + "print(f'test_skeleton_from_json: {test_skeleton_from_json}')" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "# # Read json file from sleap-io\n", + "# sleap_io_json_path = r\"C:\\repos\\sleap-io\\skeletons.json\"\n", + "\n", + "# with open(sleap_io_json_path, 'r') as f:\n", + "# sleap_io_json = f.read()\n", + "\n", + "# sleap_io_json" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Skeleton(name='Skeleton-0', description='None', nodes=['A', 'B'], edges=[('A', 'B')], symmetries=[])" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Load skeleton from json made with dev branch\n", + "skeleton_from_dev_path = r\"skeleton_from_labels_dev_branch.json\"\n", + "skeleton_from_dev = Skeleton.load_json(skeleton_from_dev_path)\n", + "\n", + "skeleton_from_dev" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Skeleton(name='Skeleton-0', description='None', nodes=['A', 'B'], edges=[('A', 'B')], symmetries=[])" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Load skeleton from json made with code changes\n", + "skeleton_from_code_changes_path = r\"test_skeleton_serialization.json\"\n", + "skeleton_from_code_changes = Skeleton.load_json(skeleton_from_code_changes_path)\n", + "\n", + "skeleton_from_code_changes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "sleap_dev_1.4.1a2", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.12" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From 4ff6fdb2c78d2b19ce92c126c3b2d71308c1fa52 Mon Sep 17 00:00:00 2001 From: Elizabeth Berrigan Date: Mon, 16 Sep 2024 19:32:55 -0700 Subject: [PATCH 16/18] refer to source and target nodes using "py/id" --- sleap/skeleton.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/sleap/skeleton.py b/sleap/skeleton.py index f4339803b..0a0b5f29c 100644 --- a/sleap/skeleton.py +++ b/sleap/skeleton.py @@ -1050,14 +1050,8 @@ def to_json(self, node_to_idx: Optional[Dict[Node, int]] = None) -> str: # in legacy SLEAP. "edge_insert_idx": edge_ind, "key": 0, # Always 0. - "source": { - "py/object": "sleap.skeleton.Node", - "py/state": {"py/tuple": [source.name, source.weight]}, - }, - "target": { - "py/object": "sleap.skeleton.Node", - "py/state": {"py/tuple": [target.name, target.weight]}, - }, + "source": {"py/id": self.nodes.index(node)}, + "target": {"py/id": self.nodes.index(node)}, "type": edge_type, } ) From f0451c52b78eaaf53d74206ebba00d08d4a1a0ba Mon Sep 17 00:00:00 2001 From: Elizabeth Berrigan Date: Mon, 16 Sep 2024 20:04:59 -0700 Subject: [PATCH 17/18] use source and target nodes to index --- sleap/skeleton.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sleap/skeleton.py b/sleap/skeleton.py index 0a0b5f29c..bfba5928b 100644 --- a/sleap/skeleton.py +++ b/sleap/skeleton.py @@ -1050,8 +1050,8 @@ def to_json(self, node_to_idx: Optional[Dict[Node, int]] = None) -> str: # in legacy SLEAP. "edge_insert_idx": edge_ind, "key": 0, # Always 0. - "source": {"py/id": self.nodes.index(node)}, - "target": {"py/id": self.nodes.index(node)}, + "source": {"py/id": self.nodes.index(source)}, + "target": {"py/id": self.nodes.index(target)}, "type": edge_type, } ) From b4f10af105c153e6fb89ca1c46793d6c41271358 Mon Sep 17 00:00:00 2001 From: Elizabeth Berrigan Date: Mon, 16 Sep 2024 20:05:34 -0700 Subject: [PATCH 18/18] use tuple format for Node --- sleap/skeleton.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sleap/skeleton.py b/sleap/skeleton.py index bfba5928b..1a031e34e 100644 --- a/sleap/skeleton.py +++ b/sleap/skeleton.py @@ -1097,7 +1097,7 @@ def to_json(self, node_to_idx: Optional[Dict[Node, int]] = None) -> str: { "id": { "py/object": "sleap.skeleton.Node", - "py/state": {"name": node.name, "weight": node.weight}, + "py/state": {"py/tuple": [node.name, node.weight]}, } } for node in self.nodes @@ -1118,7 +1118,7 @@ def to_json(self, node_to_idx: Optional[Dict[Node, int]] = None) -> str: { "id": { "py/object": "sleap.skeleton.Node", - "py/state": {"name": node.name, "weight": node.weight}, + "py/state": {"py/tuple": [node.name, node.weight]}, } } for node in self.nodes