topolib 0.5.0__tar.gz → 0.6.0__tar.gz

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.

Files changed (48) hide show
  1. {topolib-0.5.0 → topolib-0.6.0}/PKG-INFO +7 -3
  2. {topolib-0.5.0 → topolib-0.6.0}/README.md +6 -2
  3. {topolib-0.5.0 → topolib-0.6.0}/pyproject.toml +1 -1
  4. topolib-0.6.0/topolib/elements/__init__.py +5 -0
  5. {topolib-0.5.0 → topolib-0.6.0}/topolib/elements/link.py +0 -21
  6. {topolib-0.5.0 → topolib-0.6.0}/topolib/elements/node.py +0 -2
  7. {topolib-0.5.0 → topolib-0.6.0}/topolib/topology/topology.py +143 -47
  8. {topolib-0.5.0 → topolib-0.6.0}/topolib/visualization/mapview.py +53 -1
  9. topolib-0.5.0/topolib/elements/__init__.py +0 -8
  10. {topolib-0.5.0 → topolib-0.6.0}/LICENSE +0 -0
  11. {topolib-0.5.0 → topolib-0.6.0}/topolib/__init__.py +0 -0
  12. {topolib-0.5.0 → topolib-0.6.0}/topolib/analysis/__init__.py +0 -0
  13. {topolib-0.5.0 → topolib-0.6.0}/topolib/analysis/metrics.py +0 -0
  14. {topolib-0.5.0 → topolib-0.6.0}/topolib/assets/AMRES.json +0 -0
  15. {topolib-0.5.0 → topolib-0.6.0}/topolib/assets/Abilene.json +0 -0
  16. {topolib-0.5.0 → topolib-0.6.0}/topolib/assets/Bell_canada.json +0 -0
  17. {topolib-0.5.0 → topolib-0.6.0}/topolib/assets/Brazil.json +0 -0
  18. {topolib-0.5.0 → topolib-0.6.0}/topolib/assets/CESNET.json +0 -0
  19. {topolib-0.5.0 → topolib-0.6.0}/topolib/assets/CORONET.json +0 -0
  20. {topolib-0.5.0 → topolib-0.6.0}/topolib/assets/China.json +0 -0
  21. {topolib-0.5.0 → topolib-0.6.0}/topolib/assets/DT-14.json +0 -0
  22. {topolib-0.5.0 → topolib-0.6.0}/topolib/assets/DT-17.json +0 -0
  23. {topolib-0.5.0 → topolib-0.6.0}/topolib/assets/DT-50.json +0 -0
  24. {topolib-0.5.0 → topolib-0.6.0}/topolib/assets/ES-30.json +0 -0
  25. {topolib-0.5.0 → topolib-0.6.0}/topolib/assets/EURO-16.json +0 -0
  26. {topolib-0.5.0 → topolib-0.6.0}/topolib/assets/FR-43.json +0 -0
  27. {topolib-0.5.0 → topolib-0.6.0}/topolib/assets/FUNET.json +0 -0
  28. {topolib-0.5.0 → topolib-0.6.0}/topolib/assets/GCN-BG.json +0 -0
  29. {topolib-0.5.0 → topolib-0.6.0}/topolib/assets/GRNET.json +0 -0
  30. {topolib-0.5.0 → topolib-0.6.0}/topolib/assets/HyperOne.json +0 -0
  31. {topolib-0.5.0 → topolib-0.6.0}/topolib/assets/IT-21.json +0 -0
  32. {topolib-0.5.0 → topolib-0.6.0}/topolib/assets/India.json +0 -0
  33. {topolib-0.5.0 → topolib-0.6.0}/topolib/assets/JPN-12.json +0 -0
  34. {topolib-0.5.0 → topolib-0.6.0}/topolib/assets/KOREN.json +0 -0
  35. {topolib-0.5.0 → topolib-0.6.0}/topolib/assets/NORDUNet.json +0 -0
  36. {topolib-0.5.0 → topolib-0.6.0}/topolib/assets/NSFNet.json +0 -0
  37. {topolib-0.5.0 → topolib-0.6.0}/topolib/assets/PANEURO.json +0 -0
  38. {topolib-0.5.0 → topolib-0.6.0}/topolib/assets/PAVLOV.json +0 -0
  39. {topolib-0.5.0 → topolib-0.6.0}/topolib/assets/PLN-12.json +0 -0
  40. {topolib-0.5.0 → topolib-0.6.0}/topolib/assets/SANReN.json +0 -0
  41. {topolib-0.5.0 → topolib-0.6.0}/topolib/assets/SERBIA-MONTENEGRO.json +0 -0
  42. {topolib-0.5.0 → topolib-0.6.0}/topolib/assets/Telefonica-21.json +0 -0
  43. {topolib-0.5.0 → topolib-0.6.0}/topolib/assets/Turk_Telekom.json +0 -0
  44. {topolib-0.5.0 → topolib-0.6.0}/topolib/assets/UKNet.json +0 -0
  45. {topolib-0.5.0 → topolib-0.6.0}/topolib/assets/Vega_Telecom.json +0 -0
  46. {topolib-0.5.0 → topolib-0.6.0}/topolib/topology/__init__.py +0 -0
  47. {topolib-0.5.0 → topolib-0.6.0}/topolib/topology/path.py +0 -0
  48. {topolib-0.5.0 → topolib-0.6.0}/topolib/visualization/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: topolib
