archgraph-layout 1.0.0__tar.gz → 2.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. {archgraph_layout-1.0.0/archgraph_layout.egg-info → archgraph_layout-2.0.0}/PKG-INFO +21 -6
  2. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/README.md +20 -5
  3. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/archgraph/__init__.py +4 -2
  4. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/archgraph/cli.py +17 -2
  5. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/archgraph/model.py +1 -10
  6. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/archgraph/parser.py +23 -27
  7. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/archgraph/placement/engine.py +23 -7
  8. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/archgraph/placement/floorplan.py +40 -19
  9. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/archgraph/placement/hierarchy.py +37 -11
  10. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/archgraph/placement/ports.py +370 -354
  11. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/archgraph/placement/sizing.py +78 -83
  12. archgraph_layout-2.0.0/archgraph/portlabels.py +73 -0
  13. archgraph_layout-2.0.0/archgraph/progress.py +143 -0
  14. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/archgraph/render/html.py +3 -6
  15. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/archgraph/render/jsonout.py +4 -4
  16. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/archgraph/render/shapes.py +129 -240
  17. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/archgraph/render/svg.py +56 -103
  18. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/archgraph/routing/__init__.py +24 -24
  19. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/archgraph/routing/grid.py +2 -6
  20. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/archgraph/routing/router.py +1127 -1112
  21. archgraph_layout-2.0.0/archgraph/shapegeometry.py +261 -0
  22. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/archgraph/styles.py +6 -10
  23. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0/archgraph_layout.egg-info}/PKG-INFO +21 -6
  24. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/archgraph_layout.egg-info/SOURCES.txt +6 -1
  25. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/docs/xml-reference.md +26 -16
  26. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/examples/automotive.xml +1 -2
  27. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/examples/basic.xml +3 -3
  28. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/examples/datacenter.xml +1 -1
  29. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/examples/fpga_pipeline.xml +1 -1
  30. archgraph_layout-2.0.0/examples/halcyon7.xml +660 -0
  31. archgraph_layout-2.0.0/examples/halcyon7_lib.xml +233 -0
  32. archgraph_layout-2.0.0/examples/halcyon7_styles.xml +71 -0
  33. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/examples/soc.xml +6 -6
  34. archgraph_layout-1.0.0/archgraph/routing/labels.py +0 -141
  35. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/LICENSE +0 -0
  36. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/MANIFEST.in +0 -0
  37. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/archgraph/__main__.py +0 -0
  38. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/archgraph/geometry.py +0 -0
  39. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/archgraph/placement/__init__.py +0 -0
  40. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/archgraph/placement/congestion.py +0 -0
  41. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/archgraph/placement/detail.py +0 -0
  42. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/archgraph/placement/graphalgo.py +0 -0
  43. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/archgraph/placement/layered.py +0 -0
  44. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/archgraph/placement/vpsc.py +0 -0
  45. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/archgraph/quality.py +0 -0
  46. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/archgraph/render/__init__.py +0 -0
  47. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/archgraph/render/png.py +0 -0
  48. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/archgraph/routing/nudge.py +0 -0
  49. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/archgraph/textmetrics.py +0 -0
  50. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/archgraph_layout.egg-info/dependency_links.txt +0 -0
  51. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/archgraph_layout.egg-info/entry_points.txt +0 -0
  52. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/archgraph_layout.egg-info/requires.txt +0 -0
  53. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/archgraph_layout.egg-info/top_level.txt +0 -0
  54. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/examples/accelerator_mesh.xml +0 -0
  55. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/examples/flat_network.xml +0 -0
  56. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/pyproject.toml +0 -0
  57. {archgraph_layout-1.0.0 → archgraph_layout-2.0.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: archgraph-layout
3
- Version: 1.0.0
3
+ Version: 2.0.0
4
4
  Summary: Hierarchical architecture diagrams from XML with floorplanning and orthogonal routing
5
5
  Author: Ellie Hartung
6
6
  License-Expression: MIT
@@ -96,7 +96,7 @@ XML ──► parse/expand ──► size blocks ──► auto-cluster huge lev
96
96
  ──► hierarchical floorplanning (bottom-up, several passes)
97
97
  ──► detailed QP placement + edge alignment (bottom-up, then top-down)
98
98
  ──► port sides (router-chosen) ──► orthogonal routing ──► congestion-driven spacing
99
- ──► nudging ──► labels ──► SVG / HTML / JSON / PNG
99
+ ──► nudging ──► SVG / HTML / JSON / PNG
100
100
  ```
101
101
 
102
102
  **Floorplanning (placement).** Each subsystem level is a floorplanning problem over
@@ -151,19 +151,19 @@ the border of a subsystem that contains neither end is almost forbidden. Specifi
151
151
  painted over a hop. Crossings closer together than one hop share a single flat bridge.
152
152
  Each hop bumps to the side where the crossed wire has room (clear of its arrowheads,
153
153
  corners and neighbouring parallel wires). A crossing right next to a corner of the
154
- horizontal wire hops sideways on the vertical wire instead. Bit-width markers and edge
155
- labels keep clear of crossings.
154
+ horizontal wire hops sideways on the vertical wire instead. Bit-width markers
155
+ keep clear of crossings.
156
156
 
157
157
  ---
158
158
 
159
159
  ## Command line
160
160
 
161
161
  ```
162
- archgraph render FILE.xml [-o OUT.svg|.html|.json|.png ...] [--stats]
162
+ archgraph render FILE.xml [-o OUT.svg|.html|.json|.png ...] [--stats] [--progress]
163
163
  [--quality draft|normal|high|max] [--aspect 16:9] [--theme dark]
164
164
  [--direction TB] [--seed N] [--set key=value ...]
165
165
  archgraph validate FILE.xml # parse, report errors (with line numbers) and warnings
166
- archgraph stats FILE.xml # lay out and print quality metrics as JSON
166
+ archgraph stats FILE.xml [--progress] # lay out and print quality metrics as JSON
167
167
  archgraph expand FILE.xml # print the XML after templates/loops/includes expand
168
168
  archgraph FILE.xml -o out.svg # shorthand for render
169
169
  ```
@@ -172,6 +172,21 @@ archgraph FILE.xml -o out.svg # shorthand for render
172
172
  violations, wires through blocks, wire length hugging walls, minimum port escape,
173
173
  flow-consistency counts and timings.
174
174
 
175
+ `--progress` shows elapsed time and compact stage bars for port assignment, routing,
176
+ and congestion refinement, plus placement completion, output writing, and `Done`.
177
+ Bars update in place in a terminal; redirected progress logs retain stage milestones.
178
+ Placement shows completed passes and the current subsystem or optimization step.
179
+ A spinner and elapsed time refresh during long computations; redirected logs receive
180
+ an activity update every five seconds while a stage is running.
181
+ Updates go to stderr, so `stats` JSON on stdout can still be redirected to a file.
182
+ Counts describe the current stage; congestion counts show refinements performed out
183
+ of the configured maximum and can finish early. For example:
184
+
185
+ ```sh
186
+ archgraph render examples/soc.xml -o soc.html --progress
187
+ archgraph stats examples/basic.xml --progress > metrics.json
188
+ ```
189
+
175
190
  ## Python API
176
191
 
177
192
  ```python
@@ -71,7 +71,7 @@ XML ──► parse/expand ──► size blocks ──► auto-cluster huge lev
71
71
  ──► hierarchical floorplanning (bottom-up, several passes)
72
72
  ──► detailed QP placement + edge alignment (bottom-up, then top-down)
73
73
  ──► port sides (router-chosen) ──► orthogonal routing ──► congestion-driven spacing
74
- ──► nudging ──► labels ──► SVG / HTML / JSON / PNG
74
+ ──► nudging ──► SVG / HTML / JSON / PNG
75
75
  ```
76
76
 
77
77
  **Floorplanning (placement).** Each subsystem level is a floorplanning problem over
@@ -126,19 +126,19 @@ the border of a subsystem that contains neither end is almost forbidden. Specifi
126
126
  painted over a hop. Crossings closer together than one hop share a single flat bridge.
127
127
  Each hop bumps to the side where the crossed wire has room (clear of its arrowheads,
128
128
  corners and neighbouring parallel wires). A crossing right next to a corner of the
129
- horizontal wire hops sideways on the vertical wire instead. Bit-width markers and edge
130
- labels keep clear of crossings.
129
+ horizontal wire hops sideways on the vertical wire instead. Bit-width markers
130
+ keep clear of crossings.
131
131
 
132
132
  ---
133
133
 
134
134
  ## Command line
135
135
 
136
136
  ```
137
- archgraph render FILE.xml [-o OUT.svg|.html|.json|.png ...] [--stats]
137
+ archgraph render FILE.xml [-o OUT.svg|.html|.json|.png ...] [--stats] [--progress]
138
138
  [--quality draft|normal|high|max] [--aspect 16:9] [--theme dark]
139
139
  [--direction TB] [--seed N] [--set key=value ...]
140
140
  archgraph validate FILE.xml # parse, report errors (with line numbers) and warnings
141
- archgraph stats FILE.xml # lay out and print quality metrics as JSON
141
+ archgraph stats FILE.xml [--progress] # lay out and print quality metrics as JSON
142
142
  archgraph expand FILE.xml # print the XML after templates/loops/includes expand
143
143
  archgraph FILE.xml -o out.svg # shorthand for render
144
144
  ```
@@ -147,6 +147,21 @@ archgraph FILE.xml -o out.svg # shorthand for render
147
147
  violations, wires through blocks, wire length hugging walls, minimum port escape,
148
148
  flow-consistency counts and timings.
149
149
 
150
+ `--progress` shows elapsed time and compact stage bars for port assignment, routing,
151
+ and congestion refinement, plus placement completion, output writing, and `Done`.
152
+ Bars update in place in a terminal; redirected progress logs retain stage milestones.
153
+ Placement shows completed passes and the current subsystem or optimization step.
154
+ A spinner and elapsed time refresh during long computations; redirected logs receive
155
+ an activity update every five seconds while a stage is running.
156
+ Updates go to stderr, so `stats` JSON on stdout can still be redirected to a file.
157
+ Counts describe the current stage; congestion counts show refinements performed out
158
+ of the configured maximum and can finish early. For example:
159
+
160
+ ```sh
161
+ archgraph render examples/soc.xml -o soc.html --progress
162
+ archgraph stats examples/basic.xml --progress > metrics.json
163
+ ```
164
+
150
165
  ## Python API
151
166
 
152
167
  ```python
@@ -18,11 +18,12 @@ from typing import Optional
18
18
 
19
19
  from .model import ArchGraphError, Graph
20
20
  from .parser import parse_file, parse_string
21
+ from .progress import report
21
22
  # import subpackages eagerly so the public functions below are not shadowed by them
22
23
  from .placement import engine as _engine # noqa: E402
23
24
  from . import routing as _routing # noqa: E402
24
25
 
25
- __version__ = "1.0.0"
26
+ __version__ = "2.0.0"
26
27
  __all__ = ["load", "loads", "layout", "route", "build", "to_svg", "to_html", "to_json", "to_png", "save",
27
28
  "ArchGraphError", "Graph", "__version__"]
28
29
 
@@ -44,7 +45,7 @@ def layout(g: Graph) -> Graph:
44
45
 
45
46
 
46
47
  def route(g: Graph) -> Graph:
47
- """Route all connections and buses, nudge them apart and place labels."""
48
+ """Route all connections and buses and nudge them apart."""
48
49
  _routing.route(g)
49
50
  from .geometry import Rect
50
51
  rects = [e.rect for e in g.root.iter_descendants()]
@@ -55,6 +56,7 @@ def route(g: Graph) -> Graph:
55
56
  def build(source: str, **overrides) -> Graph:
56
57
  """Parse (file path or XML text), apply setting overrides, lay out and route."""
57
58
  t0 = time.time()
59
+ report("Parsing XML")
58
60
  if os.path.exists(source) or source.lower().endswith(".xml"):
59
61
  g = parse_file(source)
60
62
  else:
@@ -9,6 +9,7 @@ import time
9
9
 
10
10
  from . import __version__
11
11
  from .model import ArchGraphError
12
+ from .progress import TerminalProgress, report, reporting
12
13
 
13
14
 
14
15
  def _common(p):
@@ -20,6 +21,7 @@ def _common(p):
20
21
  p.add_argument("--aspect", help="target aspect ratio, e.g. 16:9, 1.5, square")
21
22
  p.add_argument("--direction", help="default flow LR | RL | TB | BT")
22
23
  p.add_argument("--seed", type=int, help="random seed (layouts are deterministic per seed)")
24
+ p.add_argument("--progress", action="store_true", help="show stage progress bars and elapsed time on stderr")
23
25
 
24
26
 
25
27
  def _overrides(args):
@@ -61,6 +63,11 @@ def main(argv=None) -> int:
61
63
  return main(["render"] + list(argv))
62
64
  ap.print_help()
63
65
  return 1
66
+ with reporting(TerminalProgress() if getattr(args, "progress", False) else None):
67
+ return _run(args)
68
+
69
+
70
+ def _run(args) -> int:
64
71
  try:
65
72
  if args.cmd == "validate":
66
73
  from .parser import parse_file
@@ -89,17 +96,25 @@ def main(argv=None) -> int:
89
96
  for w in g.warnings:
90
97
  print("warning:", w, file=sys.stderr)
91
98
  if args.cmd == "stats":
99
+ report("Computing quality metrics")
92
100
  print(json.dumps(metrics(g), indent=1))
101
+ report("Done")
93
102
  return 0
94
103
  outs = args.output or [os.path.splitext(args.input)[0] + ".svg"]
95
104
  for o in outs:
105
+ report(f"Writing {o}")
96
106
  save(g, o, scale=args.scale)
97
- print(f"wrote {o}")
107
+ if not args.progress:
108
+ print(f"wrote {o}")
98
109
  if args.stats:
110
+ report("Computing quality metrics")
99
111
  print(json.dumps(metrics(g), indent=1))
100
- print(f"done in {time.time() - t0:.1f}s", file=sys.stderr)
112
+ report("Done")
113
+ if not args.progress:
114
+ print(f"done in {time.time() - t0:.1f}s", file=sys.stderr)
101
115
  return 0
102
116
  except ArchGraphError as ex:
117
+ report("Failed")
103
118
  print("error:", file=sys.stderr)
104
119
  for m in ex.messages:
105
120
  print(" " + m, file=sys.stderr)
@@ -53,7 +53,6 @@ class Settings:
53
53
  junction_dots: bool = True
54
54
  shadows: bool = True
55
55
  background_grid: bool = False
56
- edge_labels: bool = True
57
56
  default_shape: str = "box"
58
57
  bundle_parallel: bool = False # merge parallel edges between the same endpoints
59
58
  margin: float = 24.0 # outer margin of the drawing
@@ -184,6 +183,7 @@ class Node(Element):
184
183
  min_h: Optional[float] = None
185
184
  virtual: bool = False # internal helper node (not drawn)
186
185
  is_note: bool = False
186
+ layout_direction: str = "LR" # inherited flow used by shape geometry
187
187
 
188
188
  kind = "node"
189
189
 
@@ -246,9 +246,6 @@ class Edge:
246
246
  id: str
247
247
  src: Endpoint
248
248
  dst: Endpoint
249
- label: str = ""
250
- src_label: str = ""
251
- dst_label: str = ""
252
249
  classes: List[str] = field(default_factory=list)
253
250
  attrs: Dict[str, str] = field(default_factory=dict)
254
251
  style: Dict[str, Any] = field(default_factory=dict)
@@ -267,9 +264,6 @@ class Edge:
267
264
  line: Optional[int] = None
268
265
  # results
269
266
  points: List[Point] = field(default_factory=list)
270
- label_pos: Optional[Tuple[float, float, float, float]] = None # x, y, w, h (centre)
271
- src_label_pos: Optional[Tuple[float, float]] = None
272
- dst_label_pos: Optional[Tuple[float, float]] = None
273
267
  routed: bool = False
274
268
  uid: int = field(default_factory=lambda: next(_counter))
275
269
 
@@ -286,14 +280,12 @@ class Edge:
286
280
  @dataclass(eq=False, repr=False)
287
281
  class BusTap:
288
282
  endpoint: Endpoint
289
- label: str = ""
290
283
  direction: Optional[str] = None
291
284
 
292
285
 
293
286
  @dataclass(eq=False, repr=False)
294
287
  class Bus:
295
288
  id: str
296
- label: str = ""
297
289
  taps: List[BusTap] = field(default_factory=list)
298
290
  classes: List[str] = field(default_factory=list)
299
291
  attrs: Dict[str, str] = field(default_factory=dict)
@@ -305,7 +297,6 @@ class Bus:
305
297
  # results
306
298
  segments: List[Tuple[Point, Point]] = field(default_factory=list)
307
299
  junctions: List[Point] = field(default_factory=list)
308
- label_pos: Optional[Tuple[float, float, float, float]] = None
309
300
  routed: bool = False
310
301
  uid: int = field(default_factory=lambda: next(_counter))
311
302
 
@@ -30,7 +30,7 @@ LINE = "__line__"
30
30
  FILE = "__file__"
31
31
 
32
32
  NODE_TAGS = {"node", "component", "block", "unit", "item", "instance"}
33
- SHAPE_TAGS = {"chip": "chip", "memory": "cylinder", "storage": "cylinder", "database": "cylinder",
33
+ SHAPE_TAGS = {"memory": "cylinder", "storage": "cylinder", "database": "cylinder",
34
34
  "fifo": "fifo", "mux": "mux", "demux": "demux", "register": "register", "processor": "box",
35
35
  "sensor": "ellipse", "connector": "connector", "alu": "alu", "adder": "adder", "port-pin": "pin"}
36
36
  SUBSYSTEM_TAGS = {"subsystem", "group", "cluster", "module", "domain", "board", "rack", "package", "die",
@@ -49,7 +49,7 @@ REF_ATTRS = {"from", "to", "src", "dst", "source", "target", "nodes", "path", "a
49
49
  SHAPE_ALIASES = {"memory": "cylinder", "storage": "cylinder", "database": "cylinder", "db": "cylinder",
50
50
  "ram": "cylinder", "disk": "cylinder", "rect": "box", "rectangle": "box", "square": "box",
51
51
  "roundbox": "rounded", "round": "rounded", "oval": "ellipse", "buffer": "triangle",
52
- "amplifier": "triangle", "amp": "triangle", "ic": "chip", "package": "folder",
52
+ "amplifier": "triangle", "amp": "triangle", "package": "folder",
53
53
  "queue": "fifo", "flop": "register", "reg": "register", "flipflop": "register",
54
54
  "text": "plaintext", "label": "plaintext", "dsp": "multiplier", "terminal": "pin",
55
55
  "io": "pin", "processor": "cpu", "pill": "pill", "monitor": "display", "screen": "display"}
@@ -62,7 +62,7 @@ def normalize_shape(s: str) -> str:
62
62
 
63
63
  KNOWN_SHAPES = {"box", "rect", "rectangle", "rounded", "roundbox", "ellipse", "oval", "circle", "diamond",
64
64
  "hexagon", "octagon", "parallelogram", "trapezoid", "cylinder", "note", "folder", "component",
65
- "chip", "cloud", "document", "fifo", "register", "mux", "demux", "alu", "adder", "multiplier",
65
+ "cloud", "document", "fifo", "register", "mux", "demux", "alu", "adder", "multiplier",
66
66
  "triangle", "invtriangle", "pin", "connector", "stack", "plaintext", "none", "tab", "cpu",
67
67
  "pill", "cross", "star", "shield", "gear", "antenna", "battery", "display"}
68
68
 
@@ -1086,25 +1086,28 @@ class Builder:
1086
1086
  e.ports[pid] = p
1087
1087
  return Endpoint(element=e, port=p, ref=ref)
1088
1088
 
1089
- EDGE_ATTRS = {"from", "to", "src", "dst", "source", "target", "label", "src-label", "tail-label",
1090
- "dst-label", "head-label", "class", "dir", "directed", "arrowhead", "arrowtail", "weight",
1089
+ EDGE_ATTRS = {"from", "to", "src", "dst", "source", "target", "class", "dir", "directed", "arrowhead", "arrowtail", "weight",
1091
1090
  "minlen", "constraint", "layout", "straight", "bits", "width-bits", "bus-width", "net",
1092
1091
  "tooltip", "id", "nodes", "path", "priority", "lanes"}
1093
1092
 
1093
+ def _connection_attrs(self, el: ET.Element, attrs: Dict[str, Any]) -> Dict[str, Any]:
1094
+ """Ignore retired connection labels while still accepting older XML files."""
1095
+ retired = {"label", "src-label", "tail-label", "dst-label", "head-label"}
1096
+ if retired.intersection(attrs) or el.find("label") is not None or (el.text or "").strip():
1097
+ self.graph.warn(f"{_loc(el)}: connection labels are no longer supported; ignored")
1098
+ return {k: v for k, v in attrs.items() if k not in retired}
1099
+
1094
1100
  def _make_edges(self, el: ET.Element, extra: Dict[str, Any], sub: Subsystem):
1095
1101
  g = self.graph
1096
1102
  attrs = {k: v for k, v in el.attrib.items() if not k.startswith("__")}
1097
1103
  attrs.update({k: v for k, v in extra.items() if not k.startswith("__")})
1104
+ attrs = self._connection_attrs(el, attrs)
1098
1105
  where = _loc(el)
1099
1106
  srcs = _split_refs(attrs.get("from") or attrs.get("src") or attrs.get("source"))
1100
1107
  dsts = _split_refs(attrs.get("to") or attrs.get("dst") or attrs.get("target"))
1101
1108
  if not srcs or not dsts:
1102
1109
  self.errors.append(f"{where}: <{el.tag}> needs from= and to=")
1103
1110
  return
1104
- label = attrs.get("label")
1105
- if label is None:
1106
- lab_el = el.find("label")
1107
- label = (lab_el.text or "").strip() if lab_el is not None else (el.text or "").strip()
1108
1111
  classes = _split_refs(attrs.get("class"))
1109
1112
  raw_style = {}
1110
1113
  for k, v in attrs.items():
@@ -1127,11 +1130,9 @@ class Builder:
1127
1130
  de = self._endpoint(d, where, eid, "d")
1128
1131
  if se is None or de is None:
1129
1132
  continue
1130
- e = Edge(id=eid, src=se, dst=de, label=label or "", classes=list(classes), style=dict(style),
1133
+ e = Edge(id=eid, src=se, dst=de, classes=list(classes), style=dict(style),
1131
1134
  line=_line(el))
1132
1135
  e.attrs = attrs
1133
- e.src_label = attrs.get("src-label", attrs.get("tail-label", ""))
1134
- e.dst_label = attrs.get("dst-label", attrs.get("head-label", ""))
1135
1136
  dirv = (attrs.get("dir") or "").lower()
1136
1137
  if attrs.get("directed") is not None:
1137
1138
  e.directed = st.parse_bool(attrs.get("directed"), True)
@@ -1165,36 +1166,38 @@ class Builder:
1165
1166
  g = self.graph
1166
1167
  where = _loc(el)
1167
1168
  bid = el.get("id") or f"bus{len(g.buses) + 1}"
1168
- bus = Bus(id=bid, label=el.get("label", bid), line=_line(el))
1169
+ attrs = self._connection_attrs(el, el.attrib)
1170
+ bus = Bus(id=bid, line=_line(el))
1169
1171
  bus.classes = _split_refs(el.get("class"))
1170
1172
  bus.bits = el.get("bits", el.get("width-bits", ""))
1171
1173
  bus.orientation = el.get("orientation", "auto")
1172
1174
  bus.tooltip = el.get("tooltip", "")
1173
- raw = {k: v for k, v in el.attrib.items()
1174
- if k not in ("id", "label", "class", "bits", "orientation", "taps", "tooltip", "width-bits")
1175
+ raw = {k: v for k, v in attrs.items()
1176
+ if k not in ("id", "class", "bits", "orientation", "taps", "tooltip", "width-bits")
1175
1177
  and not k.startswith("__")}
1176
1178
  if "style" in raw and raw["style"].lower() not in st.DASH_PATTERNS:
1177
1179
  raw.update(st.parse_css_style(raw.pop("style")))
1178
1180
  bus.style = st.normalize_style(raw)
1179
1181
  k = 0
1180
- tap_specs: List[Tuple[str, str, Optional[str], Optional[int]]] = []
1182
+ tap_specs: List[Tuple[str, Optional[str], Optional[int]]] = []
1181
1183
  for t in _split_refs(el.get("taps")):
1182
- tap_specs.append((t, "", None, _line(el)))
1184
+ tap_specs.append((t, None, _line(el)))
1183
1185
  for c in el:
1184
1186
  if c.tag in ("tap", "member", "endpoint", "connect", "pin"):
1187
+ self._connection_attrs(c, c.attrib)
1185
1188
  ref = c.get("node") or c.get("ref") or c.get("to") or c.get("id")
1186
1189
  if not ref:
1187
1190
  self.errors.append(f"{_loc(c)}: <tap> needs node=")
1188
1191
  continue
1189
1192
  if c.get("port") and ":" not in ref:
1190
1193
  ref = f"{ref}:{c.get('port')}"
1191
- tap_specs.append((ref, c.get("label", ""), c.get("dir", c.get("direction")), _line(c)))
1192
- for ref, lab, d, ln in tap_specs:
1194
+ tap_specs.append((ref, c.get("dir", c.get("direction")), _line(c)))
1195
+ for ref, d, ln in tap_specs:
1193
1196
  ep = self._endpoint(ref, f"{_loc(el)}" if ln is None else f"{os.path.basename(el.get(FILE, ''))}:{ln}",
1194
1197
  f"bus~{bid}~{k}", "t")
1195
1198
  k += 1
1196
1199
  if ep is not None:
1197
- bus.taps.append(BusTap(endpoint=ep, label=lab, direction=d))
1200
+ bus.taps.append(BusTap(endpoint=ep, direction=d))
1198
1201
  if len(bus.taps) < 2:
1199
1202
  self.errors.append(f"{where}: bus '{bid}' needs at least two taps")
1200
1203
  return
@@ -1355,11 +1358,6 @@ def _bundle_parallel(g: Graph):
1355
1358
  continue
1356
1359
  first = es[0]
1357
1360
  first.multiplicity = sum(x.multiplicity for x in es)
1358
- labels = [x.label for x in es if x.label]
1359
- if labels:
1360
- uniq = list(dict.fromkeys(labels))
1361
- first.label = ", ".join(uniq[:3]) + (" …" if len(uniq) > 3 else "")
1362
- first.label = (first.label + " " if first.label else "") + f"×{first.multiplicity}"
1363
1361
  for x in es[1:]:
1364
1362
  dropped.add(x)
1365
1363
  for ep in (x.src, x.dst):
@@ -1435,8 +1433,6 @@ def _apply_styles(g: Graph):
1435
1433
  base.update(e.style)
1436
1434
  if "color" in base and "stroke" not in e.style:
1437
1435
  base["stroke"] = base["color"]
1438
- if "label-color" not in cls and "label-color" not in e.style and "color" in (cls.keys() | e.style.keys()):
1439
- base["label-color"] = st.mix(base["color"], theme["text"], 0.35)
1440
1436
  if "stroke-width-scale" in e.style:
1441
1437
  base["stroke-width"] = base.get("stroke-width", 1.3) * e.style["stroke-width-scale"]
1442
1438
  for raw in [st.class_raw(e.classes, g.classes, "edge")]:
@@ -6,6 +6,7 @@ from typing import Dict
6
6
 
7
7
  from ..geometry import Rect
8
8
  from ..model import Graph, Subsystem
9
+ from ..progress import report
9
10
  from . import sizing
10
11
  from .hierarchy import (HierarchicalPlacer, apply_keep_together, auto_cluster, build_layout_edges, lift_hints,
11
12
  translate_element)
@@ -45,6 +46,7 @@ def unify_instances(g: Graph) -> int:
45
46
  def layout(g: Graph) -> Dict[str, float]:
46
47
  t0 = time.time()
47
48
  stats: Dict[str, float] = {}
49
+ report("Placement")
48
50
  sizing.size_all_nodes(g)
49
51
  unify_instances(g)
50
52
  apply_keep_together(g)
@@ -57,10 +59,16 @@ def layout(g: Graph) -> Dict[str, float]:
57
59
  level_hints = lift_hints(g)
58
60
  placer = HierarchicalPlacer(g, ledges, level_hints)
59
61
  placer.run()
62
+ report("Placement complete")
60
63
  g.extras["placer"] = placer
61
64
  stats["place_time"] = time.time() - t0
62
65
  stats.update({k: float(v) for k, v in placer.stats.items()})
63
66
  finish_ports(g, placer)
67
+ for _ in range(3):
68
+ if not resize_for_ports(g):
69
+ break
70
+ placer.rerun({}, {})
71
+ finish_ports(g, placer)
64
72
  stats["layout_time"] = time.time() - t0
65
73
  g.layout_stats.update(stats)
66
74
  return stats
@@ -68,6 +76,10 @@ def layout(g: Graph) -> Dict[str, float]:
68
76
 
69
77
  def finish_ports(g: Graph, placer):
70
78
  # ports, then a port-aware refinement pass, then final sliding
79
+ counted = not g.settings.float_ports or g.settings.routing == "straight"
80
+ total = len(g.all_ports()) if counted else 0
81
+ if counted:
82
+ report("Port assignment", 0, total)
71
83
  for p in g.all_ports():
72
84
  p.assigned_side = None
73
85
  assign_ports(g, slide=False)
@@ -76,17 +88,15 @@ def finish_ports(g: Graph, placer):
76
88
  distribute_ports(g)
77
89
  slide_ports(g)
78
90
  normalize(g)
91
+ if counted:
92
+ report("Port assignment", total, total)
79
93
 
80
94
 
81
95
  def resize_for_ports(g: Graph) -> int:
82
- """Grow blocks whose sides carry more ports than fit at a comfortable pitch."""
83
- from collections import Counter
84
- pitch = max(g.settings.port_spacing, 2.2 * g.settings.edge_spacing)
96
+ """Grow blocks to fit the labels on their actual assigned sides."""
85
97
  grown = 0
86
98
  for n in g.nodes():
87
- cnt = Counter(p.assigned_side for p in n.ports.values() if p.assigned_side)
88
- need_h = (max(cnt.get("E", 0), cnt.get("W", 0)) + 1) * pitch
89
- need_w = (max(cnt.get("N", 0), cnt.get("S", 0)) + 1) * pitch
99
+ need_w, need_h = sizing.size_node(n, g.settings, sizing.flow_of(n.parent, g.settings))
90
100
  if need_h > n.h + 1:
91
101
  n.h = float(int(need_h + 1.99) // 2 * 2)
92
102
  grown += 1
@@ -95,6 +105,10 @@ def resize_for_ports(g: Graph) -> int:
95
105
  grown += 1
96
106
  if grown:
97
107
  unify_instances(g)
108
+ # Subsystem dimensions are calculated by the placer, rather than set directly.
109
+ for sub in g.subsystems():
110
+ w, h = sizing.port_dimensions(sub, g.settings, sizing.flow_of(sub, g.settings))
111
+ grown += int(w > sub.w + 1 or h > sub.h + 1)
98
112
  return grown
99
113
 
100
114
 
@@ -110,10 +124,12 @@ def expand_congested(g: Graph, route_fn, max_iters: int = 1) -> int:
110
124
  grown = resize_for_ports(g)
111
125
  if worst < 6.0 and not grown:
112
126
  break
127
+ report("Congestion refinement", done, max_iters)
113
128
  placer.rerun(margin, pad)
114
129
  finish_ports(g, placer)
115
- route_fn(g)
116
130
  done += 1
131
+ report("Congestion refinement", done, max_iters)
132
+ route_fn(g)
117
133
  g.layout_stats["spacing_iterations"] = done
118
134
  return done
119
135
 
@@ -28,6 +28,7 @@ from typing import Dict, Hashable, List, Optional, Sequence, Tuple
28
28
  import numpy as np
29
29
 
30
30
  from ..geometry import E, N, S, W
31
+ from ..progress import activity
31
32
 
32
33
  FLOW_VEC = {"LR": (1.0, 0.0), "RL": (-1.0, 0.0), "TB": (0.0, 1.0), "BT": (0.0, -1.0)}
33
34
 
@@ -316,7 +317,7 @@ class Floorplanner:
316
317
  qy = cya[eb] + self._offby[er, var_a[eb]] * sgy[eb]
317
318
  dx = qx - px
318
319
  dy = qy - py
319
- wl = float(np.sum(self._ew * (np.abs(dx) + np.abs(dy))))
320
+ wl = float((self._ew * (np.abs(dx) + np.abs(dy))).sum())
320
321
  bd["wire"] = wts["wire"] * wl / (self.L0 * self.wsum)
321
322
  if self.flow_mult > 0:
322
323
  ux, uy = self.flow_vec
@@ -332,23 +333,24 @@ class Floorplanner:
332
333
  # perpendicular edges are tolerable, backward edges are expensive
333
334
  viol = (0.5 * np.maximum(0.0, delta - np.maximum(along, 0.0)) + 1.5 * np.maximum(0.0, -along))
334
335
  viol = viol * self._eflow
335
- bd["flow"] = wts["flow"] * self.flow_mult * float(np.sum(self._ew * viol)) / (self.L0 * self.wsum)
336
+ bd["flow"] = wts["flow"] * self.flow_mult * float((self._ew * viol).sum()) / (self.L0 * self.wsum)
336
337
  else:
337
338
  bd["flow"] = 0.0
338
339
  if wts.get("bend", 0) > 0:
339
340
  ha, hb, wa, wb = rha[ea], rha[eb], rwa[ea], rwa[eb]
340
341
  sy = np.maximum(0.0, np.abs(dy) - 0.35 * np.minimum(np.minimum(ha, hb), 90.0))
341
342
  sx = np.maximum(0.0, np.abs(dx) - 0.35 * np.minimum(np.minimum(wa, wb), 120.0))
342
- bd["bend"] = wts["bend"] * float(np.sum(self._ew * self._estruct * np.minimum(sx, sy))) / (self.L0 * self.wsum)
343
+ bd["bend"] = wts["bend"] * float((self._ew * self._estruct * np.minimum(sx, sy)).sum()) / (self.L0 * self.wsum)
343
344
  else:
344
345
  bd["bend"] = 0.0
345
346
  if self.use_crossings and ne >= 2:
346
347
  o1 = (qx[:, None] - px[:, None]) * (py[None, :] - py[:, None]) - (qy[:, None] - py[:, None]) * (px[None, :] - px[:, None])
347
348
  o2 = (qx[:, None] - px[:, None]) * (qy[None, :] - py[:, None]) - (qy[:, None] - py[:, None]) * (qx[None, :] - px[:, None])
348
- o3 = (qx[None, :] - px[None, :]) * (py[:, None] - py[None, :]) - (qy[None, :] - py[None, :]) * (px[:, None] - px[None, :])
349
- o4 = (qx[None, :] - px[None, :]) * (qy[:, None] - py[None, :]) - (qy[None, :] - py[None, :]) * (qx[:, None] - px[None, :])
350
- cross = (o1 * o2 < -1e-9) & (o3 * o4 < -1e-9) & self._cross_mask
351
- wcross = float(np.sum(cross * self._ewpair))
349
+ # The reverse orientation tests are the transposes of these
350
+ # same matrices; avoid recomputing two edge-by-edge products.
351
+ opposite = o1 * o2 < -1e-9
352
+ cross = opposite & opposite.T & self._cross_mask
353
+ wcross = float((cross * self._ewpair).sum())
352
354
  bd["crossing"] = wts["crossing"] * wcross / max(1.0, math.sqrt(ne))
353
355
  else:
354
356
  bd["crossing"] = 0.0
@@ -373,14 +375,12 @@ class Floorplanner:
373
375
  hit = bbox & ~(allpos | allneg)
374
376
  own = self._own
375
377
  hit &= ~own
376
- line_hits = np.sum(hit, axis=1).astype(float)
378
+ line_hits = hit.sum(axis=1).astype(float)
377
379
  # orthogonal wires take one of the two L-shaped paths between the pins; a block
378
380
  # only gets in the way when it obstructs both of them
379
381
  pxc, pyc, qxc, qyc = px[:, None], py[:, None], qx[:, None], qy[:, None]
380
- lox, hix = np.minimum(pxc, qxc), np.maximum(pxc, qxc)
381
- loy, hiy = np.minimum(pyc, qyc), np.maximum(pyc, qyc)
382
- in_x = (lox < rx1[None, :]) & (hix > rx0[None, :])
383
- in_y = (loy < ry1[None, :]) & (hiy > ry0[None, :])
382
+ in_x = (minx < rx1[None, :]) & (maxx > rx0[None, :])
383
+ in_y = (miny < ry1[None, :]) & (maxy > ry0[None, :])
384
384
 
385
385
  def hseg(yv):
386
386
  return in_x & (yv > ry0[None, :]) & (yv < ry1[None, :])
@@ -389,7 +389,7 @@ class Floorplanner:
389
389
  return in_y & (xv > rx0[None, :]) & (xv < rx1[None, :])
390
390
  l1 = (hseg(pyc) | vseg(qxc)) & ~own # horizontal first, then vertical
391
391
  l2 = (vseg(pxc) | hseg(qyc)) & ~own # vertical first, then horizontal
392
- line_hits = np.minimum(line_hits, np.minimum(np.sum(l1, axis=1), np.sum(l2, axis=1)))
392
+ line_hits = np.minimum(line_hits, np.minimum(l1.sum(axis=1), l2.sum(axis=1)))
393
393
  # orthogonal corridor blocking: when two connected blocks face each other (overlap
394
394
  # in x and are stacked, or overlap in y and sit side by side) their wires run
395
395
  # inside the shared channel; a third block sitting in that channel forces the
@@ -412,10 +412,10 @@ class Floorplanner:
412
412
  iy = np.where(own, 0.0, iy)
413
413
  span = np.where(vert, kx1 - kx0, ky1 - ky0)
414
414
  blocked = np.where(vert[:, None], ix * (iy > 0), iy * (ix > 0))
415
- frac = np.minimum(1.0, np.sum(blocked, axis=1) / np.maximum(span, 1.0))
415
+ frac = np.minimum(1.0, blocked.sum(axis=1) / np.maximum(span, 1.0))
416
416
  frac = np.where(vert | horz, frac, 0.0)
417
417
  line_hits = np.maximum(line_hits, frac)
418
- bd["through"] = wts["through"] * float(np.sum(line_hits * self._ew * self._estruct)) / max(1.0, math.sqrt(ne))
418
+ bd["through"] = wts["through"] * float((line_hits * self._ew * self._estruct).sum()) / max(1.0, math.sqrt(ne))
419
419
  else:
420
420
  bd["through"] = 0.0
421
421
  else:
@@ -748,15 +748,33 @@ class Floorplanner:
748
748
  def anneal(self, init: FPState, moves: int, accept0: float = 0.75, deadline: Optional[float] = None
749
749
  ) -> Tuple[FPState, float]:
750
750
  n = self.n
751
+ # The objective is unchanged during this anneal. Neighbour moves often
752
+ # revisit exactly the same state, especially for small subsystem levels.
753
+ # Cache only scores, and snapshot the mutable state lists in the key.
754
+ scores = {}
755
+
756
+ def evaluate(st):
757
+ key = (tuple(st.gp), tuple(st.gm), tuple(st.var), tuple(st.fx), tuple(st.fy))
758
+ cost = scores.get(key)
759
+ if cost is not None:
760
+ self.evals += 1
761
+ return cost
762
+ cost = self.evaluate(st)
763
+ # Bound memory only; eviction never skips a candidate or a move.
764
+ if len(scores) >= 8192:
765
+ scores.clear()
766
+ scores[key] = cost
767
+ return cost
768
+
751
769
  cur = self.tie(init.copy())
752
- cur_cost = self.evaluate(cur)
770
+ cur_cost = evaluate(cur)
753
771
  best, best_cost = cur.copy(), cur_cost
754
772
  if n <= 1 or moves <= 0:
755
773
  return best, best_cost
756
774
  # initial temperature from uphill move statistics
757
775
  ups = []
758
776
  for _ in range(min(80, 10 + 4 * n)):
759
- c = self.evaluate(self.neighbor(cur))
777
+ c = evaluate(self.neighbor(cur))
760
778
  if c > cur_cost:
761
779
  ups.append(c - cur_cost)
762
780
  avg_up = (sum(ups) / len(ups)) if ups else max(1e-3, 0.05 * abs(cur_cost))
@@ -767,11 +785,12 @@ class Floorplanner:
767
785
  alpha = (2e-4) ** (1.0 / steps)
768
786
  done = 0
769
787
  stale = 0
788
+ activity(step=f"0/{moves} moves")
770
789
  while done < moves:
771
790
  improved = False
772
791
  for _ in range(per_T):
773
792
  cand = self.neighbor(cur)
774
- c = self.evaluate(cand)
793
+ c = evaluate(cand)
775
794
  d = c - cur_cost
776
795
  if d <= 0 or self.rng.random() < math.exp(-d / max(T, 1e-12)):
777
796
  cur, cur_cost = cand, c
@@ -780,16 +799,18 @@ class Floorplanner:
780
799
  improved = True
781
800
  done += 1
782
801
  T *= alpha
802
+ activity(step=f"{min(done, moves)}/{moves} moves")
783
803
  stale = 0 if improved else stale + 1
784
804
  if deadline is not None and time.time() > deadline:
785
805
  break
786
806
  if stale > max(12, steps // 4) and T < T0 * 0.01:
787
807
  break
788
808
  # greedy polish from best
809
+ activity(step="polishing")
789
810
  cur, cur_cost = best.copy(), best_cost
790
811
  for _ in range(min(2000, 40 * n)):
791
812
  cand = self.neighbor(cur)
792
- c = self.evaluate(cand)
813
+ c = evaluate(cand)
793
814
  if c < cur_cost - 1e-12:
794
815
  cur, cur_cost = cand, c
795
816
  if cur_cost < best_cost: