44from dataclasses import dataclass
55from 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+
107135def _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+
309410def _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
328437def _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
344462def _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
353479def _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 ,
0 commit comments