3
- Version: 0.5.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
@@ -50,8 +50,12 @@ Description-Content-Type: text/markdown
50
50
 
51
51
  ## 📂 Examples
52
52
 
53
- Explore ready-to-run usage examples in the [`examples/`](examples/) folder!
54
- - [Show topology on a map](examples/show_topology_in_map.py) 🗺️
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) 🖼️
55
59
  - [Export topology to CSV and JSON](examples/export_csv_json.py) 📄
56
60
  - [Export topology and k-shortest paths for FlexNetSim](examples/export_flexnetsim.py) 🔀
57
61
 
@@ -17,8 +17,12 @@
17
17
 
18
18
  ## 📂 Examples
19
19
 
20
- Explore ready-to-run usage examples in the [`examples/`](examples/) folder!
21
- - [Show topology on a map](examples/show_topology_in_map.py) 🗺️
20
+
21
+ Explore ready-to-run usage examples in the [`examples/`](examples/) folder!
22
+
23
+ - [Show topology on a map](examples/show_topology_in_map.py) 🗺️
24
+ - [Show default topology in map](examples/show_default_topology_in_map.py) 🗺️
25
+ - [Export topology as PNG](examples/export_topology_png.py) 🖼️
22
26
  - [Export topology to CSV and JSON](examples/export_csv_json.py) 📄
23
27
  - [Export topology and k-shortest paths for FlexNetSim](examples/export_flexnetsim.py) 🔀
24
28
 
@@ -2,7 +2,7 @@
2
2
  [tool.poetry]
3
3
  name = "topolib"
4
4
  # Poetry requires a version field, but poetry-dynamic-versioning will override it
5
- version = "0.5.0"
5
+ version = "0.6.0"
6
6
  description = "A compact Python library for modeling, analyzing, and visualizing optical network topologies."
7
7
  authors = ["Danilo Bórquez-Paredes <danilo.borquez.p@uai.cl>"]
8
8
  license = "MIT"
@@ -0,0 +1,5 @@
1
+
2
+ from .node import Node
3
+ from .link import Link
4
+
5
+ __all__ = ["Node", "Link"]
@@ -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
@@ -9,8 +9,6 @@ from typing import Tuple
9
9
 
10
10
  class Node:
11
11
  """
12
- .. :noindex:
13
-
14
12
  Represents a node in an optical network topology.
15
13
 
16
14
  :param id: Unique identifier for the node.
@@ -10,12 +10,23 @@ https://github.com/networkx/networkx/blob/main/LICENSE.txt
10
10
 
11
11
  from typing import List, Optional, Any
12
12
  from numpy.typing import NDArray
13
- import numpy as np
14
- import networkx as nx
13
+
14
+
15
+ # Standard library imports
15
16
  import json
16
- import jsonschema
17
17
  import csv
18
+ from pathlib import Path
19
+
20
+ # Third-party imports
21
+ import numpy as np
18
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
19
30
  from topolib.elements.node import Node
20
31
  from topolib.elements.link import Link
21
32
 
@@ -144,7 +155,8 @@ class Topology:
144
155
  # Crear un dict para mapear id a Node
145
156
  node_dict = {n.id: n for n in nodes}
146
157
  links = [
147
- Link(l["id"], node_dict[l["src"]], node_dict[l["dst"]], l["length"])
158
+ Link(l["id"], node_dict[l["src"]],
159
+ node_dict[l["dst"]], l["length"])
148
160
  for l in data["links"]
149
161
  ]
150
162
  name = data.get("name", None)
@@ -217,40 +229,35 @@ class Topology:
217
229
 
218
230
  def export_to_json(self, file_path: str) -> None:
219
231
  """
