From 48f6fb9cc8e1970b8940d32a6f5b3d28fad0e7c3 Mon Sep 17 00:00:00 2001 From: Andrey Golovanov Date: Mon, 10 Mar 2025 01:34:24 +0000 Subject: [PATCH] group and adjacency expansion --- ngraph/blueprints.py | 382 ++++++++++++++++----- notebooks/scenario_dc.ipynb | 573 +++++++++++-------------------- tests/test_blueprints.py | 650 ++++++++++++++++++++++++++++++++++++ 3 files changed, 1139 insertions(+), 466 deletions(-) diff --git a/ngraph/blueprints.py b/ngraph/blueprints.py index f99a283..17aaa01 100644 --- a/ngraph/blueprints.py +++ b/ngraph/blueprints.py @@ -1,6 +1,10 @@ +from __future__ import annotations + +import copy +import re from dataclasses import dataclass +from itertools import product, zip_longest from typing import Any, Dict, List -import copy from ngraph.network import Link, Network, Node @@ -16,7 +20,7 @@ class Blueprint: Attributes: name (str): Unique identifier of this blueprint. groups (Dict[str, Any]): A mapping of group_name -> group definition - (e.g., node_count, name_template, attrs). + (e.g., node_count, name_template, attrs, use_blueprint, etc.). adjacency (List[Dict[str, Any]]): A list of adjacency dictionaries describing how groups are linked. """ @@ -33,10 +37,8 @@ class DSLExpansionContext: to be populated during DSL expansion. Attributes: - blueprints (Dict[str, Blueprint]): A dictionary of blueprint-name -> - Blueprint object. - network (Network): The Network into which expanded nodes/links - will be inserted. + blueprints (Dict[str, Blueprint]): Dictionary of blueprint-name -> Blueprint. + network (Network): The Network into which expanded nodes/links are inserted. """ blueprints: Dict[str, Blueprint] @@ -52,18 +54,19 @@ def expand_network_dsl(data: Dict[str, Any]) -> Network: 2) Build a new Network from "network" metadata (e.g. name, version). 3) Expand 'network["groups"]'. - If a group references a blueprint, incorporate that blueprint's subgroups. - - Otherwise, directly create nodes (e.g., node_count). + - Otherwise, directly create nodes. 4) Process any direct node definitions. 5) Expand adjacency definitions in 'network["adjacency"]'. 6) Process any direct link definitions. - 7) Process link overrides. - 8) Process node overrides. + 7) Process link overrides (applied in order if multiple overrides match). + 8) Process node overrides (applied in order if multiple overrides match). Args: - data: The YAML-parsed dictionary containing optional "blueprints" + "network". + data (Dict[str, Any]): The YAML-parsed dictionary containing + optional "blueprints" + "network". Returns: - The fully expanded Network object with all nodes and links. + Network: The fully expanded Network object with all nodes and links. """ # 1) Parse blueprint definitions blueprint_map: Dict[str, Blueprint] = {} @@ -85,7 +88,7 @@ def expand_network_dsl(data: Dict[str, Any]) -> Network: # Create a context ctx = DSLExpansionContext(blueprints=blueprint_map, network=net) - # 3) Expand top-level groups (blueprint usage or direct node groups) + # 3) Expand top-level groups for group_name, group_def in network_data.get("groups", {}).items(): _expand_group(ctx, parent_path="", group_name=group_name, group_def=group_def) @@ -99,10 +102,10 @@ def expand_network_dsl(data: Dict[str, Any]) -> Network: # 6) Process direct link definitions _process_direct_links(ctx.network, network_data) - # 7) Process link overrides + # 7) Process link overrides (in order) _process_link_overrides(ctx.network, network_data) - # 8) Process node overrides + # 8) Process node overrides (in order) _process_node_overrides(ctx.network, network_data) return net @@ -117,18 +120,28 @@ def _expand_group( """ Expands a single group definition into either: - Another blueprint's subgroups, or - - A direct node group (node_count, name_template, attrs). + - A direct node group (with node_count, etc.), + - Possibly replicating itself if group_name has bracket expansions. - If the group references 'use_blueprint', we expand that blueprint's groups - under the current hierarchy path. Otherwise, we create nodes directly. + If 'use_blueprint' is present, we expand that blueprint. Otherwise, we + create nodes. We also handle bracket expansions in 'group_name' to + replicate the definition multiple times. Args: - ctx: The context containing all blueprint info and the target Network. - parent_path: The parent path in the hierarchy. - group_name: The current group's name. - group_def: The group definition (e.g. {node_count, name_template} - or {use_blueprint, parameters, ...}). - """ + ctx (DSLExpansionContext): The context containing blueprint info and the Network. + parent_path (str): The parent path in the hierarchy. + group_name (str): The current group's name (may have bracket expansions). + group_def (Dict[str, Any]): The group definition (e.g. node_count, use_blueprint, etc.). + """ + # First, check if group_name has expansions like 'fa[1-16]' + expanded_names = _expand_name_patterns(group_name) + if len(expanded_names) > 1 or expanded_names[0] != group_name: + # We have multiple expansions: replicate group_def for each expanded name + for expanded_name in expanded_names: + _expand_group(ctx, parent_path, expanded_name, group_def) + return + + # No expansions: proceed normally if parent_path: effective_path = f"{parent_path}/{group_name}" else: @@ -162,38 +175,66 @@ def _expand_group( # Expand blueprint adjacency for adj_def in bp.adjacency: _expand_blueprint_adjacency(ctx, adj_def, effective_path) + else: # It's a direct node group node_count = group_def.get("node_count", 1) name_template = group_def.get("name_template", f"{group_name}-{{node_num}}") - attrs = group_def.get("attrs", {}) + + # Start from the user-supplied 'attrs' or create new + combined_attrs = copy.deepcopy(group_def.get("attrs", {})) + + # Copy any other top-level keys (besides recognized ones) into 'combined_attrs' + recognized_keys = { + "node_count", + "name_template", + "coords", + "attrs", + "use_blueprint", + "parameters", + } + for key, val in group_def.items(): + if key not in recognized_keys: + combined_attrs[key] = val for i in range(1, node_count + 1): label = name_template.format(node_num=i) node_name = f"{effective_path}/{label}" if effective_path else label - node = Node(name=node_name) - # Merge any extra attributes + + # If coords are specified, store them if "coords" in group_def: node.attrs["coords"] = group_def["coords"] - node.attrs.update(attrs) # Apply bulk attributes + + # Merge combined_attrs into node.attrs + node.attrs.update(combined_attrs) + + # Ensure node.attrs has a default "type" if not set node.attrs.setdefault("type", "node") + ctx.network.add_node(node) def _expand_blueprint_adjacency( - ctx: DSLExpansionContext, adj_def: Dict[str, Any], parent_path: str + ctx: DSLExpansionContext, + adj_def: Dict[str, Any], + parent_path: str, ) -> None: """ Expands adjacency definitions from within a blueprint, using parent_path - as the local root. + as the local root. This also handles optional expand_vars. Args: - ctx: The context object with blueprint info and the network. - adj_def: The adjacency definition inside the blueprint, + ctx (DSLExpansionContext): The context object with blueprint info and the network. + adj_def (Dict[str, Any]): The adjacency definition inside the blueprint, containing 'source', 'target', 'pattern', etc. - parent_path: The path serving as the base for the blueprint's node paths. + parent_path (str): The path serving as the base for the blueprint's node paths. """ + expand_vars = adj_def.get("expand_vars", {}) + if expand_vars: + _expand_adjacency_with_variables(ctx, adj_def, parent_path) + return + source_rel = adj_def["source"] target_rel = adj_def["target"] pattern = adj_def.get("pattern", "mesh") @@ -208,13 +249,19 @@ def _expand_blueprint_adjacency( def _expand_adjacency(ctx: DSLExpansionContext, adj_def: Dict[str, Any]) -> None: """ - Expands a top-level adjacency definition from 'network.adjacency'. + Expands a top-level adjacency definition from 'network.adjacency'. If 'expand_vars' + is provided, we expand the source/target as templates repeatedly. Args: - ctx: The context containing the target network. - adj_def: The adjacency definition dict, containing 'source', 'target', - and optional 'pattern', 'link_params'. + ctx (DSLExpansionContext): The context containing the target network. + adj_def (Dict[str, Any]): The adjacency definition dict, containing 'source', 'target', + and optional 'pattern', 'link_params', 'link_count', 'expand_vars'. """ + expand_vars = adj_def.get("expand_vars", {}) + if expand_vars: + _expand_adjacency_with_variables(ctx, adj_def, parent_path="") + return + source_path_raw = adj_def["source"] target_path_raw = adj_def["target"] pattern = adj_def.get("pattern", "mesh") @@ -229,6 +276,87 @@ def _expand_adjacency(ctx: DSLExpansionContext, adj_def: Dict[str, Any]) -> None ) +def _expand_adjacency_with_variables( + ctx: DSLExpansionContext, adj_def: Dict[str, Any], parent_path: str +) -> None: + """ + Handles adjacency expansions when 'expand_vars' is provided. + We substitute variables into the 'source' and 'target' templates to produce + multiple adjacency expansions. Then each expansion is passed to _expand_adjacency_pattern. + + Example adjacency entry: + source: "/ssw-{dc_id}" + target: "/fa{fa_id}/fadu" + expand_vars: + dc_id: [1, 2, 3] + fa_id: [5, 6] + pattern: one_to_one + link_params: + capacity: 200 + cost: 1 + expansion_mode: "cartesian" or "zip" + + Args: + ctx (DSLExpansionContext): The DSL expansion context. + adj_def (Dict[str, Any]): The adjacency definition including expand_vars, source, target, etc. + parent_path (str): Prepended to source/target if they do not start with '/'. + """ + source_template = adj_def["source"] + target_template = adj_def["target"] + pattern = adj_def.get("pattern", "mesh") + link_params = adj_def.get("link_params", {}) + link_count = adj_def.get("link_count", 1) + expand_vars = adj_def["expand_vars"] + expansion_mode = adj_def.get("expansion_mode", "cartesian") + + # Sort the variables so we have a consistent order for product or zip + var_names = sorted(expand_vars.keys()) + lists_of_values = [expand_vars[var] for var in var_names] + + if expansion_mode == "zip": + # If zip mode, we zip all lists in lockstep. All must be same length or we raise. + lengths = [len(lst) for lst in lists_of_values] + if len(set(lengths)) != 1: + raise ValueError( + f"zip expansion requires all lists be the same length; got {lengths}" + ) + + for combo_tuple in zip_longest(*lists_of_values, fillvalue=None): + combo_dict = dict(zip(var_names, combo_tuple)) + expanded_src = _join_paths( + parent_path, source_template.format(**combo_dict) + ) + expanded_tgt = _join_paths( + parent_path, target_template.format(**combo_dict) + ) + _expand_adjacency_pattern( + ctx, + expanded_src, + expanded_tgt, + pattern, + link_params, + link_count, + ) + else: + # "cartesian" by default + for combo_tuple in product(*lists_of_values): + combo_dict = dict(zip(var_names, combo_tuple)) + expanded_src = _join_paths( + parent_path, source_template.format(**combo_dict) + ) + expanded_tgt = _join_paths( + parent_path, target_template.format(**combo_dict) + ) + _expand_adjacency_pattern( + ctx, + expanded_src, + expanded_tgt, + pattern, + link_params, + link_count, + ) + + def _expand_adjacency_pattern( ctx: DSLExpansionContext, source_path: str, @@ -241,19 +369,19 @@ def _expand_adjacency_pattern( Generates Link objects for the chosen adjacency pattern among matched nodes. Supported Patterns: - * "mesh": Connect every node from source side to every node on target side, - skipping self-loops and deduplicating reversed pairs. - * "one_to_one": Pair each source node with exactly one target node (wrap-around), - requiring that the larger set size is an integer multiple - of the smaller set size. + * "mesh": Connect every source node to every target node + (no self-loops, deduplicate reversed pairs). + * "one_to_one": Pair each source node with exactly one target node + using wrap-around. The larger set size must be + a multiple of the smaller set size. Args: - ctx: The context containing the target network. - source_path: The path pattern identifying the source node group(s). - target_path: The path pattern identifying the target node group(s). - pattern: The type of adjacency pattern (e.g., "mesh", "one_to_one"). - link_params: Additional link parameters (capacity, cost, attrs). - link_count: Number of parallel links to create for each adjacency. + ctx (DSLExpansionContext): The context with the target network. + source_path (str): Path pattern identifying source node group(s). + target_path (str): Path pattern identifying target node group(s). + pattern (str): "mesh" or "one_to_one". + link_params (Dict[str, Any]): Additional link parameters (capacity, cost, etc.). + link_count (int): Number of parallel links to create for each adjacency. """ source_node_groups = ctx.network.select_node_groups_by_path(source_path) target_node_groups = ctx.network.select_node_groups_by_path(target_path) @@ -284,8 +412,8 @@ def _expand_adjacency_pattern( if bigger_count % smaller_count != 0: raise ValueError( - f"one_to_one pattern requires sizes with a multiple factor. " - f"Got source={s_count}, target={t_count}." + "one_to_one pattern requires sizes with a multiple factor; " + f"source={s_count}, target={t_count} do not align." ) for i in range(bigger_count): @@ -319,11 +447,11 @@ def _create_link( and attributes from link_params. Args: - net: The network to which the new link(s) will be added. - source: Source node name for the link. - target: Target node name for the link. - link_params: A dict possibly containing 'capacity', 'cost', and 'attrs' keys. - link_count: Number of parallel links to create between source and target. + net (Network): The network to which the new link(s) will be added. + source (str): Source node name for the link. + target (str): Target node name for the link. + link_params (Dict[str, Any]): Dict possibly containing 'capacity', 'cost', 'attrs'. + link_count (int): Number of parallel links to create between source and target. """ for _ in range(link_count): capacity = link_params.get("capacity", 1.0) @@ -343,7 +471,7 @@ def _create_link( def _process_direct_nodes(net: Network, network_data: Dict[str, Any]) -> None: """ Processes direct node definitions (network_data["nodes"]) and adds them to the network - if they do not already exist. + if they do not already exist. If the node name already exists, no new node is created. Example: nodes: @@ -352,8 +480,8 @@ def _process_direct_nodes(net: Network, network_data: Dict[str, Any]) -> None: hw_type: "X100" Args: - net: The network to which nodes are added. - network_data: DSL data containing a "nodes" dict keyed by node name -> attributes. + net (Network): The network to which nodes are added. + network_data (Dict[str, Any]): DSL data possibly containing a "nodes" dict. """ for node_name, attrs in network_data.get("nodes", {}).items(): if node_name not in net.nodes: @@ -375,11 +503,11 @@ def _process_direct_links(net: Network, network_data: Dict[str, Any]) -> None: cost: 2 attrs: color: "blue" + link_count: 2 Args: - net: The network to which links are added. - network_data: DSL data containing a "links" list, - each item must have "source", "target", and optionally "link_params". + net (Network): The network to which links are added. + network_data (Dict[str, Any]): DSL data possibly containing a "links" list. """ existing_node_names = set(net.nodes.keys()) for link_info in network_data.get("links", []): @@ -397,7 +525,8 @@ def _process_direct_links(net: Network, network_data: Dict[str, Any]) -> None: def _process_link_overrides(net: Network, network_data: Dict[str, Any]) -> None: """ Processes the 'link_overrides' section of the network DSL, updating - existing links with new parameters. + existing links with new parameters. Overrides are applied in order if + multiple items match the same link. Example: link_overrides: @@ -407,11 +536,11 @@ def _process_link_overrides(net: Network, network_data: Dict[str, Any]) -> None: capacity: 200 attrs: shared_risk_group: "SRG1" + any_direction: true Args: - net: The Network whose links will be updated. - network_data: The overall DSL data for the 'network'. - Expected to contain 'link_overrides' as a list of dicts. + net (Network): The Network whose links will be updated. + network_data (Dict[str, Any]): DSL data possibly containing 'link_overrides'. """ link_overrides = network_data.get("link_overrides", []) for link_override in link_overrides: @@ -425,7 +554,8 @@ def _process_link_overrides(net: Network, network_data: Dict[str, Any]) -> None: def _process_node_overrides(net: Network, network_data: Dict[str, Any]) -> None: """ Processes the 'node_overrides' section of the network DSL, updating - existing nodes with new attributes in bulk. + existing nodes with new attributes in bulk. Overrides are applied in order + if multiple items match the same node. Example: node_overrides: @@ -435,9 +565,8 @@ def _process_node_overrides(net: Network, network_data: Dict[str, Any]) -> None: shared_risk_group: "SRG2" Args: - net: The Network whose nodes will be updated. - network_data: The overall DSL data for the 'network'. - Expected to contain 'node_overrides' as a list of dicts. + net (Network): The Network whose nodes will be updated. + network_data (Dict[str, Any]): DSL data possibly containing 'node_overrides'. """ node_overrides = network_data.get("node_overrides", []) for override in node_overrides: @@ -458,14 +587,14 @@ def _update_links( with new parameters. If any_direction=True, both (source->target) and (target->source) links - are updated. + are updated if present. Args: - net: The network whose links should be updated. - source: A path pattern identifying source node group(s). - target: A path pattern identifying target node group(s). - link_params: New parameter values for the links (capacity, cost, attrs). - any_direction: If True, also update links in the reverse direction. + net (Network): The network whose links should be updated. + source (str): A path pattern identifying source node group(s). + target (str): A path pattern identifying target node group(s). + link_params (Dict[str, Any]): New parameter values (capacity, cost, attrs). + any_direction (bool): If True, also update reversed direction links. """ source_node_groups = net.select_node_groups_by_path(source) target_node_groups = net.select_node_groups_by_path(target) @@ -495,9 +624,9 @@ def _update_nodes(net: Network, path: str, attrs: Dict[str, Any]) -> None: Updates attributes on all nodes matching a given path pattern. Args: - net: The network containing nodes. - path: A path pattern identifying which node group(s) to modify. - attrs: A dictionary of new attributes to set/merge. + net (Network): The network containing the nodes. + path (str): A path pattern identifying which node group(s) to modify. + attrs (Dict[str, Any]): A dictionary of new attributes to set/merge. """ node_groups = net.select_node_groups_by_path(path) for _, nodes in node_groups.items(): @@ -513,20 +642,17 @@ def _apply_parameters( Example: If 'spine.node_count' = 6 is in params_overrides, - it sets 'node_count'=6 for the 'spine' subgroup. - - If 'spine.attrs.hw_type' = 'Dell', - it sets subgroup_def['attrs']['hw_type'] = 'Dell'. + it sets 'node_count' = 6 for the 'spine' subgroup. + If 'spine.attrs.hw_type' = 'Dell', it sets subgroup_def['attrs']['hw_type'] = 'Dell'. Args: - subgroup_name: Name of the subgroup in the blueprint (e.g. 'spine'). - subgroup_def: The default definition of the subgroup. - params_overrides: Overrides in the form of + subgroup_name (str): Name of the subgroup in the blueprint (e.g. 'spine'). + subgroup_def (Dict[str, Any]): The default definition of that subgroup. + params_overrides (Dict[str, Any]): Overrides in the form of {'spine.node_count': 6, 'spine.attrs.hw_type': 'Dell'}. Returns: - A copy of subgroup_def with parameter overrides applied, - including nested dictionary fields if specified by dotted paths (e.g. attrs.foo). + Dict[str, Any]: A copy of subgroup_def with parameter overrides applied. """ out = copy.deepcopy(subgroup_def) @@ -546,6 +672,11 @@ def _apply_nested_path( """ Recursively applies a path like ["attrs", "role"] to set node_def["attrs"]["role"] = value. Creates intermediate dicts as needed. + + Args: + node_def (Dict[str, Any]): The dictionary to update. + path_parts (List[str]): List of keys specifying nested fields. + value (Any): The value to set in the nested field. """ if not path_parts: return @@ -553,26 +684,95 @@ def _apply_nested_path( if len(path_parts) == 1: node_def[key] = value return - if key not in node_def or not isinstance(node_def[key], dict): node_def[key] = {} _apply_nested_path(node_def[key], path_parts[1:], value) +_RANGE_REGEX = re.compile(r"\[([^\]]+)\]") + + +def _expand_name_patterns(name: str) -> List[str]: + """ + Parses and expands bracketed expressions in a group name. For example: + + "fa[1-3]" -> ["fa1", "fa2", "fa3"] + "dc[1,3,5-6]" -> ["dc1", "dc3", "dc5", "dc6"] + "fa[1-2]_plane[5-6]" -> + ["fa1_plane5", "fa1_plane6", "fa2_plane5", "fa2_plane6"] + + If no bracket expressions are present, returns [name] unchanged. + + Args: + name (str): A group name that may contain bracket expansions. + + Returns: + List[str]: All expanded names. If no expansion was needed, returns + a single-element list with 'name' itself. + """ + matches = list(_RANGE_REGEX.finditer(name)) + if not matches: + return [name] # no expansions + + expansions_list = [] + for match in matches: + range_expr = match.group(1) + expansions_list.append(_parse_range_expr(range_expr)) + + expanded_names = [] + for combo in product(*expansions_list): + result_str = "" + last_end = 0 + for m_idx, match in enumerate(matches): + start, end = match.span() + result_str += name[last_end:start] + result_str += combo[m_idx] + last_end = end + result_str += name[last_end:] + expanded_names.append(result_str) + + return expanded_names + + +def _parse_range_expr(expr: str) -> List[str]: + """ + Parses a bracket expression that might have commas, single values, and dash ranges. + For example: "1-3,5,7-9" -> ["1", "2", "3", "5", "7", "8", "9"]. + + Args: + expr (str): The raw expression from inside brackets, e.g. "1-3,5,7-9". + + Returns: + List[str]: A sorted list of all string expansions. + """ + values = [] + parts = [x.strip() for x in expr.split(",")] + for part in parts: + if "-" in part: + start_str, end_str = part.split("-", 1) + start = int(start_str) + end = int(end_str) + for val in range(start, end + 1): + values.append(str(val)) + else: + values.append(part) + return values + + def _join_paths(parent_path: str, rel_path: str) -> str: """ Joins two path segments according to NetGraph's DSL conventions: - - If rel_path starts with '/', strip the leading slash and treat it as + - If rel_path starts with '/', we strip the leading slash and treat it as appended to parent_path if parent_path is not empty. - Otherwise, simply append rel_path to parent_path if parent_path is non-empty. Args: - parent_path: The existing path prefix. - rel_path: A relative path that may start with '/'. + parent_path (str): The existing path prefix. + rel_path (str): A relative path that may start with '/'. Returns: - The combined path as a single string. + str: The combined path as a single string. """ if rel_path.startswith("/"): rel_path = rel_path[1:] diff --git a/notebooks/scenario_dc.ipynb b/notebooks/scenario_dc.ipynb index 560ad4f..c4364b4 100644 --- a/notebooks/scenario_dc.ipynb +++ b/notebooks/scenario_dc.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ @@ -18,7 +18,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ @@ -70,56 +70,12 @@ "\n", " fa:\n", " groups:\n", - " fa1:\n", - " use_blueprint: hgrid_2tier\n", - " fa2:\n", - " use_blueprint: hgrid_2tier\n", - " fa3:\n", - " use_blueprint: hgrid_2tier\n", - " fa4:\n", - " use_blueprint: hgrid_2tier\n", - " fa5:\n", - " use_blueprint: hgrid_2tier\n", - " fa6:\n", - " use_blueprint: hgrid_2tier\n", - " fa7:\n", - " use_blueprint: hgrid_2tier\n", - " fa8:\n", - " use_blueprint: hgrid_2tier\n", - " fa9:\n", - " use_blueprint: hgrid_2tier\n", - " fa10:\n", - " use_blueprint: hgrid_2tier\n", - " fa11:\n", - " use_blueprint: hgrid_2tier\n", - " fa12:\n", - " use_blueprint: hgrid_2tier\n", - " fa13:\n", - " use_blueprint: hgrid_2tier\n", - " fa14:\n", - " use_blueprint: hgrid_2tier\n", - " fa15:\n", - " use_blueprint: hgrid_2tier\n", - " fa16:\n", + " fa[1-16]:\n", " use_blueprint: hgrid_2tier\n", " \n", " dc_fabric:\n", " groups:\n", - " plane1:\n", - " use_blueprint: f16_2tier\n", - " plane2:\n", - " use_blueprint: f16_2tier\n", - " plane3:\n", - " use_blueprint: f16_2tier\n", - " plane4:\n", - " use_blueprint: f16_2tier\n", - " plane5:\n", - " use_blueprint: f16_2tier\n", - " plane6:\n", - " use_blueprint: f16_2tier\n", - " plane7:\n", - " use_blueprint: f16_2tier\n", - " plane8:\n", + " plane[1-8]:\n", " use_blueprint: f16_2tier\n", "\n", " pod1:\n", @@ -143,71 +99,26 @@ "\n", " ebb:\n", " groups:\n", - " eb01:\n", - " node_count: 4\n", - " eb02:\n", - " node_count: 4\n", - " eb03:\n", - " node_count: 4\n", - " eb04:\n", - " node_count: 4\n", - " eb05:\n", - " node_count: 4\n", - " eb06:\n", - " node_count: 4\n", - " eb07:\n", - " node_count: 4\n", - " eb08:\n", - " node_count: 4 \n", + " eb0[1-8]:\n", + " node_count: 4 \n", "\n", " adjacency:\n", - " - source: eb01\n", - " target: eb01\n", - " pattern: mesh\n", - " link_params: { capacity: 3200, cost: 10 }\n", - " - source: eb02\n", - " target: eb02\n", - " pattern: mesh\n", - " link_params: { capacity: 3200, cost: 10 }\n", - " - source: eb03\n", - " target: eb03\n", - " pattern: mesh\n", - " link_params: { capacity: 3200, cost: 10 }\n", - " - source: eb04\n", - " target: eb04\n", - " pattern: mesh\n", - " link_params: { capacity: 3200, cost: 10 }\n", - " - source: eb05\n", - " target: eb05\n", - " pattern: mesh\n", - " link_params: { capacity: 3200, cost: 10 }\n", - " - source: eb06\n", - " target: eb06\n", - " pattern: mesh\n", - " link_params: { capacity: 3200, cost: 10 }\n", - " - source: eb07\n", - " target: eb07\n", - " pattern: mesh\n", - " link_params: { capacity: 3200, cost: 10 }\n", - " - source: eb08\n", - " target: eb08\n", - " pattern: mesh\n", - " link_params: { capacity: 3200, cost: 10 }\n", + " - source: \"eb0{idx}\"\n", + " target: \"eb0{idx}\"\n", + " expand_vars:\n", + " idx: [1, 2, 3, 4, 5, 6, 7, 8]\n", + " expansion_mode: \"zip\"\n", + " pattern: \"mesh\"\n", + " link_params: \n", + " capacity: 3200\n", + " cost: 10\n", " \n", "network:\n", " name: \"fb_region\"\n", " version: 1.0\n", "\n", " groups:\n", - " dc1:\n", - " use_blueprint: dc_fabric\n", - " dc2:\n", - " use_blueprint: dc_fabric\n", - " dc3:\n", - " use_blueprint: dc_fabric\n", - " dc5:\n", - " use_blueprint: dc_fabric\n", - " dc6:\n", + " dc[1-3, 5-6]:\n", " use_blueprint: dc_fabric\n", "\n", " fa:\n", @@ -217,98 +128,10 @@ " use_blueprint: ebb\n", "\n", " adjacency:\n", - " - source: .*/ssw/.*\n", - " target: .*/fa1/fadu/.*\n", - " pattern: one_to_one\n", - " link_params:\n", - " capacity: 200\n", - " cost: 1 \n", - " - source: .*/ssw/.*\n", - " target: .*/fa2/fadu/.*\n", - " pattern: one_to_one\n", - " link_params:\n", - " capacity: 200\n", - " cost: 1 \n", - " - source: .*/ssw/.*\n", - " target: .*/fa3/fadu/.*\n", - " pattern: one_to_one\n", - " link_params:\n", - " capacity: 200\n", - " cost: 1\n", - " - source: .*/ssw/.*\n", - " target: .*/fa4/fadu/.*\n", - " pattern: one_to_one\n", - " link_params:\n", - " capacity: 200\n", - " cost: 1 \n", - " - source: .*/ssw/.*\n", - " target: .*/fa5/fadu/.*\n", - " pattern: one_to_one\n", - " link_params:\n", - " capacity: 200\n", - " cost: 1 \n", - " - source: .*/ssw/.*\n", - " target: .*/fa6/fadu/.*\n", - " pattern: one_to_one\n", - " link_params:\n", - " capacity: 200\n", - " cost: 1 \n", - " - source: .*/ssw/.*\n", - " target: .*/fa7/fadu/.*\n", - " pattern: one_to_one\n", - " link_params:\n", - " capacity: 200\n", - " cost: 1 \n", - " - source: .*/ssw/.*\n", - " target: .*/fa8/fadu/.*\n", - " pattern: one_to_one\n", - " link_params:\n", - " capacity: 200\n", - " cost: 1\n", - " - source: .*/ssw/.*\n", - " target: .*/fa9/fadu/.*\n", - " pattern: one_to_one\n", - " link_params:\n", - " capacity: 200\n", - " cost: 1 \n", - " - source: .*/ssw/.*\n", - " target: .*/fa10/fadu/.*\n", - " pattern: one_to_one\n", - " link_params:\n", - " capacity: 200\n", - " cost: 1 \n", - " - source: .*/ssw/.*\n", - " target: .*/fa11/fadu/.*\n", - " pattern: one_to_one\n", - " link_params:\n", - " capacity: 200\n", - " cost: 1\n", - " - source: .*/ssw/.*\n", - " target: .*/fa12/fadu/.*\n", - " pattern: one_to_one\n", - " link_params:\n", - " capacity: 200\n", - " cost: 1 \n", - " - source: .*/ssw/.*\n", - " target: .*/fa13/fadu/.*\n", - " pattern: one_to_one\n", - " link_params:\n", - " capacity: 200\n", - " cost: 1 \n", - " - source: .*/ssw/.*\n", - " target: .*/fa14/fadu/.*\n", - " pattern: one_to_one\n", - " link_params:\n", - " capacity: 200\n", - " cost: 1 \n", - " - source: .*/ssw/.*\n", - " target: .*/fa15/fadu/.*\n", - " pattern: one_to_one\n", - " link_params:\n", - " capacity: 200\n", - " cost: 1 \n", - " - source: .*/ssw/.*\n", - " target: .*/fa16/fadu/.*\n", + " - source: \".*/ssw/\"\n", + " target: \".*/fa{fa_id}/fadu\"\n", + " expand_vars:\n", + " fa_id: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]\n", " pattern: one_to_one\n", " link_params:\n", " capacity: 200\n", @@ -355,7 +178,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 11, "metadata": {}, "outputs": [ { @@ -364,7 +187,7 @@ "Node(name='dc1/plane1/ssw/ssw-1', attrs={'hw_component': 'Minipack2_128x200GE', 'type': 'node', 'disabled': False})" ] }, - "execution_count": 3, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -375,7 +198,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 12, "metadata": {}, "outputs": [ { @@ -384,7 +207,7 @@ "Component(name='Minipack2_128x200GE', component_type='router', description='', cost=0.0, power_watts=1750.0, power_watts_max=0.0, capacity=0.0, ports=0, count=1, attrs={}, children={})" ] }, - "execution_count": 4, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -396,7 +219,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 13, "metadata": {}, "outputs": [ { @@ -405,7 +228,7 @@ "{('.*/fsw.*', '.*/eb.*'): 819200.0}" ] }, - "execution_count": 5, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } @@ -421,249 +244,249 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - " 6761460 function calls (6746059 primitive calls) in 1.968 seconds\n", + " 6762378 function calls (6746975 primitive calls) in 2.177 seconds\n", "\n", " Ordered by: internal time\n", "\n", " ncalls tottime percall cumtime percall filename:lineno(function)\n", - " 339840 0.356 0.000 1.061 0.000 /Users/networmix/ws/NetGraph/ngraph/lib/graph.py:143(add_edge)\n", - " 339840 0.226 0.000 0.242 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/networkx/classes/coreviews.py:81(__getitem__)\n", - " 1 0.209 0.209 0.231 0.231 /Users/networmix/ws/NetGraph/ngraph/lib/algorithms/spf.py:94(_spf_fast_all_min_cost_with_cap_remaining_dijkstra)\n", - " 339840 0.209 0.000 0.290 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/networkx/classes/multidigraph.py:417(add_edge)\n", - " 1 0.151 0.151 0.175 0.175 /Users/networmix/ws/NetGraph/ngraph/lib/algorithms/calc_capacity.py:10(_init_graph_data)\n", - " 1 0.137 0.137 1.128 1.128 /Users/networmix/ws/NetGraph/ngraph/network.py:131(to_strict_multidigraph)\n", - " 1 0.129 0.129 0.179 0.179 /Users/networmix/ws/NetGraph/ngraph/lib/algorithms/flow_init.py:6(init_flow_graph)\n", - " 339840 0.052 0.000 0.067 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/networkx/classes/coreviews.py:103(__getitem__)\n", - " 1 0.052 0.052 0.285 0.285 /Users/networmix/ws/NetGraph/ngraph/lib/algorithms/calc_capacity.py:294(calc_graph_capacity)\n", - " 725446 0.050 0.000 0.050 0.000 {method 'setdefault' of 'dict' objects}\n", - " 685700 0.040 0.000 0.040 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/networkx/classes/graph.py:462(__contains__)\n", - " 339840 0.037 0.000 0.104 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/networkx/classes/graph.py:498(__getitem__)\n", - " 345859 0.033 0.000 0.033 0.000 {method 'update' of 'dict' objects}\n", - " 345858 0.032 0.000 0.047 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/networkx/utils/misc.py:595(_clear_cache)\n", - " 679681 0.030 0.000 0.030 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/networkx/classes/coreviews.py:44(__init__)\n", - " 15393/1 0.028 0.000 0.033 0.033 /Users/networmix/ws/NetGraph/ngraph/lib/algorithms/calc_capacity.py:178(_push_flow_dfs)\n", - " 1 0.021 0.021 0.318 0.318 /Users/networmix/ws/NetGraph/ngraph/lib/algorithms/place_flow.py:29(place_flow_on_graph)\n", - " 279548 0.019 0.000 0.019 0.000 {method 'get' of 'dict' objects}\n", - " 373200 0.018 0.000 0.018 0.000 {method 'items' of 'dict' objects}\n", - " 339840 0.015 0.000 0.015 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/networkx/classes/coreviews.py:53(__getitem__)\n", - " 345866 0.015 0.000 0.015 0.000 {built-in method builtins.getattr}\n", + " 339840 0.399 0.000 1.184 0.000 /Users/networmix/ws/NetGraph/ngraph/lib/graph.py:143(add_edge)\n", + " 339840 0.253 0.000 0.269 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/networkx/classes/coreviews.py:81(__getitem__)\n", + " 1 0.251 0.251 0.274 0.274 /Users/networmix/ws/NetGraph/ngraph/lib/algorithms/spf.py:94(_spf_fast_all_min_cost_with_cap_remaining_dijkstra)\n", + " 339840 0.237 0.000 0.327 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/networkx/classes/multidigraph.py:417(add_edge)\n", + " 1 0.165 0.165 0.189 0.189 /Users/networmix/ws/NetGraph/ngraph/lib/algorithms/calc_capacity.py:10(_init_graph_data)\n", + " 1 0.155 0.155 1.281 1.281 /Users/networmix/ws/NetGraph/ngraph/network.py:131(to_strict_multidigraph)\n", + " 4/2 0.068 0.017 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/events.py:87(_run)\n", + " 1 0.061 0.061 0.111 0.111 /Users/networmix/ws/NetGraph/ngraph/lib/algorithms/flow_init.py:6(init_flow_graph)\n", + " 339840 0.056 0.000 0.073 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/networkx/classes/coreviews.py:103(__getitem__)\n", + " 1 0.053 0.053 0.303 0.303 /Users/networmix/ws/NetGraph/ngraph/lib/algorithms/calc_capacity.py:294(calc_graph_capacity)\n", + " 725446 0.052 0.000 0.052 0.000 {method 'setdefault' of 'dict' objects}\n", + " 685700 0.044 0.000 0.044 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/networkx/classes/graph.py:462(__contains__)\n", + " 339840 0.039 0.000 0.112 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/networkx/classes/graph.py:498(__getitem__)\n", + " 345859 0.036 0.000 0.036 0.000 {method 'update' of 'dict' objects}\n", + " 345858 0.034 0.000 0.051 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/networkx/utils/misc.py:595(_clear_cache)\n", + " 679681 0.033 0.000 0.033 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/networkx/classes/coreviews.py:44(__init__)\n", + " 15393/1 0.029 0.000 0.035 0.035 /Users/networmix/ws/NetGraph/ngraph/lib/algorithms/calc_capacity.py:178(_push_flow_dfs)\n", + " 1 0.023 0.023 0.339 0.339 /Users/networmix/ws/NetGraph/ngraph/lib/algorithms/place_flow.py:29(place_flow_on_graph)\n", + " 279548 0.020 0.000 0.020 0.000 {method 'get' of 'dict' objects}\n", + " 373200 0.020 0.000 0.020 0.000 {method 'items' of 'dict' objects}\n", + " 339840 0.017 0.000 0.017 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/networkx/classes/coreviews.py:53(__getitem__)\n", + " 345866 0.016 0.000 0.016 0.000 {built-in method builtins.getattr}\n", " 341568 0.013 0.000 0.013 0.000 {built-in method builtins.abs}\n", - " 182336 0.011 0.000 0.016 0.000 {built-in method builtins.sum}\n", + " 182336 0.012 0.000 0.017 0.000 {built-in method builtins.sum}\n", " 2 0.011 0.006 0.012 0.006 /Users/networmix/ws/NetGraph/ngraph/lib/algorithms/calc_capacity.py:148(_set_levels_bfs)\n", " 177732 0.010 0.000 0.010 0.000 {method 'append' of 'list' objects}\n", - " 1 0.006 0.006 0.036 0.036 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/zmq/sugar/socket.py:709(send_multipart)\n", - " 24128 0.005 0.000 0.005 0.000 /Users/networmix/ws/NetGraph/ngraph/lib/algorithms/place_flow.py:104()\n", - " 1 0.005 0.005 1.894 1.894 /Users/networmix/ws/NetGraph/ngraph/network.py:291(_compute_flow_single_group)\n", + " 24128 0.006 0.000 0.006 0.000 /Users/networmix/ws/NetGraph/ngraph/lib/algorithms/place_flow.py:104()\n", + " 1 0.005 0.005 2.040 2.040 /Users/networmix/ws/NetGraph/ngraph/network.py:291(_compute_flow_single_group)\n", + " 2 0.005 0.002 0.030 0.015 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/zmq/sugar/socket.py:709(send_multipart)\n", " 3872 0.004 0.000 0.004 0.000 {built-in method posix.urandom}\n", - " 3872 0.003 0.000 0.004 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/uuid.py:142(__init__)\n", - " 6018 0.003 0.000 0.003 0.000 {built-in method _heapq.heappop}\n", + " 6018 0.004 0.000 0.004 0.000 {built-in method _heapq.heappop}\n", + " 3872 0.004 0.000 0.005 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/uuid.py:142(__init__)\n", + " 1744 0.003 0.000 0.007 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/ipykernel/ipkernel.py:775(_clean_thread_parent_frames)\n", " 41412 0.003 0.000 0.003 0.000 {method 'add' of 'set' objects}\n", - " 1 0.003 0.003 0.020 0.020 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/IPython/core/history.py:845(writeout_cache)\n", " 6018 0.003 0.000 0.004 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/networkx/classes/digraph.py:439(add_node)\n", - " 1 0.003 0.003 0.069 0.069 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/ipykernel/iostream.py:278(_really_send)\n", - " 1746 0.002 0.000 0.004 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/ipykernel/ipkernel.py:775(_clean_thread_parent_frames)\n", + " 2 0.002 0.001 0.075 0.037 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/ipykernel/iostream.py:278(_really_send)\n", " 6018 0.002 0.000 0.007 0.000 /Users/networmix/ws/NetGraph/ngraph/lib/graph.py:104(add_node)\n", - " 1 0.002 0.002 0.729 0.729 /Users/networmix/ws/NetGraph/ngraph/lib/algorithms/max_flow.py:8(calc_max_flow)\n", - " 2 0.002 0.001 0.004 0.002 /Users/networmix/ws/NetGraph/ngraph/network.py:188(select_node_groups_by_path)\n", - " 3872 0.002 0.000 0.014 0.000 /Users/networmix/ws/NetGraph/ngraph/lib/graph.py:11(new_base64_uuid)\n", - " 12032 0.001 0.000 0.001 0.000 {method 'match' of 're.Pattern' objects}\n", - " 3872 0.001 0.000 0.009 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/uuid.py:710(uuid4)\n", - " 2/1 0.001 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/base_events.py:1953(_run_once)\n", - " 2 0.001 0.000 0.001 0.000 /Users/networmix/ws/NetGraph/ngraph/lib/graph.py:254(get_nodes)\n", - " 873 0.001 0.000 0.001 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/threading.py:1477(enumerate)\n", + " 1 0.002 0.002 0.726 0.726 /Users/networmix/ws/NetGraph/ngraph/lib/algorithms/max_flow.py:8(calc_max_flow)\n", + " 2 0.002 0.001 0.005 0.002 /Users/networmix/ws/NetGraph/ngraph/network.py:188(select_node_groups_by_path)\n", + " 2/1 0.002 0.001 0.006 0.006 {method 'control' of 'select.kqueue' objects}\n", + " 12032 0.002 0.000 0.002 0.000 {method 'match' of 're.Pattern' objects}\n", + " 3872 0.002 0.000 0.015 0.000 /Users/networmix/ws/NetGraph/ngraph/lib/graph.py:11(new_base64_uuid)\n", + " 872 0.001 0.000 0.002 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/threading.py:1477(enumerate)\n", + " 3872 0.001 0.000 0.010 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/uuid.py:710(uuid4)\n", + " 14 0.001 0.000 0.007 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/zmq/sugar/socket.py:632(send)\n", + " 1 0.001 0.001 0.006 0.006 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/IPython/core/history.py:845(writeout_cache)\n", + " 2 0.001 0.000 0.063 0.032 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:547(_run_callback)\n", + " 6976 0.001 0.000 0.001 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/threading.py:1110(ident)\n", " 3872 0.001 0.000 0.002 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/base64.py:112(urlsafe_b64encode)\n", " 15393 0.001 0.000 0.001 0.000 {built-in method builtins.min}\n", - " 12075 0.001 0.000 0.001 0.000 {method 'popleft' of 'collections.deque' objects}\n", + " 2 0.001 0.000 0.001 0.000 /Users/networmix/ws/NetGraph/ngraph/lib/graph.py:254(get_nodes)\n", + " 12076 0.001 0.000 0.001 0.000 {method 'popleft' of 'collections.deque' objects}\n", " 12073 0.001 0.000 0.001 0.000 {method 'append' of 'collections.deque' objects}\n", - " 6111 0.001 0.000 0.001 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/threading.py:1110(ident)\n", - " 3872 0.000 0.000 0.014 0.000 /Users/networmix/ws/NetGraph/ngraph/lib/graph.py:64(new_edge_key)\n", - " 3872 0.000 0.000 0.001 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/uuid.py:288(bytes)\n", - " 6017 0.000 0.000 0.000 0.000 {built-in method _heapq.heappush}\n", + " 3872 0.001 0.000 0.001 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/uuid.py:288(bytes)\n", + " 3872 0.000 0.000 0.015 0.000 /Users/networmix/ws/NetGraph/ngraph/lib/graph.py:64(new_edge_key)\n", " 3872 0.000 0.000 0.001 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/base64.py:51(b64encode)\n", - "5736/5732 0.000 0.000 0.000 0.000 {built-in method builtins.isinstance}\n", + " 6017 0.000 0.000 0.000 0.000 {built-in method _heapq.heappush}\n", + "5756/5752 0.000 0.000 0.000 0.000 {built-in method builtins.isinstance}\n", " 3872 0.000 0.000 0.000 0.000 {method 'to_bytes' of 'int' objects}\n", " 3872 0.000 0.000 0.000 0.000 {method 'count' of 'list' objects}\n", " 3872 0.000 0.000 0.000 0.000 {method 'translate' of 'bytes' objects}\n", + " 3490 0.000 0.000 0.000 0.000 {method 'keys' of 'dict' objects}\n", " 3872 0.000 0.000 0.000 0.000 {method 'decode' of 'bytes' objects}\n", - " 3872 0.000 0.000 0.000 0.000 {built-in method from_bytes}\n", " 3872 0.000 0.000 0.000 0.000 {built-in method binascii.b2a_base64}\n", - " 3494 0.000 0.000 0.000 0.000 {method 'keys' of 'dict' objects}\n", - " 3886 0.000 0.000 0.000 0.000 {built-in method builtins.len}\n", + " 3872 0.000 0.000 0.000 0.000 {built-in method from_bytes}\n", + " 3887 0.000 0.000 0.000 0.000 {built-in method builtins.len}\n", + " 1750 0.000 0.000 0.000 0.000 {method 'values' of 'dict' objects}\n", " 3872 0.000 0.000 0.000 0.000 {method 'groups' of 're.Match' objects}\n", - " 1752 0.000 0.000 0.000 0.000 {method 'values' of 'dict' objects}\n", - " 11 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/zmq/sugar/socket.py:632(send)\n", - " 874 0.000 0.000 0.000 0.000 {method '__exit__' of '_thread.RLock' objects}\n", + " 873 0.000 0.000 0.000 0.000 {method '__exit__' of '_thread.RLock' objects}\n", " 4 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/zmq/sugar/attrsettr.py:66(_get_attr_opt)\n", " 2 0.000 0.000 0.000 0.000 {built-in method builtins.compile}\n", - " 1 0.000 0.000 0.000 0.000 {method 'execute' of 'sqlite3.Connection' objects}\n", + " 15 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/enum.py:1592(__or__)\n", " 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}\n", - " 1 0.000 0.000 0.020 0.020 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/decorator.py:229(fun)\n", - " 12 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/enum.py:1592(__or__)\n", - " 54 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/enum.py:1585(_get_value)\n", - " 2/1 0.000 0.000 0.000 0.000 {method 'control' of 'select.kqueue' objects}\n", - " 23 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/enum.py:695(__call__)\n", + " 63 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/enum.py:1585(_get_value)\n", + " 2/1 0.000 0.000 0.006 0.006 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/base_events.py:1953(_run_once)\n", + " 27 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/enum.py:695(__call__)\n", " 4 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/zmq/sugar/attrsettr.py:43(__getattr__)\n", + " 1 0.000 0.000 0.000 0.000 {method 'execute' of 'sqlite3.Connection' objects}\n", " 2 0.000 0.000 0.000 0.000 {method 'recv' of '_socket.socket' objects}\n", + " 27 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/enum.py:1152(__new__)\n", + " 2/1 0.000 0.000 0.006 0.006 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/selectors.py:540(select)\n", " 1 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/inspect.py:3120(_bind)\n", - " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/ipykernel/kernelbase.py:302(poll_control_queue)\n", - " 23 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/enum.py:1152(__new__)\n", - " 3 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:654(_rebuild_io_state)\n", - " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/zmq/sugar/socket.py:780(recv_multipart)\n", - " 6 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/enum.py:1603(__and__)\n", + " 1 0.000 0.000 0.013 0.013 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/IPython/core/history.py:55(only_when_enabled)\n", " 2 0.000 0.000 0.000 0.000 {method '__exit__' of 'sqlite3.Connection' objects}\n", + " 3 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:654(_rebuild_io_state)\n", + " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/ipykernel/kernelbase.py:302(poll_control_queue)\n", " 8 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/traitlets/traitlets.py:676(__get__)\n", - " 2/1 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/selectors.py:540(select)\n", - " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/networkx/classes/digraph.py:334(__init__)\n", - " 3 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/queue.py:115(empty)\n", " 1 0.000 0.000 0.000 0.000 {method 'send' of '_socket.socket' objects}\n", + " 6 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/enum.py:1603(__and__)\n", + " 6 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/typing.py:426(inner)\n", + " 3 0.000 0.000 0.000 0.000 {method 'run' of '_contextvars.Context' objects}\n", + " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/zmq/sugar/socket.py:780(recv_multipart)\n", + " 2 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/IPython/core/interactiveshell.py:3495(compare)\n", " 3 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:677(_update_handler)\n", - " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/IPython/core/history.py:833(_writeout_input_cache)\n", + " 2 0.000 0.000 0.075 0.037 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/ipykernel/iostream.py:276()\n", + " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/networkx/classes/digraph.py:334(__init__)\n", + " 2 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/typing.py:1665(__subclasscheck__)\n", + " 4 0.000 0.000 0.000 0.000 {method 'extend' of 'list' objects}\n", + " 1 0.000 0.000 0.025 0.025 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/decorator.py:229(fun)\n", " 8 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/traitlets/traitlets.py:629(get)\n", - " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/tornado/platform/asyncio.py:225(add_callback)\n", " 3 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/base_events.py:847(_call_soon)\n", + " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph/lib/graph.py:49(__init__)\n", " 2 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/codeop.py:113(__call__)\n", - " 10 0.000 0.000 0.000 0.000 {built-in method builtins.next}\n", - " 2 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/functools.py:1023(__get__)\n", - " 3 0.000 0.000 0.000 0.000 {method 'run' of '_contextvars.Context' objects}\n", - " 2 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/typing.py:1665(__subclasscheck__)\n", - " 1 0.000 0.000 0.231 0.231 /Users/networmix/ws/NetGraph/ngraph/lib/algorithms/spf.py:159(spf)\n", - " 3 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/typing.py:426(inner)\n", - " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/tornado/queues.py:209(put_nowait)\n", + " 3 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/queue.py:115(empty)\n", " 4 0.000 0.000 0.000 0.000 :1390(_handle_fromlist)\n", - " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/tornado/queues.py:225(get)\n", - " 1 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/inspect.py:3259(bind)\n", - " 1 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/concurrent/futures/_base.py:537(set_result)\n", - " 3 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/events.py:36(__init__)\n", + " 3 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/base_events.py:767(time)\n", + " 2 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/functools.py:1023(__get__)\n", + " 9 0.000 0.000 0.000 0.000 {built-in method builtins.hasattr}\n", + " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/tornado/platform/asyncio.py:225(add_callback)\n", + " 10 0.000 0.000 0.000 0.000 {built-in method builtins.next}\n", " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/decorator.py:199(fix)\n", - " 4 0.000 0.000 0.000 0.000 {method 'extend' of 'list' objects}\n", + " 2 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/typing.py:1374(__instancecheck__)\n", " 1 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/selector_events.py:129(_read_from_self)\n", + " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/tornado/queues.py:225(get)\n", + " 1 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/concurrent/futures/_base.py:537(set_result)\n", + " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:574(_handle_events)\n", + " 1 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/inspect.py:3259(bind)\n", + " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/tornado/queues.py:209(put_nowait)\n", + " 2 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/traitlets/traitlets.py:689(set)\n", + " 1 0.000 0.000 0.274 0.274 /Users/networmix/ws/NetGraph/ngraph/lib/algorithms/spf.py:159(spf)\n", " 2 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/traitlets/traitlets.py:3631(set)\n", - " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph/lib/graph.py:49(__init__)\n", - " 2 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/re/__init__.py:330(_compile)\n", - " 9 0.000 0.000 0.000 0.000 {built-in method builtins.hasattr}\n", + " 2 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/ipykernel/iostream.py:213(_is_master_process)\n", + " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/tornado/queues.py:186(put)\n", + " 4 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/typing.py:1443(__hash__)\n", + " 3 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/events.py:36(__init__)\n", + " 2 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/traitlets/traitlets.py:3474(validate)\n", " 2 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/contextlib.py:108(__init__)\n", - " 3 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/events.py:87(_run)\n", " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/traitlets/traitlets.py:1527(_notify_observers)\n", - " 3 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/base_events.py:767(time)\n", - " 2 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/traitlets/traitlets.py:689(set)\n", - " 2 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/traitlets/traitlets.py:3474(validate)\n", + " 2 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/traitlets/traitlets.py:718(_validate)\n", + " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/networkx/classes/multidigraph.py:302(__init__)\n", + " 2 0.000 0.000 0.062 0.031 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/ipykernel/iostream.py:157(_handle_event)\n", + " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/tornado/ioloop.py:742(_run_callback)\n", + " 2 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/traitlets/traitlets.py:727(_cross_validate)\n", + " 2 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/contextlib.py:303(helper)\n", " 2 0.000 0.000 0.000 0.000 {built-in method _abc._abc_subclasscheck}\n", - " 3 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/threading.py:303(__enter__)\n", + " 1 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/inspect.py:2934(apply_defaults)\n", + " 2 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/re/__init__.py:330(_compile)\n", + " 1 0.000 0.000 0.000 0.000 /var/folders/xh/83kdwyfd0fv66b04mchbfzcc0000gn/T/ipykernel_98256/1424787899.py:1()\n", + " 1 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/threading.py:315(_acquire_restore)\n", " 3 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/networkx/classes/reportviews.py:207(__call__)\n", - " 1 0.000 0.000 0.000 0.000 :2(__init__)\n", + " 2 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/traitlets/traitlets.py:3624(validate_elements)\n", + " 2 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/tornado/queues.py:322(_consume_expired)\n", + " 3 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/threading.py:303(__enter__)\n", + " 1 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/threading.py:631(clear)\n", + " 2 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/networkx/classes/reportviews.py:333(__iter__)\n", " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:615(_handle_recv)\n", - " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/tornado/queues.py:186(put)\n", - " 1 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/inspect.py:2934(apply_defaults)\n", - " 2 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/traitlets/traitlets.py:718(_validate)\n", - " 2 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/typing.py:1443(__hash__)\n", - " 1 0.000 0.000 0.000 0.000 /var/folders/xh/83kdwyfd0fv66b04mchbfzcc0000gn/T/ipykernel_88989/1424787899.py:1()\n", - " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/ipykernel/iostream.py:213(_is_master_process)\n", - " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:574(_handle_events)\n", - " 2 0.000 0.000 0.000 0.000 :121(__subclasscheck__)\n", - " 2 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/contextlib.py:303(helper)\n", - " 3 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/queue.py:267(_qsize)\n", + " 3 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:533(sending)\n", + " 5 0.000 0.000 0.000 0.000 {built-in method builtins.iter}\n", + " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/IPython/core/history.py:833(_writeout_input_cache)\n", + " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/networkx/classes/graph.py:745(nodes)\n", + " 2 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/traitlets/traitlets.py:708(__set__)\n", " 2 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/base_events.py:818(call_soon)\n", + " 1 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/futures.py:391(_call_set_state)\n", + " 2 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/ipykernel/iostream.py:216(_check_mp_mode)\n", " 4 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/IPython/core/compilerop.py:180(extra_flags)\n", - " 2 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/re/__init__.py:287(compile)\n", - " 2 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/typing.py:1374(__instancecheck__)\n", + " 2 0.000 0.000 0.000 0.000 {built-in method builtins.issubclass}\n", + " 2 0.000 0.000 0.000 0.000 {built-in method builtins.sorted}\n", " 2 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/contextlib.py:145(__exit__)\n", - " 3 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:533(sending)\n", - " 2 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/traitlets/traitlets.py:727(_cross_validate)\n", - " 2 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/traitlets/traitlets.py:3624(validate_elements)\n", - " 1 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/threading.py:315(_acquire_restore)\n", - " 1 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/futures.py:391(_call_set_state)\n", - " 2 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/traitlets/traitlets.py:708(__set__)\n", - " 5 0.000 0.000 0.000 0.000 {built-in method builtins.iter}\n", - " 1 0.000 0.000 0.069 0.069 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/ipykernel/iostream.py:276()\n", + " 1/0 0.000 0.000 0.000 {built-in method builtins.exec}\n", + " 1 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/selector_events.py:141(_write_to_self)\n", + " 1 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/inspect.py:2881(args)\n", + " 3 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/threading.py:306(__exit__)\n", " 2 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/traitlets/traitlets.py:2304(validate)\n", + " 2 0.000 0.000 0.000 0.000 :121(__subclasscheck__)\n", + " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/tornado/queues.py:317(__put_internal)\n", + " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/tornado/queues.py:256(get_nowait)\n", + " 2 0.000 0.000 0.000 0.000 {method 'set_result' of '_asyncio.Future' objects}\n", + " 2 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/re/__init__.py:287(compile)\n", + " 1 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/base_events.py:871(call_soon_threadsafe)\n", + " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/IPython/core/history.py:839(_writeout_output_cache)\n", + " 28 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/typing.py:2367(cast)\n", " 1 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/threading.py:428(notify_all)\n", + " 4 0.000 0.000 0.000 0.000 {method 'upper' of 'str' objects}\n", " 1/0 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/IPython/core/interactiveshell.py:3543(run_code)\n", - " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/networkx/classes/multidigraph.py:302(__init__)\n", " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/traitlets/traitlets.py:1523(notify_change)\n", - " 2 0.000 0.000 0.000 0.000 {built-in method builtins.issubclass}\n", - " 1 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/inspect.py:2881(args)\n", - " 2 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/networkx/classes/reportviews.py:333(__iter__)\n", - " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/IPython/core/history.py:839(_writeout_output_cache)\n", " 2 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/contextlib.py:136(__enter__)\n", - " 1 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/threading.py:312(_release_save)\n", - " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/networkx/classes/graph.py:63(__set__)\n", - " 1/0 0.000 0.000 0.000 {built-in method builtins.exec}\n", - " 2 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/tornado/queues.py:322(_consume_expired)\n", - " 2 0.000 0.000 0.000 0.000 {built-in method builtins.sorted}\n", - " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/networkx/classes/graph.py:745(nodes)\n", - " 1 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/base_events.py:871(call_soon_threadsafe)\n", - " 3 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/threading.py:306(__exit__)\n", - " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/tornado/queues.py:256(get_nowait)\n", + " 1 0.000 0.000 0.000 0.000 {method 'values' of 'mappingproxy' objects}\n", " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/traitlets/traitlets.py:1512(_notify_trait)\n", - " 1 0.000 0.000 0.020 0.020 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/IPython/core/history.py:55(only_when_enabled)\n", - " 2 0.000 0.000 0.000 0.000 {method 'set_result' of '_asyncio.Future' objects}\n", - " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/networkx/classes/reportviews.py:187(__iter__)\n", - " 27 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/typing.py:2367(cast)\n", - " 4 0.000 0.000 0.000 0.000 {method 'upper' of 'str' objects}\n", + " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/tornado/concurrent.py:182(future_set_result_unless_cancelled)\n", " 4 0.000 0.000 0.000 0.000 {built-in method builtins.max}\n", - " 2 0.000 0.000 0.000 0.000 {built-in method _contextvars.copy_context}\n", + " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/networkx/classes/graph.py:63(__set__)\n", + " 3 0.000 0.000 0.000 0.000 {built-in method time.monotonic}\n", + " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/networkx/classes/multidigraph.py:365(adj)\n", + " 1 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/inspect.py:2904(kwargs)\n", + " 3 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/queue.py:267(_qsize)\n", " 1 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/concurrent/futures/_base.py:337(_invoke_callbacks)\n", - " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/networkx/classes/reportviews.py:180(__init__)\n", - " 1 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/selector_events.py:141(_write_to_self)\n", + " 1 0.000 0.000 0.000 0.000 :2(__init__)\n", + " 1 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/threading.py:312(_release_save)\n", + " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:685()\n", " 7 0.000 0.000 0.000 0.000 {method '__exit__' of '_thread.lock' objects}\n", - " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/networkx/classes/multidigraph.py:365(adj)\n", - " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/tornado/ioloop.py:742(_run_callback)\n", - " 1 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/threading.py:398(notify)\n", - " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/tornado/queues.py:317(__put_internal)\n", + " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/tornado/queues.py:312(_put)\n", + " 2 0.000 0.000 0.000 0.000 {built-in method _contextvars.copy_context}\n", + " 3 0.000 0.000 0.000 0.000 {method 'items' of 'mappingproxy' objects}\n", " 3 0.000 0.000 0.000 0.000 {method 'acquire' of '_thread.lock' objects}\n", - " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:547(_run_callback)\n", - " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/tornado/concurrent.py:182(future_set_result_unless_cancelled)\n", - " 2 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/IPython/core/interactiveshell.py:3495(compare)\n", + " 2 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/selector_events.py:740(_process_events)\n", + " 1 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/threading.py:398(notify)\n", + " 4 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:529(receiving)\n", " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/networkx/classes/digraph.py:41(__set__)\n", - " 1 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/threading.py:631(clear)\n", - " 1 0.000 0.000 0.000 0.000 {method 'values' of 'mappingproxy' objects}\n", - " 3 0.000 0.000 0.000 0.000 {built-in method time.monotonic}\n", - " 1 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/inspect.py:2904(kwargs)\n", - " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/ipykernel/iostream.py:157(_handle_event)\n", - " 10 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/inspect.py:2791(kind)\n", " 2 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/traitlets/traitlets.py:3486(validate_elements)\n", - " 3 0.000 0.000 0.000 0.000 {method 'items' of 'mappingproxy' objects}\n", - " 1 0.000 0.000 0.000 0.000 {method '__enter__' of '_thread.RLock' objects}\n", + " 10 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/inspect.py:2791(kind)\n", + " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/networkx/classes/reportviews.py:187(__iter__)\n", + " 2 0.000 0.000 0.000 0.000 {built-in method posix.getpid}\n", + " 4 0.000 0.000 0.000 0.000 {built-in method builtins.hash}\n", + " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/networkx/classes/digraph.py:78(__set__)\n", " 2 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/IPython/core/interactiveshell.py:1277(user_global_ns)\n", - " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/ipykernel/iostream.py:216(_check_mp_mode)\n", - " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/tornado/queues.py:173(qsize)\n", - " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/tornado/queues.py:312(_put)\n", + " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/networkx/classes/reportviews.py:180(__init__)\n", + " 3 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph/lib/graph.py:263(get_edges)\n", + " 5 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/base_events.py:2051(get_debug)\n", " 1 0.000 0.000 0.000 0.000 {built-in method _asyncio.get_running_loop}\n", + " 1 0.000 0.000 0.000 0.000 {method '__enter__' of '_thread.RLock' objects}\n", " 1 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/unix_events.py:83(_process_self_data)\n", - " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:685()\n", - " 1 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/threading.py:318(_is_owned)\n", + " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/tornado/queues.py:173(qsize)\n", + " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/tornado/queues.py:309(_get)\n", " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/tornado/queues.py:177(empty)\n", - " 5 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/base_events.py:2051(get_debug)\n", - " 4 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/inspect.py:3074(parameters)\n", - " 2 0.000 0.000 0.000 0.000 {method 'join' of 'str' objects}\n", - " 2 0.000 0.000 0.000 0.000 {built-in method builtins.hash}\n", - " 3 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/base_events.py:549(_check_closed)\n", - " 4 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/zmq/eventloop/zmqstream.py:529(receiving)\n", - " 3 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph/lib/graph.py:263(get_edges)\n", - " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/networkx/classes/digraph.py:78(__set__)\n", - " 1 0.000 0.000 0.000 0.000 {built-in method posix.getpid}\n", - " 2 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/selector_events.py:740(_process_events)\n", " 2 0.000 0.000 0.000 0.000 {method '__enter__' of '_thread.lock' objects}\n", - " 2 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/networkx/classes/reportviews.py:315(__init__)\n", + " 2 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/ipykernel/iostream.py:255(closed)\n", " 4 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/inspect.py:2779(name)\n", + " 2 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/networkx/classes/reportviews.py:315(__init__)\n", " 2 0.000 0.000 0.000 0.000 {method 'cancelled' of '_asyncio.Future' objects}\n", " 1 0.000 0.000 0.000 0.000 {method 'done' of '_asyncio.Future' objects}\n", + " 1 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/inspect.py:2873(__init__)\n", + " 2 0.000 0.000 0.000 0.000 {method 'join' of 'str' objects}\n", + " 4 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/inspect.py:3074(parameters)\n", + " 3 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/base_events.py:549(_check_closed)\n", + " 1 0.000 0.000 0.000 0.000 {method '_is_owned' of '_thread.RLock' objects}\n", + " 1 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/threading.py:318(_is_owned)\n", " 1 0.000 0.000 0.000 0.000 {built-in method _thread.allocate_lock}\n", - " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/tornado/queues.py:309(_get)\n", + " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/tornado/queues.py:59(_set_timeout)\n", " 1 0.000 0.000 0.000 0.000 {method 'release' of '_thread.lock' objects}\n", - " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/ipykernel/iostream.py:255(closed)\n", - " 1 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/base_events.py:753(is_closed)\n", - " 1 0.000 0.000 0.000 0.000 {method '_is_owned' of '_thread.RLock' objects}\n", - " 1 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/inspect.py:2873(__init__)\n", " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/tornado/locks.py:224(clear)\n", - " 1 0.000 0.000 0.000 0.000 /Users/networmix/ws/NetGraph/ngraph-venv/lib/python3.13/site-packages/tornado/queues.py:59(_set_timeout)\n", + " 1 0.000 0.000 0.000 0.000 /opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/base_events.py:753(is_closed)\n", "\n", "\n" ] @@ -671,10 +494,10 @@ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 6, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" } @@ -702,7 +525,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 15, "metadata": {}, "outputs": [], "source": [ @@ -711,7 +534,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 16, "metadata": {}, "outputs": [ { diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index 1ba9260..517722d 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -1014,3 +1014,653 @@ def test_existing_node_preserves_attrs(): # So "myattr" won't appear, because the node was created from groups. assert "myattr" not in node_obj.attrs assert node_obj.attrs["type"] == "node" + + +def test_expand_network_dsl_empty(): + """ + Tests calling expand_network_dsl with an empty dictionary => empty Network. + """ + net = expand_network_dsl({}) + assert len(net.nodes) == 0 + assert len(net.links) == 0 + assert not net.attrs, "No attributes expected in an empty scenario." + + +def test_network_version_attribute(): + """ + Tests that 'version' in the network data is recorded in net.attrs. + """ + scenario_data = { + "network": { + "version": "1.2.3", + } + } + net = expand_network_dsl(scenario_data) + assert net.attrs["version"] == "1.2.3" + + +def test_expand_group_with_bracket_expansions(): + """ + Tests bracket expansions in group names, e.g. group_name='fa[1-2]_plane[3-4]'. + We expect 4 expansions => fa1_plane3, fa1_plane4, fa2_plane3, fa2_plane4. + """ + ctx_net = Network() + ctx = DSLExpansionContext({}, ctx_net) + + group_def = {"node_count": 1} # We'll create 1 node per expanded group + _expand_group( + ctx, parent_path="", group_name="fa[1-2]_plane[3-4]", group_def=group_def + ) + + # We expect 4 groups => each with 1 node => total 4 nodes + expected_names = { + "fa1_plane3/fa1_plane3-1", + "fa1_plane4/fa1_plane4-1", + "fa2_plane3/fa2_plane3-1", + "fa2_plane4/fa2_plane4-1", + } + assert set(ctx_net.nodes.keys()) == expected_names + + +def test_apply_parameters_nested_fields(): + """ + Tests parameter overrides for nested fields beyond just 'attrs'. + E.g., 'leaf.some_field.nested_key = 999'. + """ + bp = Blueprint( + name="bp1", + groups={ + "leaf": { + "node_count": 1, + "some_field": {"nested_key": 111}, + }, + }, + adjacency=[], + ) + ctx_net = Network() + ctx = DSLExpansionContext(blueprints={"bp1": bp}, network=ctx_net) + + group_def = { + "use_blueprint": "bp1", + "parameters": { + "leaf.some_field.nested_key": 999, + }, + } + _expand_group( + ctx, + parent_path="Region", + group_name="Main", + group_def=group_def, + ) + + # Expect 1 node => "Region/Main/leaf/leaf-1" + assert len(ctx_net.nodes) == 1 + node_obj = ctx_net.nodes["Region/Main/leaf/leaf-1"] + # Check that nested field was updated + assert node_obj.attrs["some_field"]["nested_key"] == 999 + + +def test_link_overrides_repeated(): + """ + Tests applying multiple link_overrides in sequence to the same links. + Each override can set different fields, verifying that the last override + wins if there's a conflict. + """ + net = Network() + net.add_node(Node("S1")) + net.add_node(Node("T1")) + link = Link("S1", "T1", capacity=5, cost=5, attrs={}) + net.add_link(link) + + network_data = { + "link_overrides": [ + { + "source": "S1", + "target": "T1", + "link_params": { + "capacity": 100, + "attrs": {"color": "red"}, + }, + }, + { + "source": "S1", + "target": "T1", + "link_params": { + "cost": 999, + "attrs": {"color": "blue"}, # Overwrites 'color': 'red' + }, + }, + ] + } + _process_link_overrides(net, network_data) + assert link.capacity == 100 + assert link.cost == 999 + assert link.attrs["color"] == "blue" # second override wins + + +def test_node_overrides_repeated(): + """ + Tests multiple node_overrides in sequence, verifying that the last override + wins on conflicting attributes and merges others. + """ + net = Network() + net.add_node(Node("X", attrs={"existing": "keep"})) + + network_data = { + "node_overrides": [ + { + "path": "X", + "attrs": { + "role": "old_role", + "color": "red", + }, + }, + { + "path": "X", + "attrs": { + "role": "updated_role", + "speed": "fast", + }, + }, + ] + } + _process_node_overrides(net, network_data) + node_attrs = net.nodes["X"].attrs + + # existing attribute is retained + assert node_attrs["existing"] == "keep" + # role was overwritten by second override + assert node_attrs["role"] == "updated_role" + # color is from the first override but not overwritten by the second => remains + assert node_attrs["color"] == "red" + # speed is added by the second override + assert node_attrs["speed"] == "fast" + + +def test_adjacency_no_matching_source_or_target(): + """ + Tests adjacency expansion where the source or target path does not match any nodes. + Should simply skip and create no links. + """ + scenario_data = { + "network": { + "nodes": {"RealNode": {}}, + "adjacency": [ + { + "source": "/MissingSource", + "target": "/RealNode", + "pattern": "mesh", + }, + { + "source": "/RealNode", + "target": "/MissingTarget", + "pattern": "mesh", + }, + ], + } + } + net = expand_network_dsl(scenario_data) + # Only 1 node => RealNode + assert len(net.nodes) == 1 + # No valid adjacency => no links + assert len(net.links) == 0 + + +def test_provide_network_version_and_name(): + """ + Tests that both 'name' and 'version' keys in the DSL are captured as net.attrs. + """ + data = { + "network": { + "name": "NetGraphTest", + "version": "2.0-beta", + } + } + net = expand_network_dsl(data) + assert net.attrs["name"] == "NetGraphTest" + assert net.attrs["version"] == "2.0-beta" + + +def test_expand_group_with_bracket_expansions(): + """ + Tests bracket expansions in group names, e.g. group_name='fa[1-2]_plane[3-4]'. + We expect 4 expansions => fa1_plane3, fa1_plane4, fa2_plane3, fa2_plane4. + """ + ctx_net = Network() + ctx = DSLExpansionContext({}, ctx_net) + + group_def = {"node_count": 1} # We'll create 1 node per expanded group + _expand_group( + ctx, parent_path="", group_name="fa[1-2]_plane[3-4]", group_def=group_def + ) + + # We expect 4 groups => each with 1 node => total 4 nodes + expected_names = { + "fa1_plane3/fa1_plane3-1", + "fa1_plane4/fa1_plane4-1", + "fa2_plane3/fa2_plane3-1", + "fa2_plane4/fa2_plane4-1", + } + assert set(ctx_net.nodes.keys()) == expected_names + + +def test_apply_parameters_nested_fields(): + """ + Tests parameter overrides for nested fields beyond just 'attrs'. + E.g., 'leaf.some_field.nested_key = 999'. + """ + bp = Blueprint( + name="bp1", + groups={ + "leaf": { + "node_count": 1, + "some_field": {"nested_key": 111}, + }, + }, + adjacency=[], + ) + ctx_net = Network() + ctx = DSLExpansionContext(blueprints={"bp1": bp}, network=ctx_net) + + group_def = { + "use_blueprint": "bp1", + "parameters": { + "leaf.some_field.nested_key": 999, + }, + } + _expand_group( + ctx, + parent_path="Region", + group_name="Main", + group_def=group_def, + ) + + # Expect 1 node => "Region/Main/leaf/leaf-1" + assert len(ctx_net.nodes) == 1 + node_obj = ctx_net.nodes["Region/Main/leaf/leaf-1"] + # Check that nested field was updated + assert node_obj.attrs["some_field"]["nested_key"] == 999 + + +def test_link_overrides_repeated(): + """ + Tests applying multiple link_overrides in sequence to the same links. + Each override can set different fields, verifying that the last override + wins if there's a conflict. + """ + net = Network() + net.add_node(Node("S1")) + net.add_node(Node("T1")) + link = Link("S1", "T1", capacity=5, cost=5, attrs={}) + net.add_link(link) + + network_data = { + "link_overrides": [ + { + "source": "S1", + "target": "T1", + "link_params": { + "capacity": 100, + "attrs": {"color": "red"}, + }, + }, + { + "source": "S1", + "target": "T1", + "link_params": { + "cost": 999, + "attrs": {"color": "blue"}, # Overwrites 'color': 'red' + }, + }, + ] + } + _process_link_overrides(net, network_data) + assert link.capacity == 100 + assert link.cost == 999 + assert link.attrs["color"] == "blue" # second override wins + + +def test_node_overrides_repeated(): + """ + Tests multiple node_overrides in sequence, verifying that the last override + wins on conflicting attributes and merges others. + """ + net = Network() + net.add_node(Node("X", attrs={"existing": "keep"})) + + network_data = { + "node_overrides": [ + { + "path": "X", + "attrs": { + "role": "old_role", + "color": "red", + }, + }, + { + "path": "X", + "attrs": { + "role": "updated_role", + "speed": "fast", + }, + }, + ] + } + _process_node_overrides(net, network_data) + node_attrs = net.nodes["X"].attrs + + # existing attribute is retained + assert node_attrs["existing"] == "keep" + # role was overwritten by second override + assert node_attrs["role"] == "updated_role" + # color is from the first override but not overwritten by the second => remains + assert node_attrs["color"] == "red" + # speed is added by the second override + assert node_attrs["speed"] == "fast" + + +def test_adjacency_no_matching_source_or_target(): + """ + Tests adjacency expansion where the source or target path does not match any nodes. + Should simply skip and create no links. + """ + scenario_data = { + "network": { + "nodes": {"RealNode": {}}, + "adjacency": [ + { + "source": "/MissingSource", + "target": "/RealNode", + "pattern": "mesh", + }, + { + "source": "/RealNode", + "target": "/MissingTarget", + "pattern": "mesh", + }, + ], + } + } + net = expand_network_dsl(scenario_data) + # Only 1 node => RealNode + assert len(net.nodes) == 1 + # No valid adjacency => no links + assert len(net.links) == 0 + + +def test_provide_network_version_and_name(): + """ + Tests that both 'name' and 'version' keys in the DSL are captured as net.attrs. + """ + data = { + "network": { + "name": "NetGraphTest", + "version": "2.0-beta", + } + } + net = expand_network_dsl(data) + assert net.attrs["name"] == "NetGraphTest" + assert net.attrs["version"] == "2.0-beta" + + +def test_expand_group_with_bracket_expansions(): + """ + Tests bracket expansions in group names, e.g. group_name='fa[1-2]_plane[3-4]'. + We expect 4 expansions => fa1_plane3, fa1_plane4, fa2_plane3, fa2_plane4. + """ + ctx_net = Network() + ctx = DSLExpansionContext({}, ctx_net) + + group_def = {"node_count": 1} # We'll create 1 node per expanded group + _expand_group( + ctx, parent_path="", group_name="fa[1-2]_plane[3-4]", group_def=group_def + ) + + # We expect 4 groups => each with 1 node => total 4 nodes + expected_names = { + "fa1_plane3/fa1_plane3-1", + "fa1_plane4/fa1_plane4-1", + "fa2_plane3/fa2_plane3-1", + "fa2_plane4/fa2_plane4-1", + } + assert set(ctx_net.nodes.keys()) == expected_names + + +def test_apply_parameters_nested_fields(): + """ + Tests parameter overrides for nested fields beyond just 'attrs'. + E.g., 'leaf.some_field.nested_key = 999'. + """ + bp = Blueprint( + name="bp1", + groups={ + "leaf": { + "node_count": 1, + "some_field": {"nested_key": 111}, + }, + }, + adjacency=[], + ) + ctx_net = Network() + ctx = DSLExpansionContext(blueprints={"bp1": bp}, network=ctx_net) + + group_def = { + "use_blueprint": "bp1", + "parameters": { + "leaf.some_field.nested_key": 999, + }, + } + _expand_group( + ctx, + parent_path="Region", + group_name="Main", + group_def=group_def, + ) + + # Expect 1 node => "Region/Main/leaf/leaf-1" + assert len(ctx_net.nodes) == 1 + node_obj = ctx_net.nodes["Region/Main/leaf/leaf-1"] + # Check that nested field was updated + assert node_obj.attrs["some_field"]["nested_key"] == 999 + + +def test_link_overrides_repeated(): + """ + Tests applying multiple link_overrides in sequence to the same links. + Each override can set different fields, verifying that the last override + wins if there's a conflict. + """ + net = Network() + net.add_node(Node("S1")) + net.add_node(Node("T1")) + link = Link("S1", "T1", capacity=5, cost=5, attrs={}) + net.add_link(link) + + network_data = { + "link_overrides": [ + { + "source": "S1", + "target": "T1", + "link_params": { + "capacity": 100, + "attrs": {"color": "red"}, + }, + }, + { + "source": "S1", + "target": "T1", + "link_params": { + "cost": 999, + "attrs": {"color": "blue"}, # Overwrites 'color': 'red' + }, + }, + ] + } + _process_link_overrides(net, network_data) + assert link.capacity == 100 + assert link.cost == 999 + assert link.attrs["color"] == "blue" # second override wins + + +def test_node_overrides_repeated(): + """ + Tests multiple node_overrides in sequence, verifying that the last override + wins on conflicting attributes and merges others. + """ + net = Network() + net.add_node(Node("X", attrs={"existing": "keep"})) + + network_data = { + "node_overrides": [ + { + "path": "X", + "attrs": { + "role": "old_role", + "color": "red", + }, + }, + { + "path": "X", + "attrs": { + "role": "updated_role", + "speed": "fast", + }, + }, + ] + } + _process_node_overrides(net, network_data) + node_attrs = net.nodes["X"].attrs + + # existing attribute is retained + assert node_attrs["existing"] == "keep" + # role was overwritten by second override + assert node_attrs["role"] == "updated_role" + # color is from the first override but not overwritten by the second => remains + assert node_attrs["color"] == "red" + # speed is added by the second override + assert node_attrs["speed"] == "fast" + + +def test_adjacency_no_matching_source_or_target(): + """ + Tests adjacency expansion where the source or target path does not match any nodes. + Should simply skip and create no links. + """ + scenario_data = { + "network": { + "nodes": {"RealNode": {}}, + "adjacency": [ + { + "source": "/MissingSource", + "target": "/RealNode", + "pattern": "mesh", + }, + { + "source": "/RealNode", + "target": "/MissingTarget", + "pattern": "mesh", + }, + ], + } + } + net = expand_network_dsl(scenario_data) + # Only 1 node => RealNode + assert len(net.nodes) == 1 + # No valid adjacency => no links + assert len(net.links) == 0 + + +def test_provide_network_version_and_name(): + """ + Tests that both 'name' and 'version' keys in the DSL are captured as net.attrs. + """ + data = { + "network": { + "name": "NetGraphTest", + "version": "2.0-beta", + } + } + net = expand_network_dsl(data) + assert net.attrs["name"] == "NetGraphTest" + assert net.attrs["version"] == "2.0-beta" + + +def test_expand_adjacency_with_variables_zip(): + """ + Tests adjacency expansion with 'expand_vars' in 'zip' mode. + Verifies that expansions occur in lockstep for equal-length lists. + """ + scenario_data = { + "network": { + "groups": { + "RackA": {"node_count": 1, "name_template": "RackA-{node_num}"}, + "RackB": {"node_count": 1, "name_template": "RackB-{node_num}"}, + "RackC": {"node_count": 1, "name_template": "RackC-{node_num}"}, + }, + "adjacency": [ + { + "source": "/Rack{rack_id}", + "target": "/Rack{other_rack_id}", + "expand_vars": { + "rack_id": ["A", "B", "C"], + "other_rack_id": ["B", "C", "A"], + }, + "expansion_mode": "zip", + "pattern": "mesh", + "link_params": {"capacity": 100}, + } + ], + } + } + net = expand_network_dsl(scenario_data) + + # We have 3 racks: RackA-1, RackB-1, RackC-1 + # expand_vars with zip => (A->B), (B->C), (C->A) for the source/target + # pattern=mesh => each source node to each target node => but each "group" here has only 1 node + # => we get exactly 3 unique links + assert len(net.nodes) == 3 + assert len(net.links) == 3 + + link_pairs = {(l.source, l.target) for l in net.links.values()} + # RackA-1 -> RackB-1 + # RackB-1 -> RackC-1 + # RackC-1 -> RackA-1 + # (no duplication, no self-loops) + expected = { + ("RackA/RackA-1", "RackB/RackB-1"), + ("RackB/RackB-1", "RackC/RackC-1"), + ("RackC/RackC-1", "RackA/RackA-1"), + } + assert link_pairs == expected + for link in net.links.values(): + assert link.capacity == 100 + + +def test_expand_adjacency_with_variables_zip_mismatch(): + """ + Tests that 'zip' mode with lists of different lengths raises ValueError. + """ + scenario_data = { + "network": { + "groups": { + "RackA": {"node_count": 1}, + "RackB": {"node_count": 1}, + "RackC": {"node_count": 1}, + }, + "adjacency": [ + { + "source": "/Rack{rack_id}", + "target": "/Rack{other_rack_id}", + "expand_vars": { + "rack_id": ["A", "B"], + "other_rack_id": ["C", "A", "B"], # mismatch length + }, + "expansion_mode": "zip", + } + ], + } + } + + with pytest.raises(ValueError) as exc: + expand_network_dsl(scenario_data) + assert "zip expansion requires all lists be the same length" in str(exc.value)