risk-network 0.0.7b12__py3-none-any.whl → 0.0.8__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.
- risk/__init__.py +1 -1
- risk/annotations/__init__.py +1 -1
- risk/annotations/annotations.py +85 -53
- risk/annotations/io.py +3 -3
- risk/log/__init__.py +1 -1
- risk/log/{config.py → console.py} +2 -2
- risk/log/params.py +6 -6
- risk/neighborhoods/community.py +68 -61
- risk/neighborhoods/domains.py +41 -18
- risk/neighborhoods/neighborhoods.py +134 -69
- risk/network/geometry.py +5 -2
- risk/network/graph.py +69 -235
- risk/network/io.py +44 -6
- risk/network/plot/__init__.py +6 -0
- risk/network/plot/canvas.py +290 -0
- risk/network/plot/contour.py +327 -0
- risk/network/plot/labels.py +929 -0
- risk/network/plot/network.py +288 -0
- risk/network/plot/plotter.py +137 -0
- risk/network/plot/utils/color.py +424 -0
- risk/network/plot/utils/layout.py +91 -0
- risk/risk.py +70 -37
- risk/stats/hypergeom.py +1 -1
- risk/stats/permutation/permutation.py +21 -8
- risk/stats/poisson.py +2 -2
- risk/stats/stats.py +12 -10
- {risk_network-0.0.7b12.dist-info → risk_network-0.0.8.dist-info}/METADATA +84 -21
- risk_network-0.0.8.dist-info/RECORD +37 -0
- {risk_network-0.0.7b12.dist-info → risk_network-0.0.8.dist-info}/WHEEL +1 -1
- risk/network/plot.py +0 -1450
- risk_network-0.0.7b12.dist-info/RECORD +0 -30
- {risk_network-0.0.7b12.dist-info → risk_network-0.0.8.dist-info}/LICENSE +0 -0
- {risk_network-0.0.7b12.dist-info → risk_network-0.0.8.dist-info}/top_level.txt +0 -0
risk/network/graph.py
CHANGED
@@ -4,12 +4,11 @@ risk/network/graph
|
|
4
4
|
"""
|
5
5
|
|
6
6
|
from collections import defaultdict
|
7
|
-
from typing import Any, Dict, List
|
7
|
+
from typing import Any, Dict, List
|
8
8
|
|
9
9
|
import networkx as nx
|
10
10
|
import numpy as np
|
11
11
|
import pandas as pd
|
12
|
-
import matplotlib
|
13
12
|
|
14
13
|
|
15
14
|
class NetworkGraph:
|
@@ -37,7 +36,7 @@ class NetworkGraph:
|
|
37
36
|
top_annotations (pd.DataFrame): DataFrame containing annotations data for the network nodes.
|
38
37
|
domains (pd.DataFrame): DataFrame containing domain data for the network nodes.
|
39
38
|
trimmed_domains (pd.DataFrame): DataFrame containing trimmed domain data for the network nodes.
|
40
|
-
node_label_to_node_id_map (
|
39
|
+
node_label_to_node_id_map (Dict[str, Any]): A dictionary mapping node labels to their corresponding IDs.
|
41
40
|
node_enrichment_sums (np.ndarray): Array containing the enrichment sums for the nodes.
|
42
41
|
"""
|
43
42
|
self.top_annotations = top_annotations
|
@@ -46,7 +45,14 @@ class NetworkGraph:
|
|
46
45
|
self.domain_id_to_domain_terms_map = self._create_domain_id_to_domain_terms_map(
|
47
46
|
trimmed_domains
|
48
47
|
)
|
48
|
+
self.domain_id_to_domain_info_map = self._create_domain_id_to_domain_info_map(
|
49
|
+
trimmed_domains
|
50
|
+
)
|
51
|
+
self.trimmed_domains = trimmed_domains
|
49
52
|
self.node_enrichment_sums = node_enrichment_sums
|
53
|
+
self.node_id_to_domain_ids_and_enrichments_map = (
|
54
|
+
self._create_node_id_to_domain_ids_and_enrichments(domains)
|
55
|
+
)
|
50
56
|
self.node_id_to_node_label_map = {v: k for k, v in node_label_to_node_id_map.items()}
|
51
57
|
self.node_label_to_enrichment_map = dict(
|
52
58
|
zip(node_label_to_node_id_map.keys(), node_enrichment_sums)
|
@@ -58,179 +64,107 @@ class NetworkGraph:
|
|
58
64
|
self.network = _unfold_sphere_to_plane(network)
|
59
65
|
self.node_coordinates = _extract_node_coordinates(self.network)
|
60
66
|
|
61
|
-
|
67
|
+
@staticmethod
|
68
|
+
def _create_domain_id_to_node_ids_map(domains: pd.DataFrame) -> Dict[int, Any]:
|
62
69
|
"""Create a mapping from domains to the list of node IDs belonging to each domain.
|
63
70
|
|
64
71
|
Args:
|
65
72
|
domains (pd.DataFrame): DataFrame containing domain information, including the 'primary domain' for each node.
|
66
73
|
|
67
74
|
Returns:
|
68
|
-
|
75
|
+
Dict[int, Any]: A dictionary where keys are domain IDs and values are lists of node IDs belonging to each domain.
|
69
76
|
"""
|
70
|
-
cleaned_domains_matrix = domains.reset_index()[["index", "
|
71
|
-
node_to_domains_map = cleaned_domains_matrix.set_index("index")["
|
77
|
+
cleaned_domains_matrix = domains.reset_index()[["index", "primary_domain"]]
|
78
|
+
node_to_domains_map = cleaned_domains_matrix.set_index("index")["primary_domain"].to_dict()
|
72
79
|
domain_id_to_node_ids_map = defaultdict(list)
|
73
80
|
for k, v in node_to_domains_map.items():
|
74
81
|
domain_id_to_node_ids_map[v].append(k)
|
75
82
|
|
76
83
|
return domain_id_to_node_ids_map
|
77
84
|
|
78
|
-
|
79
|
-
|
80
|
-
) -> Dict[str, Any]:
|
85
|
+
@staticmethod
|
86
|
+
def _create_domain_id_to_domain_terms_map(trimmed_domains: pd.DataFrame) -> Dict[int, Any]:
|
81
87
|
"""Create a mapping from domain IDs to their corresponding terms.
|
82
88
|
|
83
89
|
Args:
|
84
90
|
trimmed_domains (pd.DataFrame): DataFrame containing domain IDs and their corresponding labels.
|
85
91
|
|
86
92
|
Returns:
|
87
|
-
|
93
|
+
Dict[int, Any]: A dictionary mapping domain IDs to their corresponding terms.
|
88
94
|
"""
|
89
95
|
return dict(
|
90
96
|
zip(
|
91
97
|
trimmed_domains.index,
|
92
|
-
trimmed_domains["
|
98
|
+
trimmed_domains["normalized_description"],
|
93
99
|
)
|
94
100
|
)
|
95
101
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
"""
|
102
|
-
domain_id_to_label_map = {}
|
103
|
-
for domain_id, node_ids in self.domain_id_to_node_ids_map.items():
|
104
|
-
domain_id_to_label_map[domain_id] = [
|
105
|
-
self.node_id_to_node_label_map[node_id] for node_id in node_ids
|
106
|
-
]
|
107
|
-
|
108
|
-
return domain_id_to_label_map
|
109
|
-
|
110
|
-
def get_domain_colors(
|
111
|
-
self,
|
112
|
-
cmap: str = "gist_rainbow",
|
113
|
-
color: Union[str, None] = None,
|
114
|
-
min_scale: float = 0.8,
|
115
|
-
max_scale: float = 1.0,
|
116
|
-
scale_factor: float = 1.0,
|
117
|
-
random_seed: int = 888,
|
118
|
-
) -> np.ndarray:
|
119
|
-
"""Generate composite colors for domains based on enrichment or specified colors.
|
102
|
+
@staticmethod
|
103
|
+
def _create_domain_id_to_domain_info_map(
|
104
|
+
trimmed_domains: pd.DataFrame,
|
105
|
+
) -> Dict[int, Dict[str, Any]]:
|
106
|
+
"""Create a mapping from domain IDs to their corresponding full description and enrichment score.
|
120
107
|
|
121
108
|
Args:
|
122
|
-
|
123
|
-
color (str or None, optional): A specific color to use for all generated colors. Defaults to None.
|
124
|
-
min_scale (float, optional): Minimum intensity scale for the colors generated by the colormap.
|
125
|
-
Controls the dimmest colors. Defaults to 0.8.
|
126
|
-
max_scale (float, optional): Maximum intensity scale for the colors generated by the colormap.
|
127
|
-
Controls the brightest colors. Defaults to 1.0.
|
128
|
-
scale_factor (float, optional): Exponent for adjusting the color scaling based on enrichment scores.
|
129
|
-
A higher value increases contrast by dimming lower scores more. Defaults to 1.0.
|
130
|
-
random_seed (int, optional): Seed for random number generation to ensure reproducibility of color assignments.
|
131
|
-
Defaults to 888.
|
109
|
+
trimmed_domains (pd.DataFrame): DataFrame containing domain IDs, full descriptions, and enrichment scores.
|
132
110
|
|
133
111
|
Returns:
|
134
|
-
|
112
|
+
Dict[int, Dict[str, Any]]: A dictionary mapping domain IDs (int) to a dictionary with 'full_descriptions' and 'enrichment_scores'.
|
135
113
|
"""
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
)
|
148
|
-
|
149
|
-
return transformed_colors
|
150
|
-
|
151
|
-
def _get_composite_node_colors(self, domain_colors: np.ndarray) -> np.ndarray:
|
152
|
-
"""Generate composite colors for nodes based on domain colors and counts.
|
114
|
+
return {
|
115
|
+
int(id_): {
|
116
|
+
"full_descriptions": trimmed_domains.at[id_, "full_descriptions"],
|
117
|
+
"enrichment_scores": trimmed_domains.at[id_, "enrichment_scores"],
|
118
|
+
}
|
119
|
+
for id_ in trimmed_domains.index
|
120
|
+
}
|
121
|
+
|
122
|
+
@staticmethod
|
123
|
+
def _create_node_id_to_domain_ids_and_enrichments(domains: pd.DataFrame) -> Dict[int, Dict]:
|
124
|
+
"""Creates a dictionary mapping each node ID to its corresponding domain IDs and enrichment values.
|
153
125
|
|
154
126
|
Args:
|
155
|
-
|
127
|
+
domains (pd.DataFrame): A DataFrame containing domain information for each node. Assumes the last
|
128
|
+
two columns are 'all domains' and 'primary domain', which are excluded from processing.
|
156
129
|
|
157
130
|
Returns:
|
158
|
-
|
131
|
+
Dict[int, Dict]: A dictionary where the key is the node ID (index of the DataFrame), and the value is another dictionary
|
132
|
+
with 'domain' (a list of domain IDs with non-zero enrichment) and 'enrichment'
|
133
|
+
(a dict of domain IDs and their corresponding enrichment values).
|
159
134
|
"""
|
160
|
-
#
|
161
|
-
|
162
|
-
#
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
135
|
+
# Initialize an empty dictionary to store the result
|
136
|
+
node_id_to_domain_ids_and_enrichments = {}
|
137
|
+
# Get the list of domain columns (excluding 'all domains' and 'primary domain')
|
138
|
+
domain_columns = domains.columns[
|
139
|
+
:-2
|
140
|
+
] # The last two columns are 'all domains' and 'primary domain'
|
141
|
+
# Iterate over each row in the dataframe
|
142
|
+
for idx, row in domains.iterrows():
|
143
|
+
# Get the domains (column names) where the enrichment score is greater than 0
|
144
|
+
all_domains = domain_columns[row[domain_columns] > 0].tolist()
|
145
|
+
# Get the enrichment values for those domains
|
146
|
+
enrichment_values = row[all_domains].to_dict()
|
147
|
+
# Store the result in the dictionary with index as the key
|
148
|
+
node_id_to_domain_ids_and_enrichments[idx] = {
|
149
|
+
"domains": all_domains, # The column names where enrichment > 0
|
150
|
+
"enrichments": enrichment_values, # The actual enrichment values for those columns
|
151
|
+
}
|
152
|
+
|
153
|
+
return node_id_to_domain_ids_and_enrichments
|
179
154
|
|
180
|
-
|
181
|
-
|
182
|
-
color (str or None, optional): A specific color to use for all generated colors. Defaults to None.
|
183
|
-
random_seed (int, optional): Seed for random number generation. Defaults to 888.
|
155
|
+
def _create_domain_id_to_node_labels_map(self) -> Dict[int, List[str]]:
|
156
|
+
"""Create a map from domain IDs to node labels.
|
184
157
|
|
185
158
|
Returns:
|
186
|
-
|
159
|
+
Dict[int, List[str]]: A dictionary mapping domain IDs to the corresponding node labels.
|
187
160
|
"""
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
color=color,
|
194
|
-
random_seed=random_seed,
|
195
|
-
)
|
196
|
-
self.network, self.domain_id_to_node_ids_map
|
197
|
-
return dict(zip(self.domain_id_to_node_ids_map.keys(), domain_colors))
|
198
|
-
|
199
|
-
|
200
|
-
def _transform_colors(
|
201
|
-
colors: np.ndarray,
|
202
|
-
enrichment_sums: np.ndarray,
|
203
|
-
min_scale: float = 0.8,
|
204
|
-
max_scale: float = 1.0,
|
205
|
-
scale_factor: float = 1.0,
|
206
|
-
) -> np.ndarray:
|
207
|
-
"""Transform colors using power scaling to emphasize high enrichment sums more.
|
208
|
-
|
209
|
-
Args:
|
210
|
-
colors (np.ndarray): An array of RGBA colors.
|
211
|
-
enrichment_sums (np.ndarray): An array of enrichment sums corresponding to the colors.
|
212
|
-
min_scale (float, optional): Minimum scale for color intensity. Defaults to 0.8.
|
213
|
-
max_scale (float, optional): Maximum scale for color intensity. Defaults to 1.0.
|
214
|
-
scale_factor (float, optional): Exponent for scaling, where values > 1 increase contrast by dimming small
|
215
|
-
values more. Defaults to 1.0.
|
216
|
-
|
217
|
-
Returns:
|
218
|
-
np.ndarray: The transformed array of RGBA colors with adjusted intensities.
|
219
|
-
"""
|
220
|
-
if min_scale == max_scale:
|
221
|
-
min_scale = max_scale - 10e-6 # Avoid division by zero
|
222
|
-
|
223
|
-
# Normalize the enrichment sums to the range [0, 1]
|
224
|
-
normalized_sums = enrichment_sums / np.max(enrichment_sums)
|
225
|
-
# Apply power scaling to dim lower values and emphasize higher values
|
226
|
-
scaled_sums = normalized_sums**scale_factor
|
227
|
-
# Linearly scale the normalized sums to the range [min_scale, max_scale]
|
228
|
-
scaled_sums = min_scale + (max_scale - min_scale) * scaled_sums
|
229
|
-
# Adjust RGB values based on scaled sums
|
230
|
-
for i in range(3): # Only adjust RGB values
|
231
|
-
colors[:, i] = scaled_sums * colors[:, i]
|
161
|
+
domain_id_to_label_map = {}
|
162
|
+
for domain_id, node_ids in self.domain_id_to_node_ids_map.items():
|
163
|
+
domain_id_to_label_map[domain_id] = [
|
164
|
+
self.node_id_to_node_label_map[node_id] for node_id in node_ids
|
165
|
+
]
|
232
166
|
|
233
|
-
|
167
|
+
return domain_id_to_label_map
|
234
168
|
|
235
169
|
|
236
170
|
def _unfold_sphere_to_plane(G: nx.Graph) -> nx.Graph:
|
@@ -283,103 +217,3 @@ def _extract_node_coordinates(G: nx.Graph) -> np.ndarray:
|
|
283
217
|
}
|
284
218
|
node_coordinates = np.vstack(list(node_positions.values()))
|
285
219
|
return node_coordinates
|
286
|
-
|
287
|
-
|
288
|
-
def _get_colors(
|
289
|
-
network,
|
290
|
-
domain_id_to_node_ids_map,
|
291
|
-
cmap: str = "gist_rainbow",
|
292
|
-
color: Union[str, None] = None,
|
293
|
-
random_seed: int = 888,
|
294
|
-
) -> List[Tuple]:
|
295
|
-
"""Generate a list of RGBA colors based on domain centroids, ensuring that domains
|
296
|
-
close in space get maximally separated colors, while keeping some randomness.
|
297
|
-
|
298
|
-
Args:
|
299
|
-
network (NetworkX graph): The graph representing the network.
|
300
|
-
domain_id_to_node_ids_map (dict): Mapping from domain IDs to lists of node IDs.
|
301
|
-
cmap (str, optional): The name of the colormap to use. Defaults to "gist_rainbow".
|
302
|
-
color (str or None, optional): A specific color to use for all generated colors.
|
303
|
-
random_seed (int): Seed for random number generation. Defaults to 888.
|
304
|
-
|
305
|
-
Returns:
|
306
|
-
List[Tuple]: List of RGBA colors.
|
307
|
-
"""
|
308
|
-
# Set random seed for reproducibility
|
309
|
-
np.random.seed(random_seed)
|
310
|
-
# Determine the number of colors to generate based on the number of domains
|
311
|
-
num_colors_to_generate = len(domain_id_to_node_ids_map)
|
312
|
-
if color:
|
313
|
-
# Generate all colors as the same specified color
|
314
|
-
rgba = matplotlib.colors.to_rgba(color)
|
315
|
-
return [rgba] * num_colors_to_generate
|
316
|
-
|
317
|
-
# Load colormap
|
318
|
-
colormap = matplotlib.colormaps.get_cmap(cmap)
|
319
|
-
# Step 1: Calculate centroids for each domain
|
320
|
-
centroids = _calculate_centroids(network, domain_id_to_node_ids_map)
|
321
|
-
# Step 2: Calculate pairwise distances between centroids
|
322
|
-
centroid_array = np.array(centroids)
|
323
|
-
dist_matrix = np.linalg.norm(centroid_array[:, None] - centroid_array, axis=-1)
|
324
|
-
# Step 3: Assign distant colors to close centroids
|
325
|
-
color_positions = _assign_distant_colors(dist_matrix, num_colors_to_generate)
|
326
|
-
# Step 4: Randomly shift the entire color palette while maintaining relative distances
|
327
|
-
global_shift = np.random.uniform(-0.1, 0.1) # Small global shift to change the overall palette
|
328
|
-
color_positions = (color_positions + global_shift) % 1 # Wrap around to keep within [0, 1]
|
329
|
-
# Step 5: Ensure that all positions remain between 0 and 1
|
330
|
-
color_positions = np.clip(color_positions, 0, 1)
|
331
|
-
|
332
|
-
# Step 6: Generate RGBA colors based on positions
|
333
|
-
return [colormap(pos) for pos in color_positions]
|
334
|
-
|
335
|
-
|
336
|
-
def _calculate_centroids(network, domain_id_to_node_ids_map):
|
337
|
-
"""Calculate the centroid for each domain based on node x and y coordinates in the network.
|
338
|
-
|
339
|
-
Args:
|
340
|
-
network (NetworkX graph): The graph representing the network.
|
341
|
-
domain_id_to_node_ids_map (dict): Mapping from domain IDs to lists of node IDs.
|
342
|
-
|
343
|
-
Returns:
|
344
|
-
List[Tuple[float, float]]: List of centroids (x, y) for each domain.
|
345
|
-
"""
|
346
|
-
centroids = []
|
347
|
-
for domain_id, node_ids in domain_id_to_node_ids_map.items():
|
348
|
-
# Extract x and y coordinates from the network nodes
|
349
|
-
node_positions = np.array(
|
350
|
-
[[network.nodes[node_id]["x"], network.nodes[node_id]["y"]] for node_id in node_ids]
|
351
|
-
)
|
352
|
-
# Compute the centroid as the mean of the x and y coordinates
|
353
|
-
centroid = np.mean(node_positions, axis=0)
|
354
|
-
centroids.append(tuple(centroid))
|
355
|
-
|
356
|
-
return centroids
|
357
|
-
|
358
|
-
|
359
|
-
def _assign_distant_colors(dist_matrix, num_colors_to_generate):
|
360
|
-
"""Assign colors to centroids that are close in space, ensuring stark color differences.
|
361
|
-
|
362
|
-
Args:
|
363
|
-
dist_matrix (ndarray): Matrix of pairwise centroid distances.
|
364
|
-
num_colors_to_generate (int): Number of colors to generate.
|
365
|
-
|
366
|
-
Returns:
|
367
|
-
np.array: Array of color positions in the range [0, 1].
|
368
|
-
"""
|
369
|
-
color_positions = np.zeros(num_colors_to_generate)
|
370
|
-
# Step 1: Sort indices by centroid proximity (based on sum of distances to others)
|
371
|
-
proximity_order = sorted(
|
372
|
-
range(num_colors_to_generate), key=lambda idx: np.sum(dist_matrix[idx])
|
373
|
-
)
|
374
|
-
# Step 2: Assign colors starting with the most distant points in proximity order
|
375
|
-
for i, idx in enumerate(proximity_order):
|
376
|
-
color_positions[idx] = i / num_colors_to_generate
|
377
|
-
|
378
|
-
# Step 3: Adjust colors so that centroids close to one another are maximally distant on the color spectrum
|
379
|
-
half_spectrum = int(num_colors_to_generate / 2)
|
380
|
-
for i in range(half_spectrum):
|
381
|
-
# Split the spectrum so that close centroids are assigned distant colors
|
382
|
-
color_positions[proximity_order[i]] = (i * 2) / num_colors_to_generate
|
383
|
-
color_positions[proximity_order[-(i + 1)]] = ((i * 2) + 1) / num_colors_to_generate
|
384
|
-
|
385
|
-
return color_positions
|
risk/network/io.py
CHANGED
@@ -5,6 +5,7 @@ risk/network/io
|
|
5
5
|
This file contains the code for the RISK class and command-line access.
|
6
6
|
"""
|
7
7
|
|
8
|
+
import copy
|
8
9
|
import json
|
9
10
|
import os
|
10
11
|
import pickle
|
@@ -13,6 +14,7 @@ import zipfile
|
|
13
14
|
from xml.dom import minidom
|
14
15
|
|
15
16
|
import networkx as nx
|
17
|
+
import numpy as np
|
16
18
|
import pandas as pd
|
17
19
|
|
18
20
|
from risk.network.geometry import assign_edge_lengths
|
@@ -153,8 +155,10 @@ class NetworkIO:
|
|
153
155
|
params.log_network(filetype=filetype)
|
154
156
|
self._log_loading(filetype)
|
155
157
|
|
158
|
+
# Important: Make a copy of the network to avoid modifying the original
|
159
|
+
network_copy = copy.deepcopy(network)
|
156
160
|
# Initialize the graph
|
157
|
-
return self._initialize_graph(
|
161
|
+
return self._initialize_graph(network_copy)
|
158
162
|
|
159
163
|
@staticmethod
|
160
164
|
def load_cytoscape_network(
|
@@ -475,16 +479,50 @@ class NetworkIO:
|
|
475
479
|
logger.debug(f"Total edges missing weights: {missing_weights}")
|
476
480
|
|
477
481
|
def _validate_nodes(self, G: nx.Graph) -> None:
|
478
|
-
"""Validate the graph structure and attributes.
|
482
|
+
"""Validate the graph structure and attributes with attribute fallback for positions and labels.
|
479
483
|
|
480
484
|
Args:
|
481
485
|
G (nx.Graph): A NetworkX graph object.
|
482
486
|
"""
|
487
|
+
# Keep track of nodes missing labels
|
488
|
+
nodes_with_missing_labels = []
|
489
|
+
|
483
490
|
for node, attrs in G.nodes(data=True):
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
491
|
+
# Attribute fallback for 'x' and 'y' attributes
|
492
|
+
if "x" not in attrs or "y" not in attrs:
|
493
|
+
if (
|
494
|
+
"pos" in attrs
|
495
|
+
and isinstance(attrs["pos"], (list, tuple, np.ndarray))
|
496
|
+
and len(attrs["pos"]) >= 2
|
497
|
+
):
|
498
|
+
attrs["x"], attrs["y"] = attrs["pos"][
|
499
|
+
:2
|
500
|
+
] # Use only x and y, ignoring z if present
|
501
|
+
else:
|
502
|
+
raise ValueError(
|
503
|
+
f"Node {node} is missing 'x', 'y', and a valid 'pos' attribute."
|
504
|
+
)
|
505
|
+
|
506
|
+
# Attribute fallback for 'label' attribute
|
507
|
+
if "label" not in attrs:
|
508
|
+
# Try alternative attribute names for label
|
509
|
+
if "name" in attrs:
|
510
|
+
attrs["label"] = attrs["name"]
|
511
|
+
elif "id" in attrs:
|
512
|
+
attrs["label"] = attrs["id"]
|
513
|
+
else:
|
514
|
+
# Collect nodes with missing labels
|
515
|
+
nodes_with_missing_labels.append(node)
|
516
|
+
attrs["label"] = str(node) # Use node ID as the label
|
517
|
+
|
518
|
+
# Issue a single warning if any labels were missing
|
519
|
+
if nodes_with_missing_labels:
|
520
|
+
total_nodes = len(G.nodes)
|
521
|
+
fraction_missing_labels = len(nodes_with_missing_labels) / total_nodes
|
522
|
+
logger.warning(
|
523
|
+
f"{len(nodes_with_missing_labels)} out of {total_nodes} nodes "
|
524
|
+
f"({fraction_missing_labels:.2%}) were missing 'label' attributes and were assigned node IDs."
|
525
|
+
)
|
488
526
|
|
489
527
|
def _assign_edge_lengths(self, G: nx.Graph) -> None:
|
490
528
|
"""Prepare the network by adjusting surface depth and calculating edge lengths.
|