Skip to content

Commit a21bc56

Browse files
authored
exposed flow_placement in CapacityProbe, added link overrides (#52)
1 parent 00ec82a commit a21bc56

9 files changed

Lines changed: 325 additions & 133 deletions

File tree

ngraph/blueprints.py

Lines changed: 157 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from dataclasses import dataclass
55
from typing import Any, Dict, List
66

7-
from ngraph.network import Network, Node, Link
7+
from ngraph.network import Link, Network, Node
88

99

1010
@dataclass(slots=True)
@@ -16,9 +16,11 @@ class Blueprint:
1616
and a name_template), plus adjacency rules describing how those groups connect.
1717
1818
Attributes:
19-
name: Unique identifier of this blueprint.
20-
groups: A mapping of group_name -> group definition (e.g. node_count, name_template).
21-
adjacency: A list of adjacency dictionaries describing how groups are linked.
19+
name (str): Unique identifier of this blueprint.
20+
groups (Dict[str, Any]): A mapping of group_name -> group definition
21+
(e.g. node_count, name_template).
22+
adjacency (List[Dict[str, Any]]): A list of adjacency dictionaries
23+
describing how groups are linked.
2224
"""
2325

2426
name: str
@@ -33,8 +35,10 @@ class DSLExpansionContext:
3335
to be populated during DSL expansion.
3436
3537
Attributes:
36-
blueprints: A dictionary of blueprint name -> Blueprint object.
37-
network: The Network into which expanded nodes/links will be inserted.
38+
blueprints (Dict[str, Blueprint]): A dictionary of blueprint-name ->
39+
Blueprint object.
40+
network (Network): The Network into which expanded nodes/links
41+
will be inserted.
3842
"""
3943

