topolib 0.4.2__py3-none-any.whl → 0.6.0__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.
Potentially problematic release.
This version of topolib might be problematic. Click here for more details.
- topolib/assets/AMRES.json +1265 -0
- topolib/assets/Bell_canada.json +925 -0
- topolib/assets/Brazil.json +699 -0
- topolib/assets/CESNET.json +657 -0
- topolib/assets/CORONET.json +1957 -0
- topolib/assets/{China_pop.json → China.json} +109 -37
- topolib/assets/DT-14.json +470 -0
- topolib/assets/DT-17.json +525 -0
- topolib/assets/{DT-50_pop.json → DT-50.json} +165 -65
- topolib/assets/ES-30.json +967 -0
- topolib/assets/EURO-16.json +455 -0
- topolib/assets/FR-43.json +1277 -0
- topolib/assets/FUNET.json +317 -0
- topolib/assets/GCN-BG.json +855 -0
- topolib/assets/GRNET.json +1717 -0
- topolib/assets/HyperOne.json +255 -0
- topolib/assets/IT-21.json +649 -0
- topolib/assets/India.json +517 -0
- topolib/assets/JPN-12.json +331 -0
- topolib/assets/KOREN.json +287 -0
- topolib/assets/NORDUNet.json +783 -0
- topolib/assets/NSFNet.json +399 -0
- topolib/assets/PANEURO.json +757 -0
- topolib/assets/PAVLOV.json +465 -0
- topolib/assets/PLN-12.json +343 -0
- topolib/assets/SANReN.json +161 -0
- topolib/assets/SERBIA-MONTENEGRO.json +139 -0
- topolib/assets/Telefonica-21.json +637 -0
- topolib/assets/Turk_Telekom.json +551 -0
- topolib/assets/UKNet.json +685 -0
- topolib/assets/Vega_Telecom.json +819 -0
- topolib/elements/__init__.py +0 -3
- topolib/elements/link.py +0 -21
- topolib/elements/node.py +0 -2
- topolib/topology/topology.py +349 -8
- topolib/visualization/mapview.py +53 -1
- {topolib-0.4.2.dist-info → topolib-0.6.0.dist-info}/METADATA +10 -4
- topolib-0.6.0.dist-info/RECORD +47 -0
- topolib-0.4.2.dist-info/RECORD +0 -18
- /topolib/assets/{Abilene_IXP.json → Abilene.json} +0 -0
- {topolib-0.4.2.dist-info → topolib-0.6.0.dist-info}/WHEEL +0 -0
- {topolib-0.4.2.dist-info → topolib-0.6.0.dist-info}/licenses/LICENSE +0 -0
topolib/elements/__init__.py
CHANGED
topolib/elements/link.py
CHANGED
|
@@ -9,8 +9,6 @@ if TYPE_CHECKING:
|
|
|
9
9
|
class Link:
|
|
10
10
|
|
|
11
11
|
"""
|
|
12
|
-
.. :noindex:
|
|
13
|
-
|
|
14
12
|
Represents a link between two nodes.
|
|
15
13
|
|
|
16
14
|
Parameters
|
|
@@ -24,17 +22,6 @@ class Link:
|
|
|
24
22
|
length : float
|
|
25
23
|
Length of the link (must be non-negative).
|
|
26
24
|
|
|
27
|
-
Attributes
|
|
28
|
-
----------
|
|
29
|
-
id : int
|
|
30
|
-
Unique identifier for the link.
|
|
31
|
-
source : :class:`topolib.elements.node.Node`
|
|
32
|
-
Source node of the link.
|
|
33
|
-
target : :class:`topolib.elements.node.Node`
|
|
34
|
-
Target node of the link.
|
|
35
|
-
length : float
|
|
36
|
-
Length of the link.
|
|
37
|
-
|
|
38
25
|
Examples
|
|
39
26
|
--------
|
|
40
27
|
>>> link = Link(1, nodeA, nodeB, 10.5)
|
|
@@ -50,8 +37,6 @@ class Link:
|
|
|
50
37
|
@property
|
|
51
38
|
def id(self) -> int:
|
|
52
39
|
"""
|
|
53
|
-
.. :noindex:
|
|
54
|
-
|
|
55
40
|
int: Unique identifier for the link.
|
|
56
41
|
"""
|
|
57
42
|
return self._id
|
|
@@ -66,8 +51,6 @@ class Link:
|
|
|
66
51
|
@property
|
|
67
52
|
def source(self) -> "Node":
|
|
68
53
|
"""
|
|
69
|
-
.. :noindex:
|
|
70
|
-
|
|
71
54
|
:class:`topolib.elements.node.Node`: Source node of the link.
|
|
72
55
|
"""
|
|
73
56
|
return self._source
|
|
@@ -87,8 +70,6 @@ class Link:
|
|
|
87
70
|
@property
|
|
88
71
|
def target(self) -> "Node":
|
|
89
72
|
"""
|
|
90
|
-
.. :noindex:
|
|
91
|
-
|
|
92
73
|
:class:`topolib.elements.node.Node`: Target node of the link.
|
|
93
74
|
"""
|
|
94
75
|
return self._target
|
|
@@ -108,8 +89,6 @@ class Link:
|
|
|
108
89
|
@property
|
|
109
90
|
def length(self) -> float:
|
|
110
91
|
"""
|
|
111
|
-
.. :noindex:
|
|
112
|
-
|
|
113
92
|
float: Length of the link (non-negative).
|
|
114
93
|
"""
|
|
115
94
|
return self._length
|
topolib/elements/node.py
CHANGED
topolib/topology/topology.py
CHANGED
|
@@ -8,9 +8,25 @@ This file uses NetworkX (BSD 3-Clause License):
|
|
|
8
8
|
https://github.com/networkx/networkx/blob/main/LICENSE.txt
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
-
from typing import List,
|
|
11
|
+
from typing import List, Optional, Any
|
|
12
|
+
from numpy.typing import NDArray
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Standard library imports
|
|
16
|
+
import json
|
|
17
|
+
import csv
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
# Third-party imports
|
|
12
21
|
import numpy as np
|
|
13
22
|
import networkx as nx
|
|
23
|
+
import jsonschema
|
|
24
|
+
import matplotlib.pyplot as plt
|
|
25
|
+
import contextily as ctx
|
|
26
|
+
import geopandas as gpd
|
|
27
|
+
from shapely.geometry import Point
|
|
28
|
+
|
|
29
|
+
# Local imports
|
|
14
30
|
from topolib.elements.node import Node
|
|
15
31
|
from topolib.elements.link import Link
|
|
16
32
|
|
|
@@ -62,7 +78,7 @@ class Topology:
|
|
|
62
78
|
self.links = links if links is not None else []
|
|
63
79
|
self.name = name
|
|
64
80
|
# Internal NetworkX graph for algorithms and visualization
|
|
65
|
-
self._graph = nx.
|
|
81
|
+
self._graph: nx.DiGraph[Any] = nx.DiGraph()
|
|
66
82
|
for node in self.nodes:
|
|
67
83
|
self._graph.add_node(node.id, node=node)
|
|
68
84
|
for link in self.links:
|
|
@@ -78,12 +94,51 @@ class Topology:
|
|
|
78
94
|
:return: Topology instance loaded from the file.
|
|
79
95
|
:rtype: Topology
|
|
80
96
|
"""
|
|
81
|
-
import json
|
|
82
|
-
from topolib.elements.node import Node
|
|
83
|
-
from topolib.elements.link import Link
|
|
84
|
-
|
|
85
97
|
with open(json_path, "r", encoding="utf-8") as f:
|
|
86
98
|
data = json.load(f)
|
|
99
|
+
|
|
100
|
+
# Validation schema for the assets JSON format
|
|
101
|
+
topology_schema: dict[str, Any] = {
|
|
102
|
+
"type": "object",
|
|
103
|
+
"properties": {
|
|
104
|
+
"name": {"type": "string"},
|
|
105
|
+
"nodes": {
|
|
106
|
+
"type": "array",
|
|
107
|
+
"items": {
|
|
108
|
+
"type": "object",
|
|
109
|
+
"properties": {
|
|
110
|
+
"id": {"type": "integer"},
|
|
111
|
+
"name": {"type": "string"},
|
|
112
|
+
"weight": {"type": "number"},
|
|
113
|
+
"latitude": {"type": "number"},
|
|
114
|
+
"longitude": {"type": "number"},
|
|
115
|
+
"pop": {"type": "integer"},
|
|
116
|
+
"DC": {"type": "integer"},
|
|
117
|
+
"IXP": {"type": "integer"},
|
|
118
|
+
},
|
|
119
|
+
"required": ["id", "name", "latitude", "longitude"],
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
"links": {
|
|
123
|
+
"type": "array",
|
|
124
|
+
"items": {
|
|
125
|
+
"type": "object",
|
|
126
|
+
"properties": {
|
|
127
|
+
"id": {"type": "integer"},
|
|
128
|
+
"src": {"type": "integer"},
|
|
129
|
+
"dst": {"type": "integer"},
|
|
130
|
+
"length": {"type": "number"},
|
|
131
|
+
},
|
|
132
|
+
"required": ["id", "src", "dst", "length"],
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
"required": ["nodes", "links"],
|
|
137
|
+
}
|
|
138
|
+
try:
|
|
139
|
+
jsonschema.validate(instance=data, schema=topology_schema)
|
|
140
|
+
except jsonschema.ValidationError as e:
|
|
141
|
+
raise ValueError(f"Invalid topology JSON format: {e.message}")
|
|
87
142
|
nodes = [
|
|
88
143
|
Node(
|
|
89
144
|
n["id"],
|
|
@@ -100,7 +155,8 @@ class Topology:
|
|
|
100
155
|
# Crear un dict para mapear id a Node
|
|
101
156
|
node_dict = {n.id: n for n in nodes}
|
|
102
157
|
links = [
|
|
103
|
-
Link(l["id"], node_dict[l["src"]],
|
|
158
|
+
Link(l["id"], node_dict[l["src"]],
|
|
159
|
+
node_dict[l["dst"]], l["length"])
|
|
104
160
|
for l in data["links"]
|
|
105
161
|
]
|
|
106
162
|
name = data.get("name", None)
|
|
@@ -152,7 +208,7 @@ class Topology:
|
|
|
152
208
|
self._graph.remove_edge(link.source.id, link.target.id)
|
|
153
209
|
self.links = [l for l in self.links if l.id != link_id]
|
|
154
210
|
|
|
155
|
-
def adjacency_matrix(self) -> np.
|
|
211
|
+
def adjacency_matrix(self) -> NDArray[np.int_]:
|
|
156
212
|
"""
|
|
157
213
|
Return the adjacency matrix of the topology as a numpy array.
|
|
158
214
|
|
|
@@ -170,3 +226,288 @@ class Topology:
|
|
|
170
226
|
node_ids = [n.id for n in self.nodes]
|
|
171
227
|
mat = nx.to_numpy_array(self._graph, nodelist=node_ids, dtype=int)
|
|
172
228
|
return mat
|
|
229
|
+
|
|
230
|
+
def export_to_json(self, file_path: str) -> None:
|
|
231
|
+
"""
|
|
232
|
+
Export the current topology to the JSON format used in the assets folder.
|
|
233
|
+
|
|
234
|
+
:param file_path: Path where the JSON file will be saved.
|
|
235
|
+
:type file_path: str
|
|
236
|
+
|
|
237
|
+
Example usage::
|
|
238
|
+
|
|
239
|
+
topo.export_to_json("/path/output.json")
|
|
240
|
+
|
|
241
|
+
Example output format::
|
|
242
|
+
|
|
243
|
+
{
|
|
244
|
+
"name": "Abilene",
|
|
245
|
+
"nodes": [
|
|
246
|
+
{
|
|
247
|
+
"id": 0,
|
|
248
|
+
"name": "Seattle",
|
|
249
|
+
...
|
|
250
|
+
}
|
|
251
|
+
],
|
|
252
|
+
"links": [
|
|
253
|
+
{
|
|
254
|
+
"id": 0,
|
|
255
|
+
"src": 0,
|
|
256
|
+
"dst": 1,
|
|
257
|
+
"length": 1482.26
|
|
258
|
+
}
|
|
259
|
+
]
|
|
260
|
+
}
|
|
261
|
+
"""
|
|
262
|
+
nodes_list: list[dict[str, Any]] = []
|
|
263
|
+
for n in self.nodes:
|
|
264
|
+
node_dict: dict[str, Any] = {
|
|
265
|
+
"id": n.id,
|
|
266
|
+
"name": getattr(n, "name", None),
|
|
267
|
+
"weight": getattr(n, "weight", 0),
|
|
268
|
+
"latitude": getattr(n, "latitude", None),
|
|
269
|
+
"longitude": getattr(n, "longitude", None),
|
|
270
|
+
"pop": getattr(n, "pop", 0),
|
|
271
|
+
"DC": getattr(n, "dc", getattr(n, "DC", 0)),
|
|
272
|
+
"IXP": getattr(n, "ixp", getattr(n, "IXP", 0)),
|
|
273
|
+
}
|
|
274
|
+
nodes_list.append(node_dict)
|
|
275
|
+
links_list: list[dict[str, Any]] = []
|
|
276
|
+
for l in self.links:
|
|
277
|
+
link_dict: dict[str, Any] = {
|
|
278
|
+
"id": l.id,
|
|
279
|
+
"src": l.source.id,
|
|
280
|
+
"dst": l.target.id,
|
|
281
|
+
"length": getattr(l, "length", None),
|
|
282
|
+
}
|
|
283
|
+
links_list.append(link_dict)
|
|
284
|
+
data: dict[str, Any] = {
|
|
285
|
+
"name": self.name if self.name else "Topology",
|
|
286
|
+
"nodes": nodes_list,
|
|
287
|
+
"links": links_list,
|
|
288
|
+
}
|
|
289
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
290
|
+
json.dump(data, f, indent=4, ensure_ascii=False)
|
|
291
|
+
|
|
292
|
+
def export_to_csv(self, filename_prefix: str) -> None:
|
|
293
|
+
"""
|
|
294
|
+
Export the topology to two CSV files: one for nodes and one for links.
|
|
295
|
+
The files will be named as <filename_prefix>_nodes.csv and <filename_prefix>_links.csv.
|
|
296
|
+
|
|
297
|
+
:param filename_prefix: Prefix for the output files (e.g., 'topology1').
|
|
298
|
+
:type filename_prefix: str
|
|
299
|
+
|
|
300
|
+
Example:
|
|
301
|
+
>>> topo.export_to_csv("mytopo")
|
|
302
|
+
# Generates 'mytopo_nodes.csv' and 'mytopo_links.csv'
|
|
303
|
+
"""
|
|
304
|
+
# Export nodes
|
|
305
|
+
nodes_path = f"{filename_prefix}_nodes.csv"
|
|
306
|
+
with open(nodes_path, "w", newline="", encoding="utf-8") as f:
|
|
307
|
+
writer = csv.writer(f)
|
|
308
|
+
# Header
|
|
309
|
+
writer.writerow(
|
|
310
|
+
["id", "name", "weight", "latitude",
|
|
311
|
+
"longitude", "pop", "DC", "IXP"]
|
|
312
|
+
)
|
|
313
|
+
for n in self.nodes:
|
|
314
|
+
writer.writerow(
|
|
315
|
+
[
|
|
316
|
+
n.id,
|
|
317
|
+
getattr(n, "name", None),
|
|
318
|
+
getattr(n, "weight", 0),
|
|
319
|
+
getattr(n, "latitude", None),
|
|
320
|
+
getattr(n, "longitude", None),
|
|
321
|
+
getattr(n, "pop", 0),
|
|
322
|
+
getattr(n, "dc", getattr(n, "DC", 0)),
|
|
323
|
+
getattr(n, "ixp", getattr(n, "IXP", 0)),
|
|
324
|
+
]
|
|
325
|
+
)
|
|
326
|
+
# Export links
|
|
327
|
+
links_path = f"{filename_prefix}_links.csv"
|
|
328
|
+
with open(links_path, "w", newline="", encoding="utf-8") as f:
|
|
329
|
+
writer = csv.writer(f)
|
|
330
|
+
writer.writerow(["id", "src", "dst", "length"])
|
|
331
|
+
for l in self.links:
|
|
332
|
+
writer.writerow(
|
|
333
|
+
[
|
|
334
|
+
l.id,
|
|
335
|
+
l.source.id,
|
|
336
|
+
l.target.id,
|
|
337
|
+
getattr(l, "length", None),
|
|
338
|
+
]
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
def export_to_flexnetsim_json(self, file_path: str, slots: int) -> None:
|
|
342
|
+
"""
|
|
343
|
+
Export the current topology to a JSON file compatible with Flex Net Sim.
|
|
344
|
+
|
|
345
|
+
:param file_path: Path where the JSON file will be saved.
|
|
346
|
+
:type file_path: str
|
|
347
|
+
:param slots: Number of slots for each link.
|
|
348
|
+
:type slots: int
|
|
349
|
+
|
|
350
|
+
The generated format includes the following fields:
|
|
351
|
+
- alias: short name of the topology (uses self.name if available)
|
|
352
|
+
- name: full name of the topology (uses self.name if available)
|
|
353
|
+
- nodes: list of nodes with 'id' field
|
|
354
|
+
- links: list of links with id, src, dst, length, slots
|
|
355
|
+
"""
|
|
356
|
+
alias = self.name if self.name else "Topology"
|
|
357
|
+
name = self.name if self.name else "Topology"
|
|
358
|
+
nodes_list = [{"id": n.id} for n in self.nodes]
|
|
359
|
+
links_list = []
|
|
360
|
+
for l in self.links:
|
|
361
|
+
link_dict = {
|
|
362
|
+
"id": l.id,
|
|
363
|
+
"src": l.source.id,
|
|
364
|
+
"dst": l.target.id,
|
|
365
|
+
"length": getattr(l, "length", None),
|
|
366
|
+
"slots": slots,
|
|
367
|
+
}
|
|
368
|
+
links_list.append(link_dict)
|
|
369
|
+
data = {
|
|
370
|
+
"alias": alias,
|
|
371
|
+
"name": name,
|
|
372
|
+
"nodes": nodes_list,
|
|
373
|
+
"links": links_list,
|
|
374
|
+
}
|
|
375
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
376
|
+
json.dump(data, f, indent=4, ensure_ascii=False)
|
|
377
|
+
|
|
378
|
+
def export_to_flexnetsim_ksp_json(self, file_path: str, k: int = 3) -> None:
|
|
379
|
+
"""
|
|
380
|
+
Export the k-shortest paths between all node pairs to a JSON file compatible with Flex Net Sim.
|
|
381
|
+
|
|
382
|
+
:param file_path: Path where the JSON file will be saved.
|
|
383
|
+
:type file_path: str
|
|
384
|
+
:param k: Number of shortest paths to compute for each node pair (default: 3).
|
|
385
|
+
:type k: int
|
|
386
|
+
|
|
387
|
+
Example output format::
|
|
388
|
+
|
|
389
|
+
{
|
|
390
|
+
"name": self.name,
|
|
391
|
+
"alias": self.name,
|
|
392
|
+
"routes": [
|
|
393
|
+
{"src": <id>, "dst": <id>, "paths": [[id, ...], ...]},
|
|
394
|
+
...
|
|
395
|
+
]
|
|
396
|
+
}
|
|
397
|
+
"""
|
|
398
|
+
|
|
399
|
+
# Build a weighted graph using link length as edge weight
|
|
400
|
+
G = nx.DiGraph()
|
|
401
|
+
for l in self.links:
|
|
402
|
+
G.add_edge(l.source.id, l.target.id,
|
|
403
|
+
weight=getattr(l, "length", 1))
|
|
404
|
+
routes = []
|
|
405
|
+
node_ids = [n.id for n in self.nodes]
|
|
406
|
+
for src in node_ids:
|
|
407
|
+
for dst in node_ids:
|
|
408
|
+
if src == dst:
|
|
409
|
+
continue
|
|
410
|
+
try:
|
|
411
|
+
# Compute k shortest paths using link length as weight
|
|
412
|
+
paths_gen = nx.shortest_simple_paths(
|
|
413
|
+
G, src, dst, weight="weight")
|
|
414
|
+
paths = []
|
|
415
|
+
for i, path in enumerate(paths_gen):
|
|
416
|
+
if i >= k:
|
|
417
|
+
break
|
|
418
|
+
paths.append(path)
|
|
419
|
+
except (nx.NetworkXNoPath, nx.NodeNotFound):
|
|
420
|
+
paths = []
|
|
421
|
+
routes.append({"src": src, "dst": dst, "paths": paths})
|
|
422
|
+
data = {
|
|
423
|
+
"name": self.name if self.name else "Topology",
|
|
424
|
+
"alias": self.name if self.name else "Topology",
|
|
425
|
+
"routes": routes,
|
|
426
|
+
}
|
|
427
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
428
|
+
json.dump(data, f, indent=4, ensure_ascii=False)
|
|
429
|
+
|
|
430
|
+
@classmethod
|
|
431
|
+
def list_available_topologies(cls) -> list[dict]:
|
|
432
|
+
"""
|
|
433
|
+
List available topologies in the assets folder.
|
|
434
|
+
|
|
435
|
+
Returns a list of dictionaries with keys:
|
|
436
|
+
- 'name': topology name (filename without extension)
|
|
437
|
+
- 'nodes': number of nodes
|
|
438
|
+
- 'links': number of links
|
|
439
|
+
|
|
440
|
+
:return: List of available topologies with metadata.
|
|
441
|
+
:rtype: list[dict]
|
|
442
|
+
"""
|
|
443
|
+
asset_dir = Path(__file__).parent.parent / "assets"
|
|
444
|
+
result = []
|
|
445
|
+
for json_path in asset_dir.glob("*.json"):
|
|
446
|
+
try:
|
|
447
|
+
topo = cls.from_json(str(json_path))
|
|
448
|
+
result.append({
|
|
449
|
+
"name": json_path.stem,
|
|
450
|
+
"nodes": len(topo.nodes),
|
|
451
|
+
"links": len(topo.links),
|
|
452
|
+
})
|
|
453
|
+
except Exception:
|
|
454
|
+
continue
|
|
455
|
+
return result
|
|
456
|
+
|
|
457
|
+
@staticmethod
|
|
458
|
+
def load_default_topology(name: str) -> "Topology":
|
|
459
|
+
"""
|
|
460
|
+
Load a default topology from the assets folder by name (filename without extension).
|
|
461
|
+
|
|
462
|
+
:param name: Name of the topology asset (without .json extension)
|
|
463
|
+
:type name: str
|
|
464
|
+
:return: Topology instance loaded from the asset file
|
|
465
|
+
:rtype: Topology
|
|
466
|
+
:raises FileNotFoundError: If the asset file does not exist
|
|
467
|
+
"""
|
|
468
|
+
asset_dir = Path(__file__).parent.parent / "assets"
|
|
469
|
+
asset_path = asset_dir / f"{name}.json"
|
|
470
|
+
if not asset_path.exists():
|
|
471
|
+
raise FileNotFoundError(
|
|
472
|
+
f"Topology asset '{name}.json' not found in assets directory.")
|
|
473
|
+
return Topology.from_json(str(asset_path))
|
|
474
|
+
|
|
475
|
+
def export_graph_png(self, filename: str, dpi: int = 150) -> None:
|
|
476
|
+
"""
|
|
477
|
+
Export the topology graph as a PNG image using Matplotlib and Contextily.
|
|
478
|
+
|
|
479
|
+
:param filename: Output PNG file path.
|
|
480
|
+
:type filename: str
|
|
481
|
+
:param dpi: Dots per inch for the saved image (default: 150).
|
|
482
|
+
:type dpi: int
|
|
483
|
+
"""
|
|
484
|
+
lons = [node.longitude for node in self.nodes]
|
|
485
|
+
lats = [node.latitude for node in self.nodes]
|
|
486
|
+
names = [node.name for node in self.nodes]
|
|
487
|
+
gdf = gpd.GeoDataFrame(
|
|
488
|
+
{"name": names},
|
|
489
|
+
geometry=[Point(x, y) for x, y in zip(lons, lats)],
|
|
490
|
+
crs="EPSG:4326",
|
|
491
|
+
)
|
|
492
|
+
gdf = gdf.to_crs(epsg=3857)
|
|
493
|
+
node_id_to_xy = {node.id: (pt.x, pt.y)
|
|
494
|
+
for node, pt in zip(self.nodes, gdf.geometry)}
|
|
495
|
+
topo_name = self.name if self.name else "Topology"
|
|
496
|
+
fig, ax = plt.subplots(figsize=(10, 7))
|
|
497
|
+
fig.suptitle(topo_name, fontsize=16)
|
|
498
|
+
for link in self.links:
|
|
499
|
+
src_id = link.source.id
|
|
500
|
+
tgt_id = link.target.id
|
|
501
|
+
if src_id in node_id_to_xy and tgt_id in node_id_to_xy:
|
|
502
|
+
x0, y0 = node_id_to_xy[src_id]
|
|
503
|
+
x1, y1 = node_id_to_xy[tgt_id]
|
|
504
|
+
ax.plot([x0, x1], [y0, y1], color="gray",
|
|
505
|
+
linewidth=1, alpha=0.7, zorder=2)
|
|
506
|
+
gdf.plot(ax=ax, color="blue", markersize=40, zorder=5)
|
|
507
|
+
for x, y, name in zip(gdf.geometry.x, gdf.geometry.y, gdf["name"]):
|
|
508
|
+
ax.text(x, y, name, fontsize=8, ha="right",
|
|
509
|
+
va="bottom", color="black")
|
|
510
|
+
ax.set_axis_off()
|
|
511
|
+
plt.tight_layout()
|
|
512
|
+
plt.savefig(filename, dpi=dpi)
|
|
513
|
+
plt.close(fig)
|
topolib/visualization/mapview.py
CHANGED
|
@@ -67,9 +67,61 @@ class MapView:
|
|
|
67
67
|
# Draw nodes
|
|
68
68
|
gdf.plot(ax=ax, color="blue", markersize=40, zorder=5)
|
|
69
69
|
for x, y, name in zip(gdf.geometry.x, gdf.geometry.y, gdf["name"]):
|
|
70
|
-
ax.text(x, y, name, fontsize=8, ha="right",
|
|
70
|
+
ax.text(x, y, name, fontsize=8, ha="right",
|
|
71
|
+
va="bottom", color="black")
|
|
71
72
|
ctx.add_basemap(ax, source=ctx.providers.OpenStreetMap.Mapnik)
|
|
72
73
|
ax.set_axis_off()
|
|
73
74
|
ax.set_title(f"Nodes and links ({topo_name})")
|
|
74
75
|
plt.tight_layout()
|
|
75
76
|
plt.show()
|
|
77
|
+
|
|
78
|
+
def export_map_png(self, filename: str, dpi: int = 150) -> None:
|
|
79
|
+
"""
|
|
80
|
+
Export the topology map as a PNG image using Matplotlib and Contextily.
|
|
81
|
+
|
|
82
|
+
:param filename: Output PNG file path.
|
|
83
|
+
:type filename: str
|
|
84
|
+
:param dpi: Dots per inch for the saved image (default: 150).
|
|
85
|
+
:type dpi: int
|
|
86
|
+
"""
|
|
87
|
+
lons: list[float] = [node.longitude for node in self.topology.nodes]
|
|
88
|
+
lats: list[float] = [node.latitude for node in self.topology.nodes]
|
|
89
|
+
names: list[str] = [node.name for node in self.topology.nodes]
|
|
90
|
+
gdf: gpd.GeoDataFrame = gpd.GeoDataFrame(
|
|
91
|
+
{"name": names},
|
|
92
|
+
geometry=[Point(x, y) for x, y in zip(lons, lats)],
|
|
93
|
+
crs="EPSG:4326",
|
|
94
|
+
)
|
|
95
|
+
gdf = gdf.to_crs(epsg=3857)
|
|
96
|
+
|
|
97
|
+
node_id_to_xy = {
|
|
98
|
+
node.id: (pt.x, pt.y) for node, pt in zip(self.topology.nodes, gdf.geometry)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
topo_name = getattr(self.topology, "name", None)
|
|
102
|
+
if topo_name is None:
|
|
103
|
+
topo_name = getattr(self.topology, "_name", None)
|
|
104
|
+
if topo_name is None:
|
|
105
|
+
topo_name = "Topology"
|
|
106
|
+
|
|
107
|
+
fig, ax = plt.subplots(figsize=(10, 7))
|
|
108
|
+
fig.suptitle(topo_name, fontsize=16)
|
|
109
|
+
for link in getattr(self.topology, "links", []):
|
|
110
|
+
src_id = getattr(link, "source").id
|
|
111
|
+
tgt_id = getattr(link, "target").id
|
|
112
|
+
if src_id in node_id_to_xy and tgt_id in node_id_to_xy:
|
|
113
|
+
x0, y0 = node_id_to_xy[src_id]
|
|
114
|
+
x1, y1 = node_id_to_xy[tgt_id]
|
|
115
|
+
ax.plot(
|
|
116
|
+
[x0, x1], [y0, y1], color="gray", linewidth=1, alpha=0.7, zorder=2
|
|
117
|
+
)
|
|
118
|
+
gdf.plot(ax=ax, color="blue", markersize=40, zorder=5)
|
|
119
|
+
for x, y, name in zip(gdf.geometry.x, gdf.geometry.y, gdf["name"]):
|
|
120
|
+
ax.text(x, y, name, fontsize=8, ha="right",
|
|
121
|
+
va="bottom", color="black")
|
|
122
|
+
ctx.add_basemap(ax, source=ctx.providers.OpenStreetMap.Mapnik)
|
|
123
|
+
ax.set_axis_off()
|
|
124
|
+
ax.set_title(f"Nodes and links ({topo_name})")
|
|
125
|
+
plt.tight_layout()
|
|
126
|
+
plt.savefig(filename, dpi=dpi)
|
|
127
|
+
plt.close(fig)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: topolib
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: A compact Python library for modeling, analyzing, and visualizing optical network topologies.
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -21,6 +21,7 @@ Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
|
21
21
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
22
|
Requires-Dist: contextily
|
|
23
23
|
Requires-Dist: geopandas
|
|
24
|
+
Requires-Dist: jsonschema (>=4.0)
|
|
24
25
|
Requires-Dist: matplotlib
|
|
25
26
|
Requires-Dist: networkx (>=2.6)
|
|
26
27
|
Requires-Dist: numpy (>=1.21)
|
|
@@ -49,9 +50,14 @@ Description-Content-Type: text/markdown
|
|
|
49
50
|
|
|
50
51
|
## 📂 Examples
|
|
51
52
|
|
|
52
|
-
|
|
53
|
-
-
|
|
54
|
-
|
|
53
|
+
|
|
54
|
+
Explore ready-to-run usage examples in the [`examples/`](examples/) folder!
|
|
55
|
+
|
|
56
|
+
- [Show topology on a map](examples/show_topology_in_map.py) 🗺️
|
|
57
|
+
- [Show default topology in map](examples/show_default_topology_in_map.py) 🗺️
|
|
58
|
+
- [Export topology as PNG](examples/export_topology_png.py) 🖼️
|
|
59
|
+
- [Export topology to CSV and JSON](examples/export_csv_json.py) 📄
|
|
60
|
+
- [Export topology and k-shortest paths for FlexNetSim](examples/export_flexnetsim.py) 🔀
|
|
55
61
|
|
|
56
62
|
---
|
|
57
63
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
topolib/__init__.py,sha256=iLmy2rOkHS_4KZWMD8BgT7R3tLMKeaTCDVf3B4FyYxM,91
|
|
2
|
+
topolib/analysis/__init__.py,sha256=qvUC9wV_jQNQIlwJXk7LJ-dkTXhH1Ttn0lKWIdwBbrc,52
|
|
3
|
+
topolib/analysis/metrics.py,sha256=2o5PlMVzepDWwSQjzamtKemggqkxh9JuzzCUaeahfok,2555
|
|
4
|
+
topolib/assets/AMRES.json,sha256=PWQLEOOIpQzDelkWg27EUTg331Ewlqm-jIQGMNyIIdo,27082
|
|
5
|
+
topolib/assets/Abilene.json,sha256=5cnPMbjsNDjmpySdQDYQ2yd83zOFvCRjlAnbX1wyzgk,6010
|
|
6
|
+
topolib/assets/Bell_canada.json,sha256=pjsZBZcp6j-IP57P8MOlcBqnfnLkzbintKzgJxcC7ZE,19921
|
|
7
|
+
topolib/assets/Brazil.json,sha256=_4rSY46CKBd6ylpLLigaUuWjLrP83OYiYorg4wChmFU,14876
|
|
8
|
+
topolib/assets/CESNET.json,sha256=i6ebe-cDZJtTCDbbeuBo-dQwb7dhD4SRdc0I0N0Eczo,13739
|
|
9
|
+
topolib/assets/CORONET.json,sha256=dSpBSR-vBdxeG1IPcnjw5YYwpzujKMceBuLghGt3xQQ,42017
|
|
10
|
+
topolib/assets/China.json,sha256=L3co98t0MYzPApUi0Hv9i-QthDPHZQSsGcCvunspsGA,23820
|
|
11
|
+
topolib/assets/DT-14.json,sha256=IyNZIWelc2lW1eOa6nHOH4HRfCIOGZtBdwFUQhiS5is,9924
|
|
12
|
+
topolib/assets/DT-17.json,sha256=LBqGD3e3ad9nSaFlnJnrIWMk1eEVC6uF6zDElFzPUu8,10972
|
|
13
|
+
topolib/assets/DT-50.json,sha256=4Pe6R3eWaiHB_KOWO2Pprm3veHl6ttZA2dPyh_GxEGQ,31869
|
|
14
|
+
topolib/assets/ES-30.json,sha256=gXrrOGNveIlWNvO214FHcGo08pCCs1mElCRHXdvIJGY,20186
|
|
15
|
+
topolib/assets/EURO-16.json,sha256=LEd_oq9nY9XP-5wzrCcmhe3VI0D_iDlaNg1lAjeRHik,9513
|
|
16
|
+
topolib/assets/FR-43.json,sha256=QK4-95ETMw4C98jMzpgkpL8y2vlSkbiS7mN9-9QCEPw,26797
|
|
17
|
+
topolib/assets/FUNET.json,sha256=RtVbWVmJHGO-a8gSqCiGWkuxjfou-Ep1JWV4b9M2qIc,6650
|
|
18
|
+
topolib/assets/GCN-BG.json,sha256=e24fO_IlVUu6x9S2zpSJWBD7S9Obkw39TkHcKGzayDQ,18299
|
|
19
|
+
topolib/assets/GRNET.json,sha256=xJMwR4DGANRBcLdjv_8gWQp3ymuSSCR2Cws9_sAx9eg,36781
|
|
20
|
+
topolib/assets/HyperOne.json,sha256=jP-BT5jmozezD0AgOkwdb6nXyqiiL6TMy6BjjVOKEGE,5264
|
|
21
|
+
topolib/assets/IT-21.json,sha256=GmYsmC8jycG1WYGwRvVJtnrurZQ7uy8LN0Kmb8Zmg6w,13503
|
|
22
|
+
topolib/assets/India.json,sha256=7wiXKgWBIO0THTpV8pNE8TCriV-zfytI2lPKK6Y3Zoc,10839
|
|
23
|
+
topolib/assets/JPN-12.json,sha256=qz4uZAjE4GvLQnlBpKAiC9Zrp197r5hI8029hsRlDEY,6902
|
|
24
|
+
topolib/assets/KOREN.json,sha256=lKW2if-IdGxQsiX6vpysXpYw__3_6OTWeZ9-oD-h5Eg,5951
|
|
25
|
+
topolib/assets/NORDUNet.json,sha256=X934bx4bTyn98ASz8Eb0-UuEBxnR-3vTy14-gFn8Oms,16412
|
|
26
|
+
topolib/assets/NSFNet.json,sha256=LeU7h6H76HXEyIpUhlzuR6cbs6Q61eEgxmyUMaNUeEk,8375
|
|
27
|
+
topolib/assets/PANEURO.json,sha256=Xh_pmTBOwlusDhAUGpPzJz4tSRXbK-FTsN3Dnlbwf48,15969
|
|
28
|
+
topolib/assets/PAVLOV.json,sha256=YRDqlodGj-zqErX1cwk4bX-XuUkgP1CpiOmtlLIBmIk,9816
|
|
29
|
+
topolib/assets/PLN-12.json,sha256=oxKs0MTe-pEqD-kTa_LATWfDRswwAnlEFubeQlXMFRU,7135
|
|
30
|
+
topolib/assets/SANReN.json,sha256=VUe7JFbzmte6ED3lNyfm1cWhefsapeZ4P-zIG6-rZXc,3394
|
|
31
|
+
topolib/assets/SERBIA-MONTENEGRO.json,sha256=83Mk5yNju7B50JolE1uCwy8wQJgsaBzZ71xxhMGYOSc,2897
|
|
32
|
+
topolib/assets/Telefonica-21.json,sha256=Wom5SQn3lLL-z-ESZ3Du_Fz_ZQ2bzP2_bNhzmnmmW_I,13302
|
|
33
|
+
topolib/assets/Turk_Telekom.json,sha256=5yO0cgtJOmh6ulCCIb8g_nDFSmv8Wn52Yt_Oyoc1Y0w,11533
|
|
34
|
+
topolib/assets/UKNet.json,sha256=9BEkRM9YFeMMpih5kyDY34TsOGFzt5KBGsmgWd6KoB0,14210
|
|
35
|
+
topolib/assets/Vega_Telecom.json,sha256=E07ZCvG4exRj1a5DV8lqS3Sdo5tRm_Lc07IzIjP2EW0,17324
|
|
36
|
+
topolib/elements/__init__.py,sha256=RIQGFgI3_R7Saf679LaP8o8D8kVtM-JadZt6XufJcQ4,75
|
|
37
|
+
topolib/elements/link.py,sha256=oTOq_Uqj8YP8VQXOpQv9Yf2A2ozhpzEAZgFV6lRER0I,3377
|
|
38
|
+
topolib/elements/node.py,sha256=ZP-2MmtJBZkqMyoN2GEEDXrt2YfPJX6LbzLkM5upb-k,4956
|
|
39
|
+
topolib/topology/__init__.py,sha256=2VRhVm4ZvKBiTDB2T8BDmLZBpwGCVFRF3fvtxxC_d28,86
|
|
40
|
+
topolib/topology/path.py,sha256=oUNwmpBcS6LMMAJIxokROm3MVqr7vRR44M3Fh5ADq_w,2057
|
|
41
|
+
topolib/topology/topology.py,sha256=LfMfBtxCu8GIJJG3Dmmw_hIQwDWDTeDA31zwOpBcVnc,18354
|
|
42
|
+
topolib/visualization/__init__.py,sha256=wv065-KB5uDbTaQIASPVfMMW5sE76Bs-q0oai48vAzk,29
|
|
43
|
+
topolib/visualization/mapview.py,sha256=pgexEKu4ibet8lmGaEGLuAh6SGRdRSWFGXLS1xSgkbA,4968
|
|
44
|
+
topolib-0.6.0.dist-info/METADATA,sha256=UTKHu9xJiegA_7mqc0IiDpx7s2wl4CWqdXOrHBCKYCk,4318
|
|
45
|
+
topolib-0.6.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
46
|
+
topolib-0.6.0.dist-info/licenses/LICENSE,sha256=kbnIP0XU6f2ualiTjEawdlU81IGPBbwc-_GF3N-1e9E,1081
|
|
47
|
+
topolib-0.6.0.dist-info/RECORD,,
|
topolib-0.4.2.dist-info/RECORD
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
topolib/__init__.py,sha256=iLmy2rOkHS_4KZWMD8BgT7R3tLMKeaTCDVf3B4FyYxM,91
|
|
2
|
-
topolib/analysis/__init__.py,sha256=qvUC9wV_jQNQIlwJXk7LJ-dkTXhH1Ttn0lKWIdwBbrc,52
|
|
3
|
-
topolib/analysis/metrics.py,sha256=2o5PlMVzepDWwSQjzamtKemggqkxh9JuzzCUaeahfok,2555
|
|
4
|
-
topolib/assets/Abilene_IXP.json,sha256=5cnPMbjsNDjmpySdQDYQ2yd83zOFvCRjlAnbX1wyzgk,6010
|
|
5
|
-
topolib/assets/China_pop.json,sha256=v-bs5qib8-CB81rvBNNM9ssoelrNGuFrPEyRCht9YpY,22262
|
|
6
|
-
topolib/assets/DT-50_pop.json,sha256=hCv1MCvipgdrQ34VT-qXJ68BdATriQ8ZObh0fLPqar4,29719
|
|
7
|
-
topolib/elements/__init__.py,sha256=ZWSd5uwVtppJtMnZ30zrAdzXhUAYIK62RrUaH9rsWu0,180
|
|
8
|
-
topolib/elements/link.py,sha256=BwjdCR8peh7dDpKunZodlIsjKcfvwjKxyQ3tmp1obb8,3778
|
|
9
|
-
topolib/elements/node.py,sha256=uOnljwA3jcVR4utUfI-om4SIt_QBsEQt8QLW9uvjr3Y,4974
|
|
10
|
-
topolib/topology/__init__.py,sha256=2VRhVm4ZvKBiTDB2T8BDmLZBpwGCVFRF3fvtxxC_d28,86
|
|
11
|
-
topolib/topology/path.py,sha256=oUNwmpBcS6LMMAJIxokROm3MVqr7vRR44M3Fh5ADq_w,2057
|
|
12
|
-
topolib/topology/topology.py,sha256=PzPx80TxWMcOwpOCh4fohcWT2YUra-4m04tBjqyMU40,5605
|
|
13
|
-
topolib/visualization/__init__.py,sha256=wv065-KB5uDbTaQIASPVfMMW5sE76Bs-q0oai48vAzk,29
|
|
14
|
-
topolib/visualization/mapview.py,sha256=4iKJMYsH6oFNj9tOcgSKLVRtoYfUuH3XLjShsGwUXcM,2799
|
|
15
|
-
topolib-0.4.2.dist-info/METADATA,sha256=O2V7bL8xtAJOhNVmbLX0mHLj1ic5ZZu_3bbnFFbEzAw,3993
|
|
16
|
-
topolib-0.4.2.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
17
|
-
topolib-0.4.2.dist-info/licenses/LICENSE,sha256=kbnIP0XU6f2ualiTjEawdlU81IGPBbwc-_GF3N-1e9E,1081
|
|
18
|
-
topolib-0.4.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|