|
1 | 1 | from __future__ import annotations |
2 | 2 | import uuid |
3 | | -from typing import Any, Dict, List, Optional, NamedTuple, Hashable |
4 | | -import concurrent.futures |
| 3 | +import base64 |
| 4 | +from dataclasses import dataclass, field |
| 5 | +from typing import Any, Dict |
5 | 6 |
|
6 | 7 |
|
7 | | -from ngraph.lib.graph import MultiDiGraph |
8 | | -from ngraph.lib.common import init_flow_graph |
9 | | -from ngraph.lib.max_flow import calc_max_flow |
| 8 | +def new_base64_uuid() -> str: |
| 9 | + """ |
| 10 | + Generate a Base64-encoded UUID without padding (~22 characters). |
| 11 | + """ |
| 12 | + return base64.urlsafe_b64encode(uuid.uuid4().bytes).decode("ascii").rstrip("=") |
10 | 13 |
|
11 | 14 |
|
12 | | -class LinkID(NamedTuple): |
13 | | - src_node: Hashable |
14 | | - dst_node: Hashable |
15 | | - unique_id: Hashable |
| 15 | +@dataclass(slots=True) |
| 16 | +class Node: |
| 17 | + """ |
| 18 | + Represents a node in the network. |
16 | 19 |
|
| 20 | + Each node is uniquely identified by its name, which is used as the key |
| 21 | + in the Network's node dictionary. |
17 | 22 |
|
18 | | -class Node: |
19 | | - def __init__(self, node_id: str, node_type: str = "simple", **attributes: Dict): |
20 | | - self.node_id: str = node_id |
21 | | - self.node_type: str = node_type |
22 | | - self.attributes: Dict[str, Any] = { |
23 | | - "node_id": node_id, |
24 | | - "node_type": node_type, |
25 | | - "plane_ids": [], |
26 | | - "total_link_capacity": 0, |
27 | | - "non_transit": False, |
28 | | - "transit_only": False, # no local sinks/sources |
29 | | - "lat": 0, |
30 | | - "lon": 0, |
31 | | - } |
32 | | - self.update_attributes(**attributes) |
33 | | - self.sub_nodes: Dict[str, "Node"] = {} # Used if node_type is 'composite' |
34 | | - self.sub_links: Dict[str, "Link"] = {} # Used if node_type is 'composite' |
35 | | - |
36 | | - def add_sub_node(self, sub_node_id: str, **attributes: Any): |
37 | | - # Logic to add a sub-node to a composite node |
38 | | - ... |
39 | | - |
40 | | - def add_sub_link( |
41 | | - self, sub_link_id: str, sub_node1: str, sub_node2: str, **attributes: Any |
42 | | - ): |
43 | | - # Logic to add a sub-link to a composite node |
44 | | - ... |
45 | | - |
46 | | - def update_attributes(self, **attributes: Any): |
47 | | - """ |
48 | | - Update the attributes of the node. |
49 | | - """ |
50 | | - self.attributes.update(attributes) |
| 23 | + :param name: The unique name of the node. |
| 24 | + :param attrs: Optional extra metadata for the node. |
| 25 | + """ |
51 | 26 |
|
| 27 | + name: str |
| 28 | + attrs: Dict[str, Any] = field(default_factory=dict) |
52 | 29 |
|
| 30 | + |
| 31 | +@dataclass(slots=True) |
53 | 32 | class Link: |
54 | | - def __init__( |
55 | | - self, |
56 | | - node1: str, |
57 | | - node2: str, |
58 | | - link_id: Optional[LinkID] = None, |
59 | | - **attributes: Dict, |
60 | | - ): |
61 | | - self.link_id: str = ( |
62 | | - LinkID(node1, node2, str(uuid.uuid4())) if link_id is None else link_id |
63 | | - ) |
64 | | - self.node1: str = node1 |
65 | | - self.node2: str = node2 |
66 | | - self.attributes: Dict[str, Any] = { |
67 | | - "link_id": self.link_id, |
68 | | - "node1": node1, |
69 | | - "node2": node2, |
70 | | - "plane_ids": [], |
71 | | - "capacity": 0, |
72 | | - "metric": 0, |
73 | | - "distance": 0, |
74 | | - } |
75 | | - self.update_attributes(**attributes) |
76 | | - |
77 | | - def update_attributes(self, **attributes: Any): |
| 33 | + """ |
| 34 | + Represents a link connecting two nodes in the network. |
| 35 | +
|
| 36 | + The 'source' and 'target' fields reference node names. A unique link ID |
| 37 | + is auto-generated from the source, target, and a random Base64-encoded UUID, |
| 38 | + allowing multiple distinct links between the same nodes. |
| 39 | +
|
| 40 | + :param source: Unique name of the source node. |
| 41 | + :param target: Unique name of the target node. |
| 42 | + :param capacity: Link capacity (default 1.0). |
| 43 | + :param latency: Link latency (default 1.0). |
| 44 | + :param cost: Link cost (default 1.0). |
| 45 | + :param attrs: Optional extra metadata for the link. |
| 46 | + :param id: Auto-generated unique link identifier. |
| 47 | + """ |
| 48 | + |
| 49 | + source: str |
| 50 | + target: str |
| 51 | + capacity: float = 1.0 |
| 52 | + latency: float = 1.0 |
| 53 | + cost: float = 1.0 |
| 54 | + attrs: Dict[str, Any] = field(default_factory=dict) |
| 55 | + id: str = field(init=False) |
| 56 | + |
| 57 | + def __post_init__(self) -> None: |
78 | 58 | """ |
79 | | - Update the attributes of the link. |
| 59 | + Auto-generate a unique link ID by combining the source, target, |
| 60 | + and a random Base64-encoded UUID. |
80 | 61 | """ |
81 | | - self.attributes.update(attributes) |
| 62 | + self.id = f"{self.source}-{self.target}-{new_base64_uuid()}" |
82 | 63 |
|
83 | 64 |
|
| 65 | +@dataclass(slots=True) |
84 | 66 | class Network: |
85 | | - def __init__(self): |
86 | | - self.planes: Dict[str, MultiDiGraph] = {} # Key is plane_id |
87 | | - self.nodes: Dict[str, Node] = {} # Key is unique node_id |
88 | | - self.links: Dict[str, Link] = {} # Key is unique link_id |
| 67 | + """ |
| 68 | + A container for network nodes and links. |
89 | 69 |
|
90 | | - @staticmethod |
91 | | - def generate_edge_id(from_node: str, to_node: str, link_id: LinkID) -> str: |
92 | | - """ |
93 | | - Generate a unique edge ID for a link between two nodes. |
94 | | - """ |
95 | | - return LinkID(from_node, to_node, link_id[2]) |
96 | | - |
97 | | - def add_plane(self, plane_id: str): |
98 | | - self.planes[plane_id] = init_flow_graph(MultiDiGraph()) |
99 | | - |
100 | | - def add_node( |
101 | | - self, |
102 | | - node_id: str, |
103 | | - plane_ids: Optional[List[str]] = None, |
104 | | - node_type: str = "simple", |
105 | | - **attributes: Any, |
106 | | - ) -> str: |
107 | | - new_node = Node(node_id, node_type, **attributes) |
108 | | - self.nodes[new_node.node_id] = new_node |
109 | | - |
110 | | - if plane_ids is None: |
111 | | - plane_ids = self.planes.keys() |
112 | | - |
113 | | - for plane_id in plane_ids: |
114 | | - self.planes[plane_id].add_node(new_node.node_id, **attributes) |
115 | | - new_node.attributes["plane_ids"].append(plane_id) |
116 | | - return new_node.node_id |
117 | | - |
118 | | - def add_link( |
119 | | - self, |
120 | | - node1: str, |
121 | | - node2: str, |
122 | | - plane_ids: Optional[List[str]] = None, |
123 | | - **attributes: Any, |
124 | | - ) -> str: |
125 | | - new_link = Link(node1, node2, **attributes) |
126 | | - self.links[new_link.link_id] = new_link |
127 | | - |
128 | | - if plane_ids is None: |
129 | | - plane_ids = self.planes.keys() |
130 | | - |
131 | | - for plane_id in plane_ids: |
132 | | - self.planes[plane_id].add_edge( |
133 | | - node1, |
134 | | - node2, |
135 | | - edge_id=self.generate_edge_id(node1, node2, new_link.link_id), |
136 | | - capacity=new_link.attributes["capacity"] / len(plane_ids), |
137 | | - metric=new_link.attributes["metric"], |
138 | | - ) |
139 | | - self.planes[plane_id].add_edge( |
140 | | - node2, |
141 | | - node1, |
142 | | - edge_id=self.generate_edge_id(node2, node1, new_link.link_id), |
143 | | - capacity=new_link.attributes["capacity"] / len(plane_ids), |
144 | | - metric=new_link.attributes["metric"], |
145 | | - ) |
146 | | - new_link.attributes["plane_ids"].append(plane_id) |
147 | | - |
148 | | - # Update the total link capacity of the nodes |
149 | | - self.nodes[node1].attributes["total_link_capacity"] += new_link.attributes[ |
150 | | - "capacity" |
151 | | - ] |
152 | | - self.nodes[node2].attributes["total_link_capacity"] += new_link.attributes[ |
153 | | - "capacity" |
154 | | - ] |
155 | | - return new_link.link_id |
156 | | - |
157 | | - @staticmethod |
158 | | - def plane_max_flow(plane_id, plane_graph, src_node, dst_nodes) -> Optional[float]: |
| 70 | + Nodes are stored in a dictionary keyed by their unique names. |
| 71 | + Links are stored in a dictionary keyed by their auto-generated IDs. |
| 72 | + The 'attrs' dict allows extra network metadata. |
| 73 | +
|
| 74 | + :param nodes: Mapping from node name to Node. |
| 75 | + :param links: Mapping from link id to Link. |
| 76 | + :param attrs: Optional extra metadata for the network. |
| 77 | + """ |
| 78 | + |
| 79 | + nodes: Dict[str, Node] = field(default_factory=dict) |
| 80 | + links: Dict[str, Link] = field(default_factory=dict) |
| 81 | + attrs: Dict[str, Any] = field(default_factory=dict) |
| 82 | + |
| 83 | + def add_node(self, node: Node) -> None: |
159 | 84 | """ |
160 | | - Calculate the maximum flow between src and dst for a single plane. |
161 | | - There can be multiple dst nodes, they all are attached to the same virtual sink node. |
| 85 | + Add a node to the network, keyed by its name. |
| 86 | +
|
| 87 | + :param node: The Node to add. |
162 | 88 | """ |
163 | | - if src_node in plane_graph: |
164 | | - for dst_node in dst_nodes: |
165 | | - if dst_node in plane_graph: |
166 | | - # add a pseudo node to the graph to act as the sink for the max flow calculation |
167 | | - plane_graph.add_edge( |
168 | | - dst_node, |
169 | | - "sink", |
170 | | - edge_id=-1, |
171 | | - capacity=2**32, |
172 | | - metric=0, |
173 | | - flow=0, |
174 | | - flows={}, |
175 | | - ) |
176 | | - if "sink" in plane_graph: |
177 | | - return calc_max_flow(plane_graph, src_node, "sink") |
178 | | - |
179 | | - def calc_max_flow( |
180 | | - self, src_nodes: List[str], dst_nodes: List[str] |
181 | | - ) -> Dict[str, Dict[str, float]]: |
| 89 | + self.nodes[node.name] = node |
| 90 | + |
| 91 | + def add_link(self, link: Link) -> None: |
182 | 92 | """ |
183 | | - Calculate the maximum flow between each of the src nodes and all of the dst nodes. |
184 | | - All the dst nodes are attached to the same virtual sink node. |
185 | | - Runs the calculation in parallel for all planes and src nodes. |
| 93 | + Add a link to the network. Both source and target nodes must exist. |
| 94 | +
|
| 95 | + :param link: The Link to add. |
| 96 | + :raises ValueError: If the source or target node is not present. |
186 | 97 | """ |
187 | | - with concurrent.futures.ProcessPoolExecutor() as executor: |
188 | | - future_to_plane_source = {} |
189 | | - for plane_id in self.planes: |
190 | | - for src_node in src_nodes: |
191 | | - future_to_plane_source[ |
192 | | - executor.submit( |
193 | | - self.plane_max_flow, |
194 | | - plane_id, |
195 | | - self.planes[plane_id], |
196 | | - src_node, |
197 | | - dst_nodes, |
198 | | - ) |
199 | | - ] = (plane_id, src_node, dst_nodes) |
200 | | - |
201 | | - results = {} |
202 | | - for future in concurrent.futures.as_completed(future_to_plane_source): |
203 | | - plane_id, src_node, dst_nodes = future_to_plane_source[future] |
204 | | - results.setdefault(src_node, {}) |
205 | | - results[src_node].setdefault(tuple(dst_nodes), {}) |
206 | | - results[src_node][tuple(dst_nodes)][plane_id] = future.result() |
207 | | - return results |
| 98 | + if link.source not in self.nodes: |
| 99 | + raise ValueError(f"Source node '{link.source}' not found in network.") |
| 100 | + if link.target not in self.nodes: |
| 101 | + raise ValueError(f"Target node '{link.target}' not found in network.") |
| 102 | + self.links[link.id] = link |
0 commit comments