@@ -55,12 +55,9 @@ def __init__(self, graph_module: torch.fx.GraphModule):
5555 _NODE_Y_PADDING = 20
5656 _LAYOUT_XSPACE = 50
5757 _LAYOUT_YSPACE = 30
58- _DUMMY_SIZE = 30 # dummy nodes (from fast-sugiyama) occupy no real width/height
59- _CHAIN_MIN_LENGTH = 2
60- _SPINE_COHESION_ALPHA = 0.7
61- _SPINE_COHESION_ITER = 3
62- _SPINE_CONTINUITY_EPSILON = 0.3
63- _EDGE_CLIP_MARGIN = 2.0
58+ _DUMMY_SIZE_X = 100 # dummy nodes (from fast-sugiyama) occupy no real width/height
59+ _DUMMY_SIZE_Y = 30 # dummy nodes (from fast-sugiyama) occupy no real width/height
60+ _SPINE_COHESION_ITER = 20
6461
6562 def _default_base_label (self , node : GraphNode ) -> str :
6663 target = str (node .info .get ("target" ) or node .info .get ("op" ) or "" )
@@ -326,7 +323,7 @@ def _compute_layout_with_ext_lines(
326323 from fast_sugiyama .layout import Layouts
327324 pack_spacing = max_w + cls ._LAYOUT_XSPACE
328325 widest_component = max ((w for _pos , w , _h , _e in adjusted ), default = 0.0 )
329- pack_width = int (max (widest_component + pack_spacing , 2000.0 ))
326+ pack_width = int (max (widest_component * 3 + pack_spacing , 2000.0 ))
330327 layouts = Layouts (adjusted ).rect_pack_layouts (
331328 max_width = pack_width ,
332329 spacing = pack_spacing ,
@@ -362,11 +359,11 @@ def _compact_components(cls, layouts, nodes, expected_gap):
362359
363360 def _w (nid ):
364361 n = nodes .get (nid )
365- return n .width if n is not None else cls ._DUMMY_SIZE
362+ return n .width if n is not None else cls ._DUMMY_SIZE_X
366363
367364 def _h (nid ):
368365 n = nodes .get (nid )
369- return n .height if n is not None else cls ._DUMMY_SIZE
366+ return n .height if n is not None else cls ._DUMMY_SIZE_Y
370367
371368 xspace = cls ._LAYOUT_XSPACE
372369 yspace = cls ._LAYOUT_YSPACE
@@ -390,16 +387,31 @@ def _sweep_min_gap(nids):
390387 x [cur ] = x [prev ] + min_gap
391388
392389 # Phase 1: chain detection (real + dummy members)
393- chains = cls ._detect_chains (el or [])
390+ chains = cls ._detect_chains (el or [], nodes )
394391
395392 # Phase 2: iterative spine cohesion + pure-A overlap fix
396- for _ in range (cls ._SPINE_COHESION_ITER ):
393+ for i in range (cls ._SPINE_COHESION_ITER + 2 ):
394+
395+ # delta for each node
396+ # We record the relative weight (chain length) and their base delta
397+ node_delta : dict = defaultdict (list )
397398 for ch in chains :
398399 if not ch :
399400 continue
400- target = sum (x [v ] for v in ch ) / len (ch )
401+ mean_x = sum (x [v ] for v in ch ) / len (ch )
402+ # emphasize end point
403+ if i < cls ._SPINE_COHESION_ITER :
404+ # we use common start and end node of chain to attract chain close together
405+ mean_x = (mean_x + x [ch [0 ]] + x [ch [- 1 ]]) / 3.0
401406 for v in ch :
402- x [v ] += cls ._SPINE_COHESION_ALPHA * (target - x [v ])
407+ # weight: len(ch)
408+ # delta: mean_x - x[v]
409+ node_delta [v ].append ((len (ch ), (mean_x - x [v ])))
410+ # move the node x
411+ for n , deltas in node_delta .items ():
412+ total_weight = sum (w for w , _ in deltas )
413+ x [n ] += sum (w / total_weight * d for w , d in deltas )
414+ # adjust node overlapping
403415 for nids in by_y .values ():
404416 _sweep_min_gap (nids )
405417
@@ -434,51 +446,74 @@ def _sweep_min_gap(nids):
434446 return new_layouts
435447
436448 @classmethod
437- def _detect_chains (cls , edge_list ):
449+ def _detect_chains (cls , edge_list , nodes ):
450+ # Here we break the graph into chains (connected node list)
451+ # The longer the chain the better (aligned visual vertical axis)
452+ # We achieve long chain by calculating best_prev and best_succ with rank
453+ # We must let the chain start and end node to be shared
454+ # The shared end points (common nodes) will be used to pull chains near in later iterative loop
438455 from collections import defaultdict
439456
440457 if not edge_list :
441458 return []
442459
443- in_deg : dict = defaultdict (int )
444- out_deg : dict = defaultdict (int )
445- succ : dict = {}
460+ succ : dict = defaultdict (set )
461+ prev : dict = defaultdict (set )
446462 for u , v in edge_list :
447- out_deg [u ] += 1
448- in_deg [v ] += 1
449- # Record unique successor only when out_deg == 1 so we avoid
450- # ambiguity; overwritten on branching but we'll reject branching
451- # nodes from chain interiors via the in==1 and out==1 test.
452- succ [u ] = v
463+ succ [u ].add (v )
464+ prev [v ].add (u )
465+
466+ all_nodes = set (succ ) | set (prev )
467+
468+ # max depth a node's output can reach
469+ node_out_rank : dict = defaultdict (int )
470+ # the succer node that have maximal node_out_rank
471+ best_succ : dict = {}
472+ graph_output_nodes = [n for n in all_nodes if len (succ [n ]) == 0 ]
473+ stack = graph_output_nodes
474+ while stack :
475+ n = stack .pop ()
476+ for pn in prev [n ]:
477+ score = 2 if pn in nodes else 1
478+ if node_out_rank [pn ] < node_out_rank [n ] + score :
479+ node_out_rank [pn ] = node_out_rank [n ] + score
480+ stack .append (pn )
481+ best_succ [pn ] = n
482+
483+ # max depth a node's input can reach
484+ node_in_rank : dict = defaultdict (int )
485+ # the prev node that have maximal node_in_rank
486+ best_prev : dict = {}
487+ graph_input_nodes = [n for n in all_nodes if len (prev [n ]) == 0 ]
488+ stack = graph_input_nodes
489+ while stack :
490+ n = stack .pop ()
491+ for nn in succ [n ]:
492+ score = 2 if nn in nodes else 1
493+ if node_in_rank [nn ] < node_in_rank [n ] + score :
494+ node_in_rank [nn ] = node_in_rank [n ] + score
495+ stack .append (nn )
496+ best_prev [nn ] = n
453497
454- all_nodes = set (in_deg ) | set (out_deg )
455-
456- def is_interior (n ):
457- return out_deg .get (n , 0 ) == 1
458498
459499 visited : set = set ()
460500 chains : list = []
461- for start in all_nodes :
462- if is_interior ( start ) :
501+ for start in sorted ( all_nodes , key = lambda n : node_out_rank [ n ], reverse = True ) :
502+ if start in visited :
463503 continue
464- # Walk from each successor of `start` forward while interior
465- # Collect successors from the edge list since `succ` only stores
466- # one (fine for branch starts, but may miss other branches)
467- start_succs = [v for (u , v ) in edge_list if u == start ]
468- for w in start_succs :
469- if w in visited :
470- continue
471- walk = [start , w ]
472- cur = w
473- while is_interior (cur ) and succ .get (cur ) is not None :
474- nxt = succ [cur ]
475- if nxt in walk :
476- break
477- walk .append (nxt )
478- visited .add (cur )
479- cur = nxt
480- if len (walk ) >= cls ._CHAIN_MIN_LENGTH :
481- chains .append (walk )
504+ cur = start
505+ walk = [start ]
506+ if start in best_prev :
507+ walk .insert (0 , best_prev [start ])
508+ while cur not in visited :
509+ visited .add (cur )
510+ if cur not in best_succ :
511+ break
512+ nxt = best_succ [cur ]
513+ walk .append (nxt )
514+ cur = nxt
515+ if len (walk ) >= 2 : # always true
516+ chains .append (walk )
482517 return chains
483518
484519 @staticmethod
0 commit comments