220
- Exporta la topología actual al formato JSON usado en la carpeta assets.
232
+ Export the current topology to the JSON format used in the assets folder.
221
233
 
222
- :param file_path: Ruta donde se guardará el archivo JSON.
234
+ :param file_path: Path where the JSON file will be saved.
223
235
  :type file_path: str
224
236
 
225
- **Ejemplo de uso**
226
- >>> topo.export_to_json("/ruta/salida.json")
227
-
228
- **Ejemplo de formato de salida**
229
- {
230
- "name": "Abilene",
231
- "nodes": [
232
- {
233
- "id": 0,
234
- "name": "Seattle",
235
- "weight": 0,
236
- "longitude": -122.3328481,
237
- "latitude": 47.6061389,
238
- "pop": 780995,
239
- "DC": 58,
240
- "IXP": 5
241
- },
242
- # ...
243
- ],
244
- "links": [
245
- {
246
- "id": 0,
247
- "src": 0,
248
- "dst": 1,
249
- "length": 1482.26
250
- },
251
- # ...
252
- ]
253
- }
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
+ }
254
261
  """
255
262
  nodes_list: list[dict[str, Any]] = []
256
263
  for n in self.nodes:
@@ -300,7 +307,8 @@ class Topology:
300
307
  writer = csv.writer(f)
301
308
  # Header
302
309
  writer.writerow(
303
- ["id", "name", "weight", "latitude", "longitude", "pop", "DC", "IXP"]
310
+ ["id", "name", "weight", "latitude",
311
+ "longitude", "pop", "DC", "IXP"]
304
312
  )
305
313
  for n in self.nodes:
306
314
  writer.writerow(
@@ -376,21 +384,23 @@ class Topology:
376
384
  :param k: Number of shortest paths to compute for each node pair (default: 3).
377
385
  :type k: int
378
386
 
379
- The output format matches Flex Net Sim's routes.json:
380
- {
381
- "name": self.name,
382
- "alias": self.name,
383
- "routes": [
384
- {"src": <id>, "dst": <id>, "paths": [[id, ...], ...]},
385
- ...
386
- ]
387
- }
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
+ }
388
397
  """
389
398
 
390
399
  # Build a weighted graph using link length as edge weight
391
400
  G = nx.DiGraph()
392
401
  for l in self.links:
393
- G.add_edge(l.source.id, l.target.id, weight=getattr(l, "length", 1))
402
+ G.add_edge(l.source.id, l.target.id,
403
+ weight=getattr(l, "length", 1))
394
404
  routes = []
395
405
  node_ids = [n.id for n in self.nodes]
396
406
  for src in node_ids:
@@ -399,7 +409,8 @@ class Topology:
399
409
  continue
400
410
  try:
401
411
  # Compute k shortest paths using link length as weight
402
- paths_gen = nx.shortest_simple_paths(G, src, dst, weight="weight")
412
+ paths_gen = nx.shortest_simple_paths(
413
+ G, src, dst, weight="weight")
403
414
  paths = []
404
415
  for i, path in enumerate(paths_gen):
405
416
  if i >= k:
@@ -415,3 +426,88 @@ class Topology:
415
426
  }
416
427
  with open(file_path, "w", encoding="utf-8") as f:
417
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)
@@ -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", va="bottom", color="black")
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,8 +0,0 @@
1
-
2
- from .node import Node
3
- from .link import Link
4
-
5
- # Hide re-exported Link from Sphinx index to avoid duplicate warnings
6
- Link.__doc__ = """.. :noindex:"""
7
-
8
- __all__ = ["Node", "Link"]
File without changes
File without changes