4044
blueprints: Dict[str, Blueprint]
@@ -54,12 +58,14 @@ def expand_network_dsl(data: Dict[str, Any]) -> Network:
5458
4) Process any direct node definitions.
5559
5) Expand adjacency definitions in 'network["adjacency"]'.
5660
6) Process any direct link definitions.
61+
7) Process link overrides.
5762
5863
Args:
59-
data: The YAML-parsed dictionary containing optional "blueprints" + "network".
64+
data (Dict[str, Any]): The YAML-parsed dictionary containing
65+
optional "blueprints" + "network".
6066
6167
Returns:
62-
A fully expanded Network object with all nodes and links.
68+
Network: A fully expanded Network object with all nodes and links.
6369
"""
6470
# 1) Parse blueprint definitions
6571
blueprint_map: Dict[str, Blueprint] = {}
@@ -101,9 +107,31 @@ def expand_network_dsl(data: Dict[str, Any]) -> Network:
101107
# 6) Process direct link definitions
102108
_process_direct_links(ctx.network, network_data)
103109

110+
# 7) Process link overrides
111+
_process_link_overrides(ctx.network, network_data)
112+
104113
return net
105114

106115

116+
def _process_link_overrides(network: Network, network_data: Dict[str, Any]) -> None:
117+
"""
118+
Processes the 'link_overrides' section of the network DSL, updating
119+
existing links with new parameters.
120+
121+
Args:
122+
network (Network): The Network whose links will be updated.
123+
network_data (Dict[str, Any]): The overall DSL data for the 'network'.
124+
Expected to contain 'link_overrides' as a list of dicts, each with
125+
'source', 'target', and 'link_params'.
126+
"""
127+
link_overrides = network_data.get("link_overrides", [])
128+
for link_override in link_overrides:
129+
source = link_override["source"]
130+
target = link_override["target"]
131+
link_params = link_override["link_params"]
132+
_update_links(network, source, target, link_params)
133+
134+
107135
def _expand_group(
108136
ctx: DSLExpansionContext,
109137
parent_path: str,
@@ -117,11 +145,15 @@ def _expand_group(
117145
- Another blueprint's subgroups, or
118146
- A direct node group (node_count, name_template).
119147
120-
We do *not* skip the subgroup name even inside blueprint expansion, because
121-
typically the 'group_name' is "leaf"/"spine" etc., not the blueprint’s name.
122-
123-
So the final path is always 'parent_path + "/" + group_name' if parent_path is non-empty,
124-
otherwise just group_name.
148+
Args:
149+
ctx (DSLExpansionContext): The context containing all blueprint info
150+
and the target Network.
151+
parent_path (str): The parent path in the hierarchy.
152+
group_name (str): The current group's name.
153+
group_def (Dict[str, Any]): The group definition (e.g. {node_count, name_template}
154+
or {use_blueprint, parameters, ...}).
155+
blueprint_expansion (bool): Indicates whether we are expanding within
156+
a blueprint context or not.
125157
"""
126158
# Construct the effective path by appending group_name if parent_path is non-empty
127159
if parent_path:
@@ -182,7 +214,14 @@ def _expand_blueprint_adjacency(
182214
parent_path: str,
183215
) -> None:
184216
"""
185-
Expands adjacency definitions from within a blueprint, using parent_path as the local root.
217+
Expands adjacency definitions from within a blueprint, using parent_path
218+
as the local root.
219+
220+
Args:
221+
ctx (DSLExpansionContext): The context object with blueprint info and the network.
222+
adj_def (Dict[str, Any]): The adjacency definition inside the blueprint,
223+
containing 'source', 'target', 'pattern', etc.
224+
parent_path (str): The path that serves as the base for the blueprint's node paths.
186225
"""
187226
source_rel = adj_def["source"]
188227
target_rel = adj_def["target"]
@@ -201,6 +240,11 @@ def _expand_adjacency(
201240
) -> None:
202241
"""
203242
Expands a top-level adjacency definition from 'network.adjacency'.
243+
244+
Args:
245+
ctx (DSLExpansionContext): The context containing the target network.
246+
adj_def (Dict[str, Any]): The adjacency definition dict, containing
247+
'source', 'target', and optional 'pattern', 'link_params'.
204248
"""
205249
source_path_raw = adj_def["source"]
206250
target_path_raw = adj_def["target"]
@@ -230,6 +274,13 @@ def _expand_adjacency_pattern(
230274
* "one_to_one": Pair each source node with exactly one target node, supporting
231275
wrap-around if one side is an integer multiple of the other.
232276
Also skips self-loops.
277+
278+
Args:
279+
ctx (DSLExpansionContext): The context containing the target network.
280+
source_path (str): The path pattern that identifies the source node group(s).
281+
target_path (str): The path pattern that identifies the target node group(s).
282+
pattern (str): The type of adjacency pattern (e.g., "mesh", "one_to_one").
283+
link_params (Dict[str, Any]): Additional link parameters (capacity, cost, attrs).
233284
"""
234285
source_node_groups = ctx.network.select_node_groups_by_path(source_path)
235286
target_node_groups = ctx.network.select_node_groups_by_path(target_path)
@@ -281,7 +332,6 @@ def _expand_adjacency_pattern(
281332
if pair not in dedup_pairs:
282333
dedup_pairs.add(pair)
283334
_create_link(ctx.network, sn, tn, link_params)
284-
285335
else:
286336
raise ValueError(f"Unknown adjacency pattern: {pattern}")
287337

@@ -291,6 +341,13 @@ def _create_link(
291341
) -> None:
292342
"""
293343
Creates and adds a Link to the network, applying capacity/cost/attrs from link_params.
344+
345+
Args:
346+
net (Network): The network to which the new link is added.
347+
source (str): Source node name for the link.
348+
target (str): Target node name for the link.
349+
link_params (Dict[str, Any]): A dict possibly containing 'capacity', 'cost',
350+
and 'attrs' keys.
294351
"""
295352
capacity = link_params.get("capacity", 1.0)
296353
cost = link_params.get("cost", 1.0)
@@ -306,15 +363,67 @@ def _create_link(
306363
net.add_link(link)
307364

308365

366+
def _update_links(
367+
net: Network,
368+
source: str,
369+
target: str,
370+
link_params: Dict[str, Any],
371+
any_direction: bool = True,
372+
) -> None:
373+
"""
374+
Update all Link objects between nodes matching 'source' and 'target' paths
375+
with new parameters.
376+
377+
Args:
378+
net (Network): The network whose links should be updated.
379+
source (str): A path pattern identifying source node group(s).
380+
target (str): A path pattern identifying target node group(s).
381+
link_params (Dict[str, Any]): New parameter values for the links (capacity, cost, attrs).
382+
any_direction (bool): If True, also update links in the reverse direction.
383+
"""
384+
source_node_groups = net.select_node_groups_by_path(source)
385+
target_node_groups = net.select_node_groups_by_path(target)
386+
387+
source_nodes = {
388+
node.name for _, nodes in source_node_groups.items() for node in nodes
389+
}
390+
target_nodes = {
391+
node.name for _, nodes in target_node_groups.items() for node in nodes
392+
}
393+
394+
for link in net.links.values():
395+
if link.source in source_nodes and link.target in target_nodes:
396+
link.capacity = link_params.get("capacity", link.capacity)
397+
link.cost = link_params.get("cost", link.cost)
398+
link.attrs.update(link_params.get("attrs", {}))
399+
400+
if (
401+
any_direction
402+
and link.source in target_nodes
403+
and link.target in source_nodes
404+
):
405+
link.capacity = link_params.get("capacity", link.capacity)
406+
link.cost = link_params.get("cost", link.cost)
407+
link.attrs.update(link_params.get("attrs", {}))
408+
409+
309410
def _apply_parameters(
310411
subgroup_name: str, subgroup_def: Dict[str, Any], params_overrides: Dict[str, Any]
311412
) -> Dict[str, Any]:
312413
"""
313414
Applies user-provided parameter overrides to a blueprint subgroup.
314415
315-
E.g.:
316-
if 'spine.node_count' = 6 is in params_overrides,
317-
we set 'node_count'=6 for the 'spine' subgroup.
416+
Example:
417+
If 'spine.node_count'=6 is in params_overrides,
418+
we set 'node_count'=6 for the 'spine' subgroup.
419+
420+
Args:
421+
subgroup_name (str): Name of the subgroup in the blueprint (e.g. 'spine').
422+
subgroup_def (Dict[str, Any]): The default definition of the subgroup.
423+
params_overrides (Dict[str, Any]): Overrides in the form of { 'spine.node_count': <val> }.
424+
425+
Returns:
426+
Dict[str, Any]: A copy of subgroup_def with parameter overrides applied.
318427
"""
319428
out = dict(subgroup_def)
320429
for key, val in params_overrides.items():
@@ -327,22 +436,39 @@ def _apply_parameters(
327436

328437
def _join_paths(parent_path: str, rel_path: str) -> str:
329438
"""
330-
If rel_path starts with '/', interpret that as relative to 'parent_path';
331-
otherwise, simply append rel_path to parent_path with '/' if needed.
439+
Joins two path segments according to NetGraph's DSL conventions:
440+
- If rel_path starts with '/', remove the leading slash and treat it
441+
as a relative path appended to parent_path (if present).
442+
- Otherwise, simply append rel_path to parent_path if parent_path is non-empty.
443+
444+
Args:
445+
parent_path (str): The existing path prefix.
446+
rel_path (str): A relative path that may start with '/'.
447+
448+
Returns:
449+
str: The combined path as a single string.
332450
"""
333451
if rel_path.startswith("/"):
334452
rel_path = rel_path[1:]
335453
if parent_path:
336454
return f"{parent_path}/{rel_path}"
337-
else:
338-
return rel_path
455+
return rel_path
456+
339457
if parent_path:
340458
return f"{parent_path}/{rel_path}"
341459
return rel_path
342460

343461

344462
def _process_direct_nodes(net: Network, network_data: Dict[str, Any]) -> None:
345-
"""Processes direct node definitions (network_data["nodes"])."""
463+
"""
464+
Processes direct node definitions (network_data["nodes"]) and adds them to the network
465+
if they do not already exist.
466+
467+
Args:
468+
net (Network): The network to which nodes are added.
469+
network_data (Dict[str, Any]): DSL data containing a "nodes" dict
470+
keyed by node name -> attributes.
471+
"""
346472
for node_name, node_attrs in network_data.get("nodes", {}).items():
347473
if node_name not in net.nodes:
348474
new_node = Node(name=node_name, attrs=node_attrs or {})
@@ -352,14 +478,21 @@ def _process_direct_nodes(net: Network, network_data: Dict[str, Any]) -> None:
352478

353479
def _process_direct_links(net: Network, network_data: Dict[str, Any]) -> None:
354480
"""
355-
Processes direct link definitions (network_data["links"]).
481+
Processes direct link definitions (network_data["links"]) and adds them to the network.
482+
483+
Args:
484+
net (Network): The network to which links are added.
485+
network_data (Dict[str, Any]): DSL data containing a "links" list,
486+
each item must have "source", "target", and optionally "link_params".
356487
"""
357488
existing_node_names = set(net.nodes.keys())
358489
for link_info in network_data.get("links", []):
359490
source = link_info["source"]
360491
target = link_info["target"]
361492
if source not in existing_node_names or target not in existing_node_names:
362493
raise ValueError(f"Link references unknown node(s): {source}, {target}.")
494+
if source == target:
495+
raise ValueError(f"Link cannot have the same source and target: {source}")
363496
link_params = link_info.get("link_params", {})
364497
link = Link(
365498
source=source,

ngraph/lib/algorithms/base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ class EdgeSelect(IntEnum):
5858

5959

6060
class FlowPlacement(IntEnum):
61-
"""Ways to distribute flow on parallel edges."""
61+
"""Ways to distribute flow across parallel equal cost paths."""
6262

6363
PROPORTIONAL = 1 # Flow is split proportional to capacity (Dinic-like approach)
64-
EQUAL_BALANCED = 2 # Flow is equally divided among parallel edges
64+
EQUAL_BALANCED = 2 # Flow is equally divided among parallel paths of equal cost

0 commit comments

Comments
 (0)