napistu 0.2.5.dev7__py3-none-any.whl → 0.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- napistu/__main__.py +126 -96
- napistu/constants.py +35 -41
- napistu/context/__init__.py +10 -0
- napistu/context/discretize.py +462 -0
- napistu/context/filtering.py +387 -0
- napistu/gcs/__init__.py +1 -1
- napistu/identifiers.py +74 -15
- napistu/indices.py +68 -0
- napistu/ingestion/__init__.py +1 -1
- napistu/ingestion/bigg.py +47 -62
- napistu/ingestion/constants.py +18 -133
- napistu/ingestion/gtex.py +113 -0
- napistu/ingestion/hpa.py +147 -0
- napistu/ingestion/sbml.py +0 -97
- napistu/ingestion/string.py +2 -2
- napistu/matching/__init__.py +10 -0
- napistu/matching/constants.py +18 -0
- napistu/matching/interactions.py +518 -0
- napistu/matching/mount.py +529 -0
- napistu/matching/species.py +510 -0
- napistu/mcp/__init__.py +7 -4
- napistu/mcp/__main__.py +128 -72
- napistu/mcp/client.py +16 -25
- napistu/mcp/codebase.py +201 -145
- napistu/mcp/component_base.py +170 -0
- napistu/mcp/config.py +223 -0
- napistu/mcp/constants.py +45 -2
- napistu/mcp/documentation.py +253 -136
- napistu/mcp/documentation_utils.py +13 -48
- napistu/mcp/execution.py +372 -305
- napistu/mcp/health.py +47 -65
- napistu/mcp/profiles.py +10 -6
- napistu/mcp/server.py +161 -80
- napistu/mcp/tutorials.py +139 -87
- napistu/modify/__init__.py +1 -1
- napistu/modify/gaps.py +1 -1
- napistu/network/__init__.py +1 -1
- napistu/network/constants.py +101 -34
- napistu/network/data_handling.py +388 -0
- napistu/network/ig_utils.py +351 -0
- napistu/network/napistu_graph_core.py +354 -0
- napistu/network/neighborhoods.py +40 -40
- napistu/network/net_create.py +373 -309
- napistu/network/net_propagation.py +47 -19
- napistu/network/{net_utils.py → ng_utils.py} +124 -272
- napistu/network/paths.py +67 -51
- napistu/network/precompute.py +11 -11
- napistu/ontologies/__init__.py +10 -0
- napistu/ontologies/constants.py +129 -0
- napistu/ontologies/dogma.py +243 -0
- napistu/ontologies/genodexito.py +649 -0
- napistu/ontologies/mygene.py +369 -0
- napistu/ontologies/renaming.py +198 -0
- napistu/rpy2/__init__.py +229 -86
- napistu/rpy2/callr.py +47 -77
- napistu/rpy2/constants.py +24 -23
- napistu/rpy2/rids.py +61 -648
- napistu/sbml_dfs_core.py +587 -222
- napistu/scverse/__init__.py +15 -0
- napistu/scverse/constants.py +28 -0
- napistu/scverse/loading.py +727 -0
- napistu/utils.py +118 -10
- {napistu-0.2.5.dev7.dist-info → napistu-0.3.1.dist-info}/METADATA +8 -3
- napistu-0.3.1.dist-info/RECORD +133 -0
- tests/conftest.py +22 -0
- tests/test_context_discretize.py +56 -0
- tests/test_context_filtering.py +267 -0
- tests/test_identifiers.py +100 -0
- tests/test_indices.py +65 -0
- tests/{test_edgelist.py → test_ingestion_napistu_edgelist.py} +2 -2
- tests/test_matching_interactions.py +108 -0
- tests/test_matching_mount.py +305 -0
- tests/test_matching_species.py +394 -0
- tests/test_mcp_config.py +193 -0
- tests/test_mcp_documentation_utils.py +12 -3
- tests/test_mcp_server.py +156 -19
- tests/test_network_data_handling.py +397 -0
- tests/test_network_ig_utils.py +23 -0
- tests/test_network_neighborhoods.py +19 -0
- tests/test_network_net_create.py +459 -0
- tests/test_network_ng_utils.py +30 -0
- tests/test_network_paths.py +56 -0
- tests/{test_precomputed_distances.py → test_network_precompute.py} +8 -6
- tests/test_ontologies_genodexito.py +58 -0
- tests/test_ontologies_mygene.py +39 -0
- tests/test_ontologies_renaming.py +110 -0
- tests/test_rpy2_callr.py +79 -0
- tests/test_rpy2_init.py +151 -0
- tests/test_sbml.py +0 -31
- tests/test_sbml_dfs_core.py +134 -10
- tests/test_scverse_loading.py +778 -0
- tests/test_set_coverage.py +2 -2
- tests/test_utils.py +121 -1
- napistu/mechanism_matching.py +0 -1353
- napistu/rpy2/netcontextr.py +0 -467
- napistu-0.2.5.dev7.dist-info/RECORD +0 -98
- tests/test_igraph.py +0 -367
- tests/test_mechanism_matching.py +0 -784
- tests/test_net_utils.py +0 -149
- tests/test_netcontextr.py +0 -105
- tests/test_rpy2.py +0 -61
- /napistu/ingestion/{cpr_edgelist.py → napistu_edgelist.py} +0 -0
- {napistu-0.2.5.dev7.dist-info → napistu-0.3.1.dist-info}/WHEEL +0 -0
- {napistu-0.2.5.dev7.dist-info → napistu-0.3.1.dist-info}/entry_points.txt +0 -0
- {napistu-0.2.5.dev7.dist-info → napistu-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {napistu-0.2.5.dev7.dist-info → napistu-0.3.1.dist-info}/top_level.txt +0 -0
- /tests/{test_obo.py → test_ingestion_obo.py} +0 -0
@@ -0,0 +1,351 @@
|
|
1
|
+
"""
|
2
|
+
General utilities for working with igraph.Graph objects.
|
3
|
+
|
4
|
+
This module contains utilities that can be broadly applied to any igraph.Graph
|
5
|
+
object, not specific to NapistuGraph subclasses.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from __future__ import annotations
|
9
|
+
|
10
|
+
import logging
|
11
|
+
import random
|
12
|
+
from typing import Any, Optional, Sequence
|
13
|
+
|
14
|
+
import igraph as ig
|
15
|
+
import numpy as np
|
16
|
+
import pandas as pd
|
17
|
+
|
18
|
+
logger = logging.getLogger(__name__)
|
19
|
+
|
20
|
+
|
21
|
+
def get_graph_summary(graph: ig.Graph) -> dict[str, Any]:
|
22
|
+
"""
|
23
|
+
Calculate common summary statistics for an igraph network.
|
24
|
+
|
25
|
+
Parameters
|
26
|
+
----------
|
27
|
+
graph : ig.Graph
|
28
|
+
The input network.
|
29
|
+
|
30
|
+
Returns
|
31
|
+
-------
|
32
|
+
dict
|
33
|
+
A dictionary of summary statistics with the following keys:
|
34
|
+
- n_edges (int): number of edges
|
35
|
+
- n_vertices (int): number of vertices
|
36
|
+
- n_components (int): number of weakly connected components
|
37
|
+
- stats_component_sizes (dict): summary statistics for the component sizes
|
38
|
+
- top10_large_components (list[dict]): the top 10 largest components with 10 example vertices
|
39
|
+
- top10_smallest_components (list[dict]): the top 10 smallest components with 10 example vertices
|
40
|
+
- average_path_length (float): the average shortest path length between all vertices
|
41
|
+
- top10_betweenness (list[dict]): the top 10 vertices by betweenness centrality
|
42
|
+
- top10_harmonic_centrality (list[dict]): the top 10 vertices by harmonic centrality
|
43
|
+
"""
|
44
|
+
stats = {}
|
45
|
+
stats["n_edges"] = graph.ecount()
|
46
|
+
stats["n_vertices"] = graph.vcount()
|
47
|
+
components = graph.components(mode="weak")
|
48
|
+
stats["n_components"] = len(components)
|
49
|
+
component_sizes = [len(c) for c in components]
|
50
|
+
stats["stats_component_sizes"] = pd.Series(component_sizes).describe().to_dict()
|
51
|
+
|
52
|
+
# get the top 10 largest components and 10 example nodes
|
53
|
+
stats["top10_large_components"] = _get_top_n_component_stats(
|
54
|
+
graph, components, component_sizes, n=10, ascending=False
|
55
|
+
)
|
56
|
+
stats["top10_smallest_components"] = _get_top_n_component_stats(
|
57
|
+
graph, components, component_sizes, n=10, ascending=True
|
58
|
+
)
|
59
|
+
|
60
|
+
stats["average_path_length"] = graph.average_path_length()
|
61
|
+
|
62
|
+
# Top 10 by betweenness and harmonic centrality
|
63
|
+
betweenness = graph.betweenness()
|
64
|
+
stats["top10_betweenness"] = _get_top_n_nodes(
|
65
|
+
graph, betweenness, "betweenness", n=10
|
66
|
+
)
|
67
|
+
harmonic = graph.harmonic_centrality()
|
68
|
+
stats["top10_harmonic_centrality"] = _get_top_n_nodes(
|
69
|
+
graph, harmonic, "harmonic_centrality", n=10
|
70
|
+
)
|
71
|
+
|
72
|
+
return stats
|
73
|
+
|
74
|
+
|
75
|
+
def filter_to_largest_subgraph(graph: ig.Graph) -> ig.Graph:
|
76
|
+
"""
|
77
|
+
Filter an igraph to its largest weakly connected component.
|
78
|
+
|
79
|
+
Parameters
|
80
|
+
----------
|
81
|
+
graph : ig.Graph
|
82
|
+
The input network.
|
83
|
+
|
84
|
+
Returns
|
85
|
+
-------
|
86
|
+
ig.Graph
|
87
|
+
The largest weakly connected component.
|
88
|
+
"""
|
89
|
+
component_members = graph.components(mode="weak")
|
90
|
+
component_sizes = [len(x) for x in component_members]
|
91
|
+
|
92
|
+
top_component_members = [
|
93
|
+
m
|
94
|
+
for s, m in zip(component_sizes, component_members)
|
95
|
+
if s == max(component_sizes)
|
96
|
+
][0]
|
97
|
+
|
98
|
+
largest_subgraph = graph.induced_subgraph(top_component_members)
|
99
|
+
return largest_subgraph
|
100
|
+
|
101
|
+
|
102
|
+
def graph_to_pandas_dfs(graph: ig.Graph) -> tuple[pd.DataFrame, pd.DataFrame]:
|
103
|
+
"""
|
104
|
+
Convert an igraph to Pandas DataFrames for vertices and edges.
|
105
|
+
|
106
|
+
Parameters
|
107
|
+
----------
|
108
|
+
graph : ig.Graph
|
109
|
+
An igraph network.
|
110
|
+
|
111
|
+
Returns
|
112
|
+
-------
|
113
|
+
vertices : pandas.DataFrame
|
114
|
+
A table with one row per vertex.
|
115
|
+
edges : pandas.DataFrame
|
116
|
+
A table with one row per edge.
|
117
|
+
"""
|
118
|
+
vertices = pd.DataFrame(
|
119
|
+
[{**{"index": v.index}, **v.attributes()} for v in graph.vs]
|
120
|
+
)
|
121
|
+
edges = pd.DataFrame(
|
122
|
+
[
|
123
|
+
{**{"source": e.source, "target": e.target}, **e.attributes()}
|
124
|
+
for e in graph.es
|
125
|
+
]
|
126
|
+
)
|
127
|
+
return vertices, edges
|
128
|
+
|
129
|
+
|
130
|
+
def create_induced_subgraph(
|
131
|
+
graph: ig.Graph,
|
132
|
+
vertices: Optional[list[str]] = None,
|
133
|
+
n_vertices: int = 5000,
|
134
|
+
) -> ig.Graph:
|
135
|
+
"""
|
136
|
+
Create a subgraph from an igraph including a set of vertices and their connections.
|
137
|
+
|
138
|
+
Parameters
|
139
|
+
----------
|
140
|
+
graph : ig.Graph
|
141
|
+
The input network.
|
142
|
+
vertices : list, optional
|
143
|
+
List of vertex names to include. If None, a random sample is selected.
|
144
|
+
n_vertices : int, optional
|
145
|
+
Number of vertices to sample if `vertices` is None. Default is 5000.
|
146
|
+
|
147
|
+
Returns
|
148
|
+
-------
|
149
|
+
ig.Graph
|
150
|
+
The induced subgraph.
|
151
|
+
"""
|
152
|
+
if vertices is not None:
|
153
|
+
selected_vertices = vertices
|
154
|
+
else:
|
155
|
+
# Assume vertices have a 'name' attribute, fallback to indices
|
156
|
+
if "name" in graph.vs.attributes():
|
157
|
+
vertex_names = graph.vs["name"]
|
158
|
+
else:
|
159
|
+
vertex_names = list(range(graph.vcount()))
|
160
|
+
selected_vertices = random.sample(
|
161
|
+
vertex_names, min(n_vertices, len(vertex_names))
|
162
|
+
)
|
163
|
+
|
164
|
+
subgraph = graph.induced_subgraph(selected_vertices)
|
165
|
+
return subgraph
|
166
|
+
|
167
|
+
|
168
|
+
def validate_edge_attributes(graph: ig.Graph, edge_attributes: list[str]) -> None:
|
169
|
+
"""
|
170
|
+
Validate that all required edge attributes exist in an igraph.
|
171
|
+
|
172
|
+
Parameters
|
173
|
+
----------
|
174
|
+
graph : ig.Graph
|
175
|
+
The network.
|
176
|
+
edge_attributes : list of str
|
177
|
+
List of edge attribute names to check.
|
178
|
+
|
179
|
+
Returns
|
180
|
+
-------
|
181
|
+
None
|
182
|
+
|
183
|
+
Raises
|
184
|
+
------
|
185
|
+
TypeError
|
186
|
+
If "edge_attributes" is not a list or str.
|
187
|
+
ValueError
|
188
|
+
If any required edge attribute is missing from the graph.
|
189
|
+
"""
|
190
|
+
if isinstance(edge_attributes, list):
|
191
|
+
attrs = edge_attributes
|
192
|
+
elif isinstance(edge_attributes, str):
|
193
|
+
attrs = [edge_attributes]
|
194
|
+
else:
|
195
|
+
raise TypeError('"edge_attributes" must be a list or str')
|
196
|
+
|
197
|
+
available_attributes = graph.es[0].attributes().keys() if graph.ecount() > 0 else []
|
198
|
+
missing_attributes = set(attrs).difference(available_attributes)
|
199
|
+
n_missing_attrs = len(missing_attributes)
|
200
|
+
|
201
|
+
if n_missing_attrs > 0:
|
202
|
+
raise ValueError(
|
203
|
+
f"{n_missing_attrs} edge attributes were missing ({', '.join(missing_attributes)}). "
|
204
|
+
f"The available edge attributes are {', '.join(available_attributes)}"
|
205
|
+
)
|
206
|
+
|
207
|
+
return None
|
208
|
+
|
209
|
+
|
210
|
+
def validate_vertex_attributes(graph: ig.Graph, vertex_attributes: list[str]) -> None:
|
211
|
+
"""
|
212
|
+
Validate that all required vertex attributes exist in an igraph.
|
213
|
+
|
214
|
+
Parameters
|
215
|
+
----------
|
216
|
+
graph : ig.Graph
|
217
|
+
The network.
|
218
|
+
vertex_attributes : list of str
|
219
|
+
List of vertex attribute names to check.
|
220
|
+
|
221
|
+
Returns
|
222
|
+
-------
|
223
|
+
None
|
224
|
+
|
225
|
+
Raises
|
226
|
+
------
|
227
|
+
TypeError
|
228
|
+
If "vertex_attributes" is not a list or str.
|
229
|
+
ValueError
|
230
|
+
If any required vertex attribute is missing from the graph.
|
231
|
+
"""
|
232
|
+
if isinstance(vertex_attributes, list):
|
233
|
+
attrs = vertex_attributes
|
234
|
+
elif isinstance(vertex_attributes, str):
|
235
|
+
attrs = [vertex_attributes]
|
236
|
+
else:
|
237
|
+
raise TypeError('"vertex_attributes" must be a list or str')
|
238
|
+
|
239
|
+
available_attributes = graph.vs[0].attributes().keys() if graph.vcount() > 0 else []
|
240
|
+
missing_attributes = set(attrs).difference(available_attributes)
|
241
|
+
n_missing_attrs = len(missing_attributes)
|
242
|
+
|
243
|
+
if n_missing_attrs > 0:
|
244
|
+
raise ValueError(
|
245
|
+
f"{n_missing_attrs} vertex attributes were missing ({', '.join(missing_attributes)}). "
|
246
|
+
f"The available vertex attributes are {', '.join(available_attributes)}"
|
247
|
+
)
|
248
|
+
|
249
|
+
return None
|
250
|
+
|
251
|
+
|
252
|
+
# Internal utility functions
|
253
|
+
|
254
|
+
|
255
|
+
def _get_top_n_idx(arr: Sequence, n: int, ascending: bool = False) -> Sequence[int]:
|
256
|
+
"""Returns the indices of the top n values in an array
|
257
|
+
|
258
|
+
Args:
|
259
|
+
arr (Sequence): An array of values
|
260
|
+
n (int): The number of top values to return
|
261
|
+
ascending (bool, optional): Whether to return the top or bottom n values. Defaults to False.
|
262
|
+
|
263
|
+
Returns:
|
264
|
+
Sequence[int]: The indices of the top n values
|
265
|
+
"""
|
266
|
+
order = np.argsort(arr)
|
267
|
+
if ascending:
|
268
|
+
return order[:n] # type: ignore
|
269
|
+
else:
|
270
|
+
return order[-n:][::-1] # type: ignore
|
271
|
+
|
272
|
+
|
273
|
+
def _get_top_n_objects(
|
274
|
+
object_vals: Sequence, objects: Sequence, n: int = 10, ascending: bool = False
|
275
|
+
) -> list:
|
276
|
+
"""Get the top N objects based on a ranking measure."""
|
277
|
+
idxs = _get_top_n_idx(object_vals, n, ascending=ascending)
|
278
|
+
top_objects = [objects[idx] for idx in idxs]
|
279
|
+
return top_objects
|
280
|
+
|
281
|
+
|
282
|
+
def _get_top_n_component_stats(
|
283
|
+
graph: ig.Graph,
|
284
|
+
components,
|
285
|
+
component_sizes: Sequence[int],
|
286
|
+
n: int = 10,
|
287
|
+
ascending: bool = False,
|
288
|
+
) -> list[dict[str, Any]]:
|
289
|
+
"""
|
290
|
+
Summarize the top N components' network properties.
|
291
|
+
|
292
|
+
Parameters
|
293
|
+
----------
|
294
|
+
graph : ig.Graph
|
295
|
+
The network.
|
296
|
+
components : list
|
297
|
+
List of components (as lists of vertex indices).
|
298
|
+
component_sizes : Sequence[int]
|
299
|
+
Sizes of each component.
|
300
|
+
n : int, optional
|
301
|
+
Number of top components to return. Default is 10.
|
302
|
+
ascending : bool, optional
|
303
|
+
If True, return smallest components; otherwise, largest. Default is False.
|
304
|
+
|
305
|
+
Returns
|
306
|
+
-------
|
307
|
+
list of dict
|
308
|
+
Each dict contains:
|
309
|
+
- 'n': size of the component
|
310
|
+
- 'examples': up to 10 example vertex attribute dicts from the component
|
311
|
+
"""
|
312
|
+
top_components = _get_top_n_objects(component_sizes, components, n, ascending)
|
313
|
+
top_component_stats = [
|
314
|
+
{"n": len(c), "examples": [graph.vs[n].attributes() for n in c[:10]]}
|
315
|
+
for c in top_components
|
316
|
+
]
|
317
|
+
return top_component_stats
|
318
|
+
|
319
|
+
|
320
|
+
def _get_top_n_nodes(
|
321
|
+
graph: ig.Graph,
|
322
|
+
vals: Sequence,
|
323
|
+
val_name: str,
|
324
|
+
n: int = 10,
|
325
|
+
ascending: bool = False,
|
326
|
+
) -> list[dict[str, Any]]:
|
327
|
+
"""
|
328
|
+
Get the top N nodes by a node attribute.
|
329
|
+
|
330
|
+
Parameters
|
331
|
+
----------
|
332
|
+
graph : ig.Graph
|
333
|
+
The network.
|
334
|
+
vals : Sequence
|
335
|
+
Sequence of node attribute values.
|
336
|
+
val_name : str
|
337
|
+
Name of the attribute.
|
338
|
+
n : int, optional
|
339
|
+
Number of top nodes to return. Default is 10.
|
340
|
+
ascending : bool, optional
|
341
|
+
If True, return nodes with smallest values; otherwise, largest. Default is False.
|
342
|
+
|
343
|
+
Returns
|
344
|
+
-------
|
345
|
+
list of dict
|
346
|
+
Each dict contains the value and the node's attributes.
|
347
|
+
"""
|
348
|
+
top_idxs = _get_top_n_idx(vals, n, ascending=ascending)
|
349
|
+
top_node_attrs = [graph.vs[idx].attributes() for idx in top_idxs]
|
350
|
+
top_vals = [vals[idx] for idx in top_idxs]
|
351
|
+
return [{val_name: val, **node} for val, node in zip(top_vals, top_node_attrs)]
|
@@ -0,0 +1,354 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import copy
|
4
|
+
import logging
|
5
|
+
from typing import Any, Optional
|
6
|
+
|
7
|
+
import igraph as ig
|
8
|
+
import pandas as pd
|
9
|
+
|
10
|
+
from napistu.network.constants import (
|
11
|
+
NAPISTU_GRAPH_EDGES,
|
12
|
+
EDGE_REVERSAL_ATTRIBUTE_MAPPING,
|
13
|
+
EDGE_DIRECTION_MAPPING,
|
14
|
+
)
|
15
|
+
|
16
|
+
logger = logging.getLogger(__name__)
|
17
|
+
|
18
|
+
|
19
|
+
class NapistuGraph(ig.Graph):
|
20
|
+
"""
|
21
|
+
A subclass of igraph.Graph with additional functionality for molecular network analysis.
|
22
|
+
|
23
|
+
This class extends igraph.Graph with domain-specific methods and metadata tracking
|
24
|
+
for biological pathway and molecular interaction networks. All standard igraph
|
25
|
+
methods are available, plus additional functionality for edge reversal and
|
26
|
+
metadata management.
|
27
|
+
|
28
|
+
Parameters
|
29
|
+
----------
|
30
|
+
*args : tuple
|
31
|
+
Positional arguments passed to igraph.Graph constructor
|
32
|
+
**kwargs : dict
|
33
|
+
Keyword arguments passed to igraph.Graph constructor
|
34
|
+
|
35
|
+
Attributes
|
36
|
+
----------
|
37
|
+
is_reversed : bool
|
38
|
+
Whether the graph edges have been reversed from their original direction
|
39
|
+
graph_type : str or None
|
40
|
+
Type of graph (e.g., 'bipartite', 'regulatory', 'surrogate')
|
41
|
+
weighting_strategy : str or None
|
42
|
+
Strategy used for edge weighting (e.g., 'topology', 'mixed', 'calibrated')
|
43
|
+
|
44
|
+
Methods
|
45
|
+
-------
|
46
|
+
from_igraph(graph, **metadata)
|
47
|
+
Create a NapistuGraph from an existing igraph.Graph
|
48
|
+
reverse_edges()
|
49
|
+
Reverse all edges in the graph in-place
|
50
|
+
set_metadata(**kwargs)
|
51
|
+
Set metadata for the graph in-place
|
52
|
+
get_metadata(key=None)
|
53
|
+
Get metadata from the graph
|
54
|
+
copy()
|
55
|
+
Create a deep copy of the NapistuGraph
|
56
|
+
|
57
|
+
Examples
|
58
|
+
--------
|
59
|
+
Create a NapistuGraph from scratch:
|
60
|
+
|
61
|
+
>>> ng = NapistuGraph(directed=True)
|
62
|
+
>>> ng.add_vertices(3)
|
63
|
+
>>> ng.add_edges([(0, 1), (1, 2)])
|
64
|
+
|
65
|
+
Convert from existing igraph:
|
66
|
+
|
67
|
+
>>> import igraph as ig
|
68
|
+
>>> g = ig.Graph.Erdos_Renyi(10, 0.3)
|
69
|
+
>>> ng = NapistuGraph.from_igraph(g, graph_type='random')
|
70
|
+
|
71
|
+
Reverse edges and check state:
|
72
|
+
|
73
|
+
>>> ng.reverse_edges()
|
74
|
+
>>> print(ng.is_reversed)
|
75
|
+
True
|
76
|
+
|
77
|
+
Set and retrieve metadata:
|
78
|
+
|
79
|
+
>>> ng.set_metadata(experiment_id='exp_001', date='2024-01-01')
|
80
|
+
>>> print(ng.get_metadata('experiment_id'))
|
81
|
+
'exp_001'
|
82
|
+
|
83
|
+
Notes
|
84
|
+
-----
|
85
|
+
NapistuGraph inherits from igraph.Graph, so all standard igraph methods
|
86
|
+
(degree, shortest_paths, betweenness, etc.) are available. The additional
|
87
|
+
functionality is designed specifically for molecular network analysis.
|
88
|
+
|
89
|
+
Edge reversal swaps 'from'/'to' attributes, negates stoichiometry values,
|
90
|
+
and updates direction metadata according to predefined mapping rules.
|
91
|
+
"""
|
92
|
+
|
93
|
+
def __init__(self, *args, **kwargs):
|
94
|
+
"""
|
95
|
+
Initialize a NapistuGraph.
|
96
|
+
|
97
|
+
Accepts all the same arguments as igraph.Graph constructor.
|
98
|
+
"""
|
99
|
+
super().__init__(*args, **kwargs)
|
100
|
+
|
101
|
+
# Initialize metadata
|
102
|
+
self._metadata = {
|
103
|
+
"is_reversed": False,
|
104
|
+
"graph_type": None,
|
105
|
+
"weighting_strategy": None,
|
106
|
+
"creation_params": {},
|
107
|
+
}
|
108
|
+
|
109
|
+
@classmethod
|
110
|
+
def from_igraph(cls, graph: ig.Graph, **metadata) -> "NapistuGraph":
|
111
|
+
"""
|
112
|
+
Create a NapistuGraph from an existing igraph.Graph.
|
113
|
+
|
114
|
+
Parameters
|
115
|
+
----------
|
116
|
+
graph : ig.Graph
|
117
|
+
The igraph to convert
|
118
|
+
**metadata : dict
|
119
|
+
Additional metadata to store with the graph
|
120
|
+
|
121
|
+
Returns
|
122
|
+
-------
|
123
|
+
NapistuGraph
|
124
|
+
A new NapistuGraph instance
|
125
|
+
"""
|
126
|
+
# Create new instance with same structure
|
127
|
+
new_graph = cls(
|
128
|
+
n=graph.vcount(),
|
129
|
+
edges=[(e.source, e.target) for e in graph.es],
|
130
|
+
directed=graph.is_directed(),
|
131
|
+
)
|
132
|
+
|
133
|
+
# Copy all vertex attributes
|
134
|
+
for attr in graph.vs.attributes():
|
135
|
+
new_graph.vs[attr] = graph.vs[attr]
|
136
|
+
|
137
|
+
# Copy all edge attributes
|
138
|
+
for attr in graph.es.attributes():
|
139
|
+
new_graph.es[attr] = graph.es[attr]
|
140
|
+
|
141
|
+
# Copy graph attributes
|
142
|
+
for attr in graph.attributes():
|
143
|
+
new_graph[attr] = graph[attr]
|
144
|
+
|
145
|
+
# Set metadata
|
146
|
+
new_graph._metadata.update(metadata)
|
147
|
+
|
148
|
+
return new_graph
|
149
|
+
|
150
|
+
def reverse_edges(self) -> None:
|
151
|
+
"""
|
152
|
+
Reverse all edges in the graph.
|
153
|
+
|
154
|
+
This swaps edge directions and updates all associated attributes
|
155
|
+
according to the edge reversal mapping utilities. Modifies the graph in-place.
|
156
|
+
|
157
|
+
Returns
|
158
|
+
-------
|
159
|
+
None
|
160
|
+
"""
|
161
|
+
# Get current edge dataframe
|
162
|
+
edges_df = self.get_edge_dataframe()
|
163
|
+
|
164
|
+
# Apply systematic attribute swapping using utilities
|
165
|
+
reversed_edges_df = _apply_edge_reversal_mapping(edges_df)
|
166
|
+
|
167
|
+
# Handle special cases using utilities
|
168
|
+
reversed_edges_df = _handle_special_reversal_cases(reversed_edges_df)
|
169
|
+
|
170
|
+
# Update edge attributes
|
171
|
+
for attr in reversed_edges_df.columns:
|
172
|
+
if attr in self.es.attributes():
|
173
|
+
self.es[attr] = reversed_edges_df[attr].values
|
174
|
+
|
175
|
+
# Update metadata
|
176
|
+
self._metadata["is_reversed"] = not self._metadata["is_reversed"]
|
177
|
+
|
178
|
+
logger.info(
|
179
|
+
f"Reversed graph edges. Current state: reversed={self._metadata['is_reversed']}"
|
180
|
+
)
|
181
|
+
|
182
|
+
return None
|
183
|
+
|
184
|
+
@property
|
185
|
+
def is_reversed(self) -> bool:
|
186
|
+
"""Check if the graph has been reversed."""
|
187
|
+
return self._metadata["is_reversed"]
|
188
|
+
|
189
|
+
@property
|
190
|
+
def graph_type(self) -> Optional[str]:
|
191
|
+
"""Get the graph type (bipartite, regulatory, etc.)."""
|
192
|
+
return self._metadata["graph_type"]
|
193
|
+
|
194
|
+
@property
|
195
|
+
def weighting_strategy(self) -> Optional[str]:
|
196
|
+
"""Get the weighting strategy used."""
|
197
|
+
return self._metadata["weighting_strategy"]
|
198
|
+
|
199
|
+
def set_metadata(self, **kwargs) -> None:
|
200
|
+
"""
|
201
|
+
Set metadata for the graph.
|
202
|
+
|
203
|
+
Modifies the graph's metadata in-place.
|
204
|
+
|
205
|
+
Parameters
|
206
|
+
----------
|
207
|
+
**kwargs : dict
|
208
|
+
Metadata key-value pairs to set
|
209
|
+
"""
|
210
|
+
self._metadata.update(kwargs)
|
211
|
+
|
212
|
+
return None
|
213
|
+
|
214
|
+
def get_metadata(self, key: Optional[str] = None) -> Any:
|
215
|
+
"""
|
216
|
+
Get metadata from the graph.
|
217
|
+
|
218
|
+
Parameters
|
219
|
+
----------
|
220
|
+
key : str, optional
|
221
|
+
Specific metadata key to retrieve. If None, returns all metadata.
|
222
|
+
|
223
|
+
Returns
|
224
|
+
-------
|
225
|
+
Any
|
226
|
+
The requested metadata value, or all metadata if key is None
|
227
|
+
"""
|
228
|
+
if key is None:
|
229
|
+
return self._metadata.copy()
|
230
|
+
return self._metadata.get(key)
|
231
|
+
|
232
|
+
def copy(self) -> "NapistuGraph":
|
233
|
+
"""
|
234
|
+
Create a deep copy of the NapistuGraph.
|
235
|
+
|
236
|
+
Returns
|
237
|
+
-------
|
238
|
+
NapistuGraph
|
239
|
+
A deep copy of this graph including metadata
|
240
|
+
"""
|
241
|
+
# Use igraph's copy method to get the graph structure and attributes
|
242
|
+
new_graph = super().copy()
|
243
|
+
|
244
|
+
# Convert to NapistuGraph and copy metadata
|
245
|
+
napistu_copy = NapistuGraph.from_igraph(new_graph)
|
246
|
+
napistu_copy._metadata = copy.deepcopy(self._metadata)
|
247
|
+
|
248
|
+
return napistu_copy
|
249
|
+
|
250
|
+
def __str__(self) -> str:
|
251
|
+
"""String representation including metadata."""
|
252
|
+
base_str = super().__str__()
|
253
|
+
metadata_str = (
|
254
|
+
f"Reversed: {self.is_reversed}, "
|
255
|
+
f"Type: {self.graph_type}, "
|
256
|
+
f"Weighting: {self.weighting_strategy}"
|
257
|
+
)
|
258
|
+
return f"{base_str}\nNapistuGraph metadata: {metadata_str}"
|
259
|
+
|
260
|
+
def __repr__(self) -> str:
|
261
|
+
"""Detailed representation."""
|
262
|
+
return self.__str__()
|
263
|
+
|
264
|
+
|
265
|
+
def _apply_edge_reversal_mapping(edges_df: pd.DataFrame) -> pd.DataFrame:
|
266
|
+
"""
|
267
|
+
Apply systematic attribute mapping for edge reversal.
|
268
|
+
|
269
|
+
This function swaps paired attributes according to EDGE_REVERSAL_ATTRIBUTE_MAPPING.
|
270
|
+
For example, 'from' becomes 'to', 'weights' becomes 'upstream_weights', etc.
|
271
|
+
|
272
|
+
Parameters
|
273
|
+
----------
|
274
|
+
edges_df : pd.DataFrame
|
275
|
+
Current edge attributes
|
276
|
+
|
277
|
+
Returns
|
278
|
+
-------
|
279
|
+
pd.DataFrame
|
280
|
+
Edge dataframe with swapped attributes
|
281
|
+
|
282
|
+
Warnings
|
283
|
+
--------
|
284
|
+
Logs warnings when expected attribute pairs are missing
|
285
|
+
"""
|
286
|
+
# Find which attributes have pairs in the mapping
|
287
|
+
available_attrs = set(edges_df.columns)
|
288
|
+
|
289
|
+
# Find pairs where both attributes exist
|
290
|
+
valid_mapping = {}
|
291
|
+
missing_pairs = []
|
292
|
+
|
293
|
+
for source_attr, target_attr in EDGE_REVERSAL_ATTRIBUTE_MAPPING.items():
|
294
|
+
if source_attr in available_attrs:
|
295
|
+
if target_attr in available_attrs:
|
296
|
+
valid_mapping[source_attr] = target_attr
|
297
|
+
else:
|
298
|
+
missing_pairs.append(f"{source_attr} -> {target_attr}")
|
299
|
+
|
300
|
+
# Warn about attributes that can't be swapped
|
301
|
+
if missing_pairs:
|
302
|
+
logger.warning(
|
303
|
+
f"The following edge attributes cannot be swapped during reversal "
|
304
|
+
f"because their paired attribute is missing: {', '.join(missing_pairs)}"
|
305
|
+
)
|
306
|
+
|
307
|
+
return edges_df.rename(columns=valid_mapping)
|
308
|
+
|
309
|
+
|
310
|
+
def _handle_special_reversal_cases(edges_df: pd.DataFrame) -> pd.DataFrame:
|
311
|
+
"""
|
312
|
+
Handle special cases that need more than simple attribute swapping.
|
313
|
+
|
314
|
+
This includes:
|
315
|
+
- Flipping stoichiometry signs (* -1)
|
316
|
+
- Mapping direction enums (forward <-> reverse)
|
317
|
+
|
318
|
+
Parameters
|
319
|
+
----------
|
320
|
+
edges_df : pd.DataFrame
|
321
|
+
Edge dataframe after basic attribute swapping
|
322
|
+
|
323
|
+
Returns
|
324
|
+
-------
|
325
|
+
pd.DataFrame
|
326
|
+
Edge dataframe with special cases handled
|
327
|
+
|
328
|
+
Warnings
|
329
|
+
--------
|
330
|
+
Logs warnings when expected attributes are missing
|
331
|
+
"""
|
332
|
+
result_df = edges_df.copy()
|
333
|
+
|
334
|
+
# Handle stoichiometry sign flip
|
335
|
+
if NAPISTU_GRAPH_EDGES.STOICHIOMETRY in result_df.columns:
|
336
|
+
result_df[NAPISTU_GRAPH_EDGES.STOICHIOMETRY] *= -1
|
337
|
+
else:
|
338
|
+
logger.warning(
|
339
|
+
f"Missing expected '{NAPISTU_GRAPH_EDGES.STOICHIOMETRY}' attribute during edge reversal. "
|
340
|
+
"Stoichiometry signs will not be flipped."
|
341
|
+
)
|
342
|
+
|
343
|
+
# Handle direction enum mapping
|
344
|
+
if NAPISTU_GRAPH_EDGES.DIRECTION in result_df.columns:
|
345
|
+
result_df[NAPISTU_GRAPH_EDGES.DIRECTION] = result_df[
|
346
|
+
NAPISTU_GRAPH_EDGES.DIRECTION
|
347
|
+
].map(EDGE_DIRECTION_MAPPING)
|
348
|
+
else:
|
349
|
+
logger.warning(
|
350
|
+
f"Missing expected '{NAPISTU_GRAPH_EDGES.DIRECTION}' attribute during edge reversal. "
|
351
|
+
"Direction metadata will not be updated."
|
352
|
+
)
|
353
|
+
|
354
|
+
return result_df
|