Skip to content

Commit cefce7d

Browse files
authored
Network explorer (#62)
* introducing network explorer and components lib
1 parent 25723f2 commit cefce7d

14 files changed

Lines changed: 5807 additions & 1103 deletions

ngraph/blueprints.py

Lines changed: 93 additions & 131 deletions
Large diffs are not rendered by default.

ngraph/components.py

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
from __future__ import annotations
2+
3+
import yaml
4+
from copy import deepcopy
5+
from dataclasses import dataclass, field
6+
from typing import Dict, Any, Optional
7+
8+
9+
@dataclass(slots=True)
10+
class Component:
11+
"""
12+
A generic component that can represent chassis, line cards, optics, etc.
13+
Components can have nested children, each with their own cost, power, etc.
14+
15+
Attributes:
16+
name (str): Name of the component (e.g., "SpineChassis" or "400G-LR4").
17+
component_type (str): A string label (e.g., "chassis", "linecard", "optic").
18+
description (str): A human-readable description of this component.
19+
cost (float): Cost (capex) of a single instance of this component.
20+
power_watts (float): Typical/nominal power usage (watts) for one instance.
21+
power_watts_max (float): Maximum/peak power usage (watts) for one instance.
22+
capacity (float): A generic capacity measure (e.g., platform capacity).
23+
ports (int): Number of ports if relevant for this component.
24+
count (int): How many identical copies of this component are present.
25+
attrs (Dict[str, Any]): Arbitrary key-value attributes for extra metadata.
26+
children (Dict[str, Component]): Nested child components (e.g., line cards
27+
inside a chassis), keyed by child name.
28+
"""
29+
30+
name: str
31+
component_type: str = "generic"
32+
description: str = ""
33+
cost: float = 0.0
34+
35+
power_watts: float = 0.0 # Typical power usage
36+
power_watts_max: float = 0.0 # Peak power usage
37+
38+
capacity: float = 0.0
39+
ports: int = 0
40+
count: int = 1
41+
42+
attrs: Dict[str, Any] = field(default_factory=dict)
43+
children: Dict[str, Component] = field(default_factory=dict)
44+
45+
def total_cost(self) -> float:
46+
"""
47+
Computes the total (recursive) cost of this component, including children,
48+
multiplied by this component's count.
49+
50+
Returns:
51+
float: The total cost.
52+
"""
53+
single_instance_cost = self.cost
54+
for child in self.children.values():
55+
single_instance_cost += child.total_cost()
56+
return single_instance_cost * self.count
57+
58+
def total_power(self) -> float:
59+
"""
60+
Computes the total *typical* (recursive) power usage of this component,
61+
including children, multiplied by this component's count.
62+
63+
Returns:
64+
float: The total typical power in watts.
65+
"""
66+
single_instance_power = self.power_watts
67+
for child in self.children.values():
68+
single_instance_power += child.total_power()
69+
return single_instance_power * self.count
70+
71+
def total_power_max(self) -> float:
72+
"""
73+
Computes the total *peak* (recursive) power usage of this component,
74+
including children, multiplied by this component's count.
75+
76+
Returns:
77+
float: The total maximum (peak) power in watts.
78+
"""
79+
single_instance_power_max = self.power_watts_max
80+
for child in self.children.values():
81+
single_instance_power_max += child.total_power_max()
82+
return single_instance_power_max * self.count
83+
84+
def total_capacity(self) -> float:
85+
"""
86+
Computes the total (recursive) capacity of this component,
87+
including children, multiplied by this component's count.
88+
89+
Returns:
90+
float: The total capacity (dimensionless or user-defined units).
91+
"""
92+
single_instance_capacity = self.capacity
93+
for child in self.children.values():
94+
single_instance_capacity += child.total_capacity()
95+
return single_instance_capacity * self.count
96+
97+
def as_dict(self, include_children: bool = True) -> Dict[str, Any]:
98+
"""
99+
Returns a dictionary containing all properties of this component.
100+
101+
Args:
102+
include_children (bool): If True, recursively includes children.
103+
104+
Returns:
105+
Dict[str, Any]: Dictionary representation of this component.
106+
"""
107+
data = {
108+
"name": self.name,
109+
"component_type": self.component_type,
110+
"description": self.description,
111+
"cost": self.cost,
112+
"power_watts": self.power_watts,
113+
"power_watts_max": self.power_watts_max,
114+
"capacity": self.capacity,
115+
"ports": self.ports,
116+
"count": self.count,
117+
"attrs": dict(self.attrs), # shallow copy
118+
}
119+
if include_children:
120+
data["children"] = {
121+
child_name: child.as_dict(True)
122+
for child_name, child in self.children.items()
123+
}
124+
return data
125+
126+
127+
@dataclass(slots=True)
128+
class ComponentsLibrary:
129+
"""
130+
Holds a collection of named Components. Each entry is a top-level "template"
131+
that can be referenced for cost/power/capacity lookups, possibly with nested children.
132+
133+
Example (YAML-like):
134+
components:
135+
BigSwitch:
136+
component_type: chassis
137+
cost: 20000
138+
power_watts: 1750
139+
capacity: 25600
140+
children:
141+
PIM16Q-16x200G:
142+
component_type: linecard
143+
cost: 1000
144+
power_watts: 10
145+
ports: 16
146+
count: 8
147+
200G-FR4:
148+
component_type: optic
149+
cost: 2000
150+
power_watts: 6
151+
power_watts_max: 6.5
152+
"""
153+
154+
components: Dict[str, Component] = field(default_factory=dict)
155+
156+
def get(self, name: str) -> Optional[Component]:
157+
"""
158+
Retrieves a Component by its name from the library.
159+
160+
Args:
161+
name (str): Name of the component.
162+
163+
Returns:
164+
Optional[Component]: The requested Component or None if not found.
165+
"""
166+
return self.components.get(name)
167+
168+
def merge(
169+
self, other: ComponentsLibrary, override: bool = True
170+
) -> ComponentsLibrary:
171+
"""
172+
Merges another ComponentsLibrary into this one. By default (override=True),
173+
duplicate components in `other` overwrite those in the current library.
174+
175+
Args:
176+
other (ComponentsLibrary): Another library to merge into this one.
177+
override (bool): If True, components in `other` override existing ones.
178+
179+
Returns:
180+
ComponentsLibrary: This instance, updated in place.
181+
"""
182+
for comp_name, comp_obj in other.components.items():
183+
if override or comp_name not in self.components:
184+
self.components[comp_name] = comp_obj
185+
return self
186+
187+
def clone(self) -> ComponentsLibrary:
188+
"""
189+
Creates a deep copy of this ComponentsLibrary.
190+
191+
Returns:
192+
ComponentsLibrary: A new, cloned library instance.
193+
"""
194+
return ComponentsLibrary(components=deepcopy(self.components))
195+
196+
@classmethod
197+
def from_dict(cls, data: Dict[str, Any]) -> ComponentsLibrary:
198+
"""
199+
Constructs a ComponentsLibrary from a dictionary of raw component definitions.
200+
201+
Args:
202+
data (Dict[str, Any]): Raw component definitions.
203+
204+
Returns:
205+
ComponentsLibrary: A newly constructed library.
206+
"""
207+
components_map: Dict[str, Component] = {}
208+
for comp_name, comp_def in data.items():
209+
components_map[comp_name] = cls._build_component(comp_name, comp_def)
210+
return cls(components=components_map)
211+
212+
@classmethod
213+
def _build_component(cls, name: str, definition_data: Dict[str, Any]) -> Component:
214+
"""
215+
Recursively constructs a single Component from a dictionary definition.
216+
217+
Args:
218+
name (str): Name of the component.
219+
definition_data (Dict[str, Any]): Dictionary data for the component definition.
220+
221+
Returns:
222+
Component: The constructed Component instance.
223+
"""
224+
comp_type = definition_data.get("component_type", "generic")
225+
cost = float(definition_data.get("cost", 0.0))
226+
power = float(definition_data.get("power_watts", 0.0))
227+
power_max = float(definition_data.get("power_watts_max", 0.0))
228+
capacity = float(definition_data.get("capacity", 0.0))
229+
ports = int(definition_data.get("ports", 0))
230+
count = int(definition_data.get("count", 1))
231+
232+
child_definitions = definition_data.get("children", {})
233+
children_map: Dict[str, Component] = {}
234+
for child_name, child_data in child_definitions.items():
235+
children_map[child_name] = cls._build_component(child_name, child_data)
236+
237+
recognized_keys = {
238+
"component_type",
239+
"cost",
240+
"power_watts",
241+
"power_watts_max",
242+
"capacity",
243+
"ports",
244+
"children",
245+
"attrs",
246+
"count",
247+
"description",
248+
}
249+
attrs: Dict[str, Any] = dict(definition_data.get("attrs", {}))
250+
leftover_keys = {
251+
k: v for k, v in definition_data.items() if k not in recognized_keys
252+
}
253+
attrs.update(leftover_keys)
254+
255+
return Component(
256+
name=name,
257+
component_type=comp_type,
258+
description=definition_data.get("description", ""),
259+
cost=cost,
260+
power_watts=power,
261+
power_watts_max=power_max,
262+
capacity=capacity,
263+
ports=ports,
264+
count=count,
265+
attrs=attrs,
266+
children=children_map,
267+
)
268+
269+
@classmethod
270+
def from_yaml(cls, yaml_str: str) -> ComponentsLibrary:
271+
"""
272+
Constructs a ComponentsLibrary from a YAML string. If the YAML contains
273+
a top-level 'components' key, that key is used; otherwise the entire
274+
top-level is treated as component definitions.
275+
276+
Args:
277+
yaml_str (str): A YAML-formatted string of component definitions.
278+
279+
Returns:
280+
ComponentsLibrary: A newly built components library.
281+
282+
Raises:
283+
ValueError: If the top-level is not a dictionary or if the 'components'
284+
key is present but not a dictionary.
285+
"""
286+
data = yaml.safe_load(yaml_str)
287+
if not isinstance(data, dict):
288+
raise ValueError("Top-level must be a dict in Components YAML.")
289+
290+
components_data = data.get("components") or data
291+
if not isinstance(components_data, dict):
292+
raise ValueError("'components' must be a dict if present.")
293+
294+
return cls.from_dict(components_data)

0 commit comments

Comments
 (0)