Skip to content

Commit 33c7322

Browse files
committed
support for risk groups in network, nodes, links
1 parent c716785 commit 33c7322

13 files changed

Lines changed: 1435 additions & 4559 deletions

ngraph/blueprints.py

Lines changed: 389 additions & 138 deletions
Large diffs are not rendered by default.

ngraph/network.py

Lines changed: 73 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,15 @@ class Node:
3030
the key in the Network's node dictionary.
3131
3232
Attributes:
33-
name: Unique identifier for the node.
34-
disabled: Whether the node is disabled (excluded from calculations).
35-
attrs: Additional metadata (e.g., coordinates, region, shared_risk_groups).
33+
name (str): Unique identifier for the node.
34+
disabled (bool): Whether the node is disabled (excluded from calculations).
35+
risk_groups (Set[str]): Set of risk group names this node belongs to.
36+
attrs (Dict[str, Any]): Additional metadata (e.g., coordinates, region).
3637
"""
3738

3839
name: str
3940
disabled: bool = False
41+
risk_groups: Set[str] = field(default_factory=set)
4042
attrs: Dict[str, Any] = field(default_factory=dict)
4143

4244

@@ -46,20 +48,22 @@ class Link:
4648
Represents a directed link between two nodes in the network.
4749
4850
Attributes:
49-
source: Name of the source node.
50-
target: Name of the target node.
51-
capacity: Link capacity (default 1.0).
52-
cost: Link cost (default 1.0).
53-
disabled: Whether the link is disabled.
54-
attrs: Additional metadata (e.g., distance, shared_risk_groups).
55-
id: Auto-generated unique identifier: "{source}|{target}|<base64_uuid>".
51+
source (str): Name of the source node.
52+
target (str): Name of the target node.
53+
capacity (float): Link capacity (default 1.0).
54+
cost (float): Link cost (default 1.0).
55+
disabled (bool): Whether the link is disabled.
56+
risk_groups (Set[str]): Set of risk group names this link belongs to.
57+
attrs (Dict[str, Any]): Additional metadata (e.g., distance).
58+
id (str): Auto-generated unique identifier: "{source}|{target}|<base64_uuid>".
5659
"""
5760

5861
source: str
5962
target: str
6063
capacity: float = 1.0
6164
cost: float = 1.0
6265
disabled: bool = False
66+
risk_groups: Set[str] = field(default_factory=set)
6367
attrs: Dict[str, Any] = field(default_factory=dict)
6468
id: str = field(init=False)
6569

@@ -76,10 +80,9 @@ class RiskGroup:
7680
Represents a shared-risk or failure domain, which may have nested children.
7781
7882
Attributes:
79-
name: Unique name of this risk group.
80-
children: Subdomains in a nested structure.
81-
disabled: Whether this group was declared disabled on load.
82-
Disabling is enforced by `disable_risk_group()` in the Network.
83+
name (str): Unique name of this risk group.
84+
children (List[RiskGroup]): Subdomains in a nested structure.
85+
disabled (bool): Whether this group was declared disabled on load.
8386
"""
8487

8588
name: str
@@ -95,29 +98,25 @@ class Network:
9598
Attributes:
9699
nodes (Dict[str, Node]): Mapping from node name -> Node object.
97100
links (Dict[str, Link]): Mapping from link ID -> Link object.
98-
risk_groups: Top-level risk groups by name.
101+
risk_groups (Dict[str, RiskGroup]): Top-level risk groups by name.
99102
attrs (Dict[str, Any]): Optional metadata about the network.
100103
"""
101104

102105
nodes: Dict[str, Node] = field(default_factory=dict)
103106
links: Dict[str, Link] = field(default_factory=dict)
107+
risk_groups: Dict[str, RiskGroup] = field(default_factory=dict)
104108
attrs: Dict[str, Any] = field(default_factory=dict)
105109

