1+ from copy import deepcopy
12import math
23from typing import (
34 Any ,
1920
2021from accelforge .util .exceptions import EvaluationError
2122
23+ from accelforge .frontend .arch .spatialable import Spatialable
24+
2225from pydantic import Discriminator
2326from accelforge .util ._basetypes import _uninstantiable
2427from accelforge .util .parallel import _SVGJupyterRender
@@ -68,6 +71,29 @@ def find(self, name: str, default: Any = _FIND_SENTINEL) -> Union["Leaf", Any]:
6871 return default
6972 raise ValueError (f"Leaf { name } not found in { self } " )
7073
74+ def iterate_hierarchically (self , _parents = None ):
75+ """
76+ Iterates over all Leaf nodes while also yielding the list of all Leaf
77+ nodes that are hierarchical parents over the current node.
78+ """
79+ if _parents is None :
80+ _parents = []
81+
82+ if isinstance (self , Leaf ):
83+ yield self , _parents
84+ _parents .append (self )
85+ return
86+
87+ assert isinstance (self , Branch )
88+ if isinstance (self , (Fork , Array )):
89+ for node in self .nodes :
90+ yield from node .iterate_hierarchically (list (_parents ))
91+ elif isinstance (self , Hierarchical ):
92+ for node in self .nodes :
93+ yield from node .iterate_hierarchically (_parents )
94+ else :
95+ raise RuntimeError ("unhandled structure type" )
96+
7197 def _render_node_name (self ) -> str :
7298 """The name for a Pydot node."""
7399 return f"{ self .__class__ .__name__ } _{ id (self )} "
@@ -137,12 +163,6 @@ def __str__(self) -> str:
137163 result = f"{ result } [{ spatial_str } ]"
138164 return result
139165
140- def _spatial_str (self , include_newline = True ) -> str :
141- if not self .spatial :
142- return ""
143- result = ", " .join (f"{ s .fanout } × { s .name } " for s in self .spatial )
144- return f"\n [{ result } ]" if include_newline else result
145-
146166 def _render_node_label (self ) -> str :
147167 return f"{ self .name } " + self ._spatial_str ()
148168
@@ -162,7 +182,9 @@ class Branch(ArchNode):
162182 Annotated ["Memory" , Tag ("Memory" )],
163183 Annotated ["Toll" , Tag ("Toll" )],
164184 Annotated ["Container" , Tag ("Container" )],
185+ Annotated ["Network" , Tag ("Network" )],
165186 Annotated ["Hierarchical" , Tag ("Hierarchical" )],
187+ Annotated ["Array" , Tag ("Array" )],
166188 Annotated ["Fork" , Tag ("Fork" )],
167189 ],
168190 Discriminator (_get_tag ),
@@ -235,6 +257,94 @@ def _power_gating(
235257 return result , non_power_gated_porp
236258
237259
260+ class Array (Branch , Spatialable ):
261+ def model_post_init (self , __context__ = None ) -> None :
262+ for node in self .nodes :
263+ if isinstance (node , Fork ):
264+ raise EvaluationError ("cannot have fork inside array" )
265+
266+ def _flatten (
267+ self ,
268+ compute_node : str ,
269+ fanout : int = 1 ,
270+ return_fanout : bool = False ,
271+ ):
272+ from accelforge .frontend .arch .components import Compute
273+ nodes = []
274+
275+ for node in self .nodes :
276+ try :
277+ if isinstance (node , Branch ):
278+ raise RuntimeError ("do not put branches inside array" )
279+ elif isinstance (node , Leaf ):
280+ fanout *= node .get_fanout ()
281+ node = deepcopy (node )
282+ node .spatial = EvalableList ()
283+ nodes .append (node )
284+ else :
285+ raise RuntimeError (f"unhandled structure type { node } " )
286+ except EvaluationError as e :
287+ e .add_field (node )
288+ raise e
289+
290+ if return_fanout :
291+ return nodes , fanout
292+ return nodes
293+
294+ def _render_node_label (self ) -> str :
295+ return f"Array { self ._spatial_str ()} "
296+
297+ def _render_node_color (self ) -> str :
298+ return "#FCC2FC"
299+
300+ def _parent2child_names (
301+ self , parent_name : str = None
302+ ) -> tuple [list [tuple [str , str , str ]], str ]:
303+ from accelforge .frontend .arch .components import Compute
304+
305+ edges = []
306+ current_parent_name = parent_name
307+
308+ for node in self .nodes :
309+ if isinstance (node , Branch ):
310+ raise EvaluationError ("do not put branch in array" )
311+ elif isinstance (node , Compute ):
312+ # Compute nodes branch off to the side like a Fork
313+ if current_parent_name is not None :
314+ edges .append ((current_parent_name , node ._render_node_name (), "dashed" ))
315+ else :
316+ if current_parent_name is not None :
317+ edges .append ((current_parent_name , node ._render_node_name (), "dashed" ))
318+ return edges , self ._render_node_name ()
319+
320+ def _render_make_children (self ) -> list [pydot .Node ]:
321+ """Renders only children, not the Hierarchical node itself."""
322+ result = [self ._render_node ()]
323+ for node in self .nodes :
324+ children = node ._render_make_children ()
325+ result .extend (child for child in children if child is not None )
326+ return result
327+
328+ def render (self ) -> str :
329+ """Renders the architecture as a Pydot graph."""
330+ graph = _pydot_graph ()
331+
332+ # Render all nodes (Hierarchical nodes return None and are filtered out)
333+ for node in self ._render_make_children ():
334+ if node is not None :
335+ graph .add_node (node )
336+
337+ # Add edges
338+ edges , _ = self ._parent2child_names ()
339+ for parent_name , child_name in edges :
340+ graph .add_edge (pydot .Edge (parent_name , child_name ))
341+
342+ return _SVGJupyterRender (graph .create_svg (prog = "dot" ).decode ("utf-8" ))
343+
344+ def _repr_svg_ (self ) -> str :
345+ return self .render ()
346+
347+
238348class Hierarchical (Branch ):
239349 def _flatten (
240350 self ,
@@ -265,6 +375,18 @@ def _flatten(
265375 ):
266376 break
267377 assert not isinstance (node , Fork )
378+ elif isinstance (node , Array ):
379+ new_nodes = node ._flatten (
380+ compute_node , fanout , return_fanout = False
381+ )
382+ nodes .extend (new_nodes )
383+ nodes .append (node )
384+ fanout *= node .get_fanout ()
385+ if any (
386+ isinstance (n , Compute ) and n .name == compute_node
387+ for n in new_nodes
388+ ):
389+ break
268390 elif isinstance (node , Compute ):
269391 if node .name == compute_node :
270392 fanout *= node .get_fanout ()
@@ -299,23 +421,27 @@ def _parent2child_names(
299421 edges .extend (child_edges )
300422 if last_child_name is not None :
301423 current_parent_name = last_child_name
424+ elif isinstance (node , Array ):
425+ child_edges , last_child_name = node ._parent2child_names (
426+ node ._render_node_name ()
427+ )
428+ edges .extend (child_edges )
429+ edges .append ((current_parent_name , last_child_name , "solid" ))
430+ if last_child_name is not None :
431+ current_parent_name = last_child_name
302432 elif isinstance (node , Compute ):
303433 # Compute nodes branch off to the side like a Fork
304434 if current_parent_name is not None :
305- edges .append ((current_parent_name , node ._render_node_name ()))
435+ edges .append ((current_parent_name , node ._render_node_name (), "solid" ))
306436 else :
307437 if current_parent_name is not None :
308- edges .append ((current_parent_name , node ._render_node_name ()))
438+ edges .append ((current_parent_name , node ._render_node_name (), "solid" ))
309439
310440 # Update parent for next iteration
311441 current_parent_name = node ._render_node_name ()
312442
313443 return edges , current_parent_name
314444
315- def _render_node (self ) -> pydot .Node :
316- """Hierarchical nodes should not be rendered."""
317- return None
318-
319445 def _render_make_children (self ) -> list [pydot .Node ]:
320446 """Renders only children, not the Hierarchical node itself."""
321447 result = []
@@ -335,8 +461,8 @@ def render(self) -> str:
335461
336462 # Add edges
337463 edges , _ = self ._parent2child_names ()
338- for parent_name , child_name in edges :
339- graph .add_edge (pydot .Edge (parent_name , child_name ))
464+ for parent_name , child_name , style in edges :
465+ graph .add_edge (pydot .Edge (parent_name , child_name , style = style ))
340466
341467 return _SVGJupyterRender (graph .create_svg (prog = "dot" ).decode ("utf-8" ))
342468
0 commit comments