106110
def add_node(self, node: Node) -> None:
107111
"""
108112
Add a node to the network (keyed by node.name).
109113
110-
Auto-tags node.attrs["type"] = "node" if not already set,
111-
and node.attrs["disabled"] = False if not specified.
112-
113114
Args:
114-
node: Node to add.
115+
node (Node): Node to add.
115116
116117
Raises:
117118
ValueError: If a node with the same name already exists.
118119
"""
119-
node.attrs.setdefault("type", "node")
120-
node.attrs.setdefault("disabled", False)
121120
if node.name in self.nodes:
122121
raise ValueError(f"Node '{node.name}' already exists in the network.")
123122
self.nodes[node.name] = node
@@ -126,11 +125,8 @@ def add_link(self, link: Link) -> None:
126125
"""
127126
Add a link to the network (keyed by the link's auto-generated ID).
128127
129-
Auto-tags link.attrs["type"] = "link" if not already set,
130-
and link.attrs["disabled"] = False if not specified.
131-
132128
Args:
133-
link: Link to add.
129+
link (Link): Link to add.
134130
135131
Raises:
136132
ValueError: If the link's source or target node does not exist.
@@ -140,8 +136,6 @@ def add_link(self, link: Link) -> None:
140136
if link.target not in self.nodes:
141137
raise ValueError(f"Target node '{link.target}' not found in network.")
142138

143-
link.attrs.setdefault("type", "link")
144-
link.attrs.setdefault("disabled", False)
145139
self.links[link.id] = link
146140

147141
def to_strict_multidigraph(self, add_reverse: bool = True) -> StrictMultiDiGraph:
@@ -151,10 +145,10 @@ def to_strict_multidigraph(self, add_reverse: bool = True) -> StrictMultiDiGraph
151145
Skips disabled nodes/links. Optionally adds reverse edges.
152146
153147
Args:
154-
add_reverse: If True, also add a reverse edge for each link.
148+
add_reverse (bool): If True, also add a reverse edge for each link.
155149
156150
Returns:
157-
A directed multigraph representation of the network.
151+
StrictMultiDiGraph: A directed multigraph representation of the network.
158152
"""
159153
graph = StrictMultiDiGraph()
160154
disabled_nodes = {name for name, nd in self.nodes.items() if nd.disabled}
@@ -199,14 +193,13 @@ def select_node_groups_by_path(self, path: str) -> Dict[str, List[Node]]:
199193
"""
200194
Select and group nodes whose names match a given regular expression.
201195
202-
This method uses re.match(), so the pattern is anchored at the start
203-
of the node name. If the pattern includes capturing groups,
204-
the group label is formed by joining all non-None captures with '|'.
205-
If no capturing groups exist, the group label is the original
206-
pattern string.
196+
Uses re.match(), so the pattern is anchored at the start of the node name.
197+
If the pattern includes capturing groups, the group label is formed by
198+
joining all non-None captures with '|'. If no capturing groups exist,
199+
the group label is the original pattern string.
207200
208201
Args:
209-
path: A Python regular expression pattern (e.g., "^foo", "bar(\\d+)", etc.).
202+
path (str): A Python regular expression pattern (e.g., "^foo", "bar(\\d+)", etc.).
210203
211204
Returns:
212205
Dict[str, List[Node]]: A mapping from group label -> list of matching nodes.
@@ -240,17 +233,18 @@ def max_flow(
240233
Returns a dictionary of flow values keyed by (source_label, sink_label).
241234
242235
Args:
243-
source_path: Regex pattern for selecting source nodes.
244-
sink_path: Regex pattern for selecting sink nodes.
245-
mode: Either "combine" or "pairwise".
246-
- "combine": Treat all matched sources as one group,
247-
and all matched sinks as one group. Returns a single entry.
248-
- "pairwise": Compute flow for each (source_group, sink_group) pair.
249-
shortest_path: If True, flows are constrained to shortest paths.
250-
flow_placement: Determines how parallel equal-cost paths are handled.
236+
source_path (str): Regex pattern for selecting source nodes.
237+
sink_path (str): Regex pattern for selecting sink nodes.
238+
mode (str): Either "combine" or "pairwise".
239+
- "combine": Treat all matched sources as one group,
240+
and all matched sinks as one group. Returns a single entry.
241+
- "pairwise": Compute flow for each (source_group, sink_group) pair.
242+
shortest_path (bool): If True, flows are constrained to shortest paths.
243+
flow_placement (FlowPlacement): Determines how parallel equal-cost paths
244+
are handled.
251245
252246
Returns:
253-
A dict of {(src_label, snk_label): flow_value}.
247+
Dict[Tuple[str, str], float]: Flow values keyed by (src_label, snk_label).
254248
255249
Raises:
256250
ValueError: If no matching source or sink groups are found, or invalid mode.
@@ -303,7 +297,7 @@ def _compute_flow_single_group(
303297
sources: List[Node],
304298
sinks: List[Node],
305299
shortest_path: bool,
306-
flow_placement: FlowPlacement,
300+
flow_placement: Optional[FlowPlacement],
307301
) -> float:
308302
"""
309303
Attach a pseudo-source and pseudo-sink to the provided node lists,
@@ -313,14 +307,18 @@ def _compute_flow_single_group(
313307
Disabled nodes are excluded from flow computation.
314308
315309
Args:
316-
sources: List of source nodes.
317-
sinks: List of sink nodes.
318-
shortest_path: If True, restrict flows to shortest paths only.
319-
flow_placement: Strategy for placing flow among parallel equal-cost paths.
310+
sources (List[Node]): List of source nodes.
311+
sinks (List[Node]): List of sink nodes.
312+
shortest_path (bool): If True, restrict flows to shortest paths only.
313+
flow_placement (FlowPlacement or None): Strategy for placing flow among
314+
parallel equal-cost paths. If None, defaults to FlowPlacement.PROPORTIONAL.
320315
321316
Returns:
322-
The computed max flow value, or 0.0 if no active sources or sinks.
317+
float: The computed max flow value, or 0.0 if no active sources or sinks.
323318
"""
319+
if flow_placement is None:
320+
flow_placement = FlowPlacement.PROPORTIONAL
321+
324322
active_sources = [s for s in sources if not s.disabled]
325323
active_sinks = [s for s in sinks if not s.disabled]
326324

@@ -353,7 +351,7 @@ def disable_node(self, node_name: str) -> None:
353351
Mark a node as disabled.
354352
355353
Args:
356-
node_name: Name of the node to disable.
354+
node_name (str): Name of the node to disable.
357355
358356
Raises:
359357
ValueError: If the specified node does not exist.
@@ -367,7 +365,7 @@ def enable_node(self, node_name: str) -> None:
367365
Mark a node as enabled.
368366
369367
Args:
370-
node_name: Name of the node to enable.
368+
node_name (str): Name of the node to enable.
371369
372370
Raises:
373371
ValueError: If the specified node does not exist.
@@ -381,7 +379,7 @@ def disable_link(self, link_id: str) -> None:
381379
Mark a link as disabled.
382380
383381
Args:
384-
link_id: ID of the link to disable.
382+
link_id (str): ID of the link to disable.
385383
386384
Raises:
387385
ValueError: If the specified link does not exist.
@@ -395,7 +393,7 @@ def enable_link(self, link_id: str) -> None:
395393
Mark a link as enabled.
396394
397395
Args:
398-
link_id: ID of the link to enable.
396+
link_id (str): ID of the link to enable.
399397
400398
Raises:
401399
ValueError: If the specified link does not exist.
@@ -428,11 +426,11 @@ def get_links_between(self, source: str, target: str) -> List[str]:
428426
to the target node.
429427
430428
Args:
431-
source: Source node name.
432-
target: Target node name.
429+
source (str): Source node name.
430+
target (str): Target node name.
433431
434432
Returns:
435-
A list of link IDs for all direct links from source to target.
433+
List[str]: A list of link IDs for all direct links from source to target.
436434
"""
437435
matches = []
438436
for link_id, link in self.links.items():
@@ -450,12 +448,12 @@ def find_links(
450448
Search for links using optional regex patterns for source or target node names.
451449
452450
Args:
453-
source_regex: Regex pattern to match link.source. If None, matches all sources.
454-
target_regex: Regex pattern to match link.target. If None, matches all targets.
455-
any_direction: If True, also match where source and target are reversed.
451+
source_regex (str or None): Regex to match link.source. If None, matches all sources.
452+
target_regex (str or None): Regex to match link.target. If None, matches all targets.
453+
any_direction (bool): If True, also match reversed source/target.
456454
457455
Returns:
458-
A list of unique Link objects that match the criteria.
456+
List[Link]: A list of unique Link objects that match the criteria.
459457
"""
460458
src_pat = re.compile(source_regex) if source_regex else None
461459
tgt_pat = re.compile(target_regex) if target_regex else None
@@ -482,12 +480,12 @@ def find_links(
482480

483481
def disable_risk_group(self, name: str, recursive: bool = True) -> None:
484482
"""
485-
Disable all nodes/links that have 'name' in their shared_risk_groups.
483+
Disable all nodes/links that have 'name' in their risk_groups.
486484
If recursive=True, also disable items belonging to child risk groups.
487485
488486
Args:
489-
name: The name of the risk group to disable.
490-
recursive: If True, also disable subgroups recursively.
487+
name (str): The name of the risk group to disable.
488+
recursive (bool): If True, also disable subgroups recursively.
491489
"""
492490
if name not in self.risk_groups:
493491
return
@@ -500,19 +498,19 @@ def disable_risk_group(self, name: str, recursive: bool = True) -> None:
500498
if recursive:
501499
queue.extend(grp.children)
502500

501+
# Disable nodes
503502
for node_name, node_obj in self.nodes.items():
504-
srgs = node_obj.attrs.get("shared_risk_groups", [])
505-
if any(g in to_disable for g in srgs):
503+
if node_obj.risk_groups & to_disable:
506504
self.disable_node(node_name)
507505

506+
# Disable links
508507
for link_id, link_obj in self.links.items():
509-
srgs = link_obj.attrs.get("shared_risk_groups", [])
510-
if any(g in to_disable for g in srgs):
508+
if link_obj.risk_groups & to_disable:
511509
self.disable_link(link_id)
512510

513511
def enable_risk_group(self, name: str, recursive: bool = True) -> None:
514512
"""
515-
Enable all nodes/links that have 'name' in their shared_risk_groups.
513+
Enable all nodes/links that have 'name' in their risk_groups.
516514
If recursive=True, also enable items belonging to child risk groups.
517515
518516
Note:
@@ -521,8 +519,8 @@ def enable_risk_group(self, name: str, recursive: bool = True) -> None:
521519
remain disabled.
522520
523521
Args:
524-
name: The name of the risk group to enable.
525-
recursive: If True, also enable subgroups recursively.
522+
name (str): The name of the risk group to enable.
523+
recursive (bool): If True, also enable subgroups recursively.
526524
"""
527525
if name not in self.risk_groups:
528526
return
@@ -535,12 +533,12 @@ def enable_risk_group(self, name: str, recursive: bool = True) -> None:
535533
if recursive:
536534
queue.extend(grp.children)
537535

536+
# Enable nodes
538537
for node_name, node_obj in self.nodes.items():
539-
srgs = node_obj.attrs.get("shared_risk_groups", [])
540-
if any(g in to_enable for g in srgs):
538+
if node_obj.risk_groups & to_enable:
541539
self.enable_node(node_name)
542540

541+
# Enable links
543542
for link_id, link_obj in self.links.items():
544-
srgs = link_obj.attrs.get("shared_risk_groups", [])
545-
if any(g in to_enable for g in srgs):
543+
if link_obj.risk_groups & to_enable:
546544
self.enable_link(link_id)

0 commit comments

Comments
 (0)