archgraph-layout 1.0.0__py3-none-any.whl
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.
- archgraph/__init__.py +108 -0
- archgraph/__main__.py +5 -0
- archgraph/cli.py +110 -0
- archgraph/geometry.py +256 -0
- archgraph/model.py +394 -0
- archgraph/parser.py +1508 -0
- archgraph/placement/__init__.py +0 -0
- archgraph/placement/congestion.py +108 -0
- archgraph/placement/detail.py +456 -0
- archgraph/placement/engine.py +133 -0
- archgraph/placement/floorplan.py +876 -0
- archgraph/placement/graphalgo.py +345 -0
- archgraph/placement/hierarchy.py +1086 -0
- archgraph/placement/layered.py +411 -0
- archgraph/placement/ports.py +354 -0
- archgraph/placement/sizing.py +234 -0
- archgraph/placement/vpsc.py +496 -0
- archgraph/quality.py +144 -0
- archgraph/render/__init__.py +0 -0
- archgraph/render/html.py +154 -0
- archgraph/render/jsonout.py +43 -0
- archgraph/render/png.py +35 -0
- archgraph/render/shapes.py +240 -0
- archgraph/render/svg.py +1077 -0
- archgraph/routing/__init__.py +162 -0
- archgraph/routing/grid.py +267 -0
- archgraph/routing/labels.py +141 -0
- archgraph/routing/nudge.py +291 -0
- archgraph/routing/router.py +1112 -0
- archgraph/styles.py +405 -0
- archgraph/textmetrics.py +92 -0
- archgraph_layout-1.0.0.dist-info/METADATA +245 -0
- archgraph_layout-1.0.0.dist-info/RECORD +37 -0
- archgraph_layout-1.0.0.dist-info/WHEEL +5 -0
- archgraph_layout-1.0.0.dist-info/entry_points.txt +2 -0
- archgraph_layout-1.0.0.dist-info/licenses/LICENSE +21 -0
- archgraph_layout-1.0.0.dist-info/top_level.txt +1 -0
archgraph/model.py
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
"""Core data model: graph, subsystems, nodes, ports, edges, buses and hints."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import itertools
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any, Dict, Iterator, List, Optional, Tuple
|
|
7
|
+
|
|
8
|
+
from .geometry import Point, Rect
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ArchGraphError(Exception):
|
|
12
|
+
"""Raised for invalid input; carries all messages collected while parsing."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, messages):
|
|
15
|
+
if isinstance(messages, str):
|
|
16
|
+
messages = [messages]
|
|
17
|
+
self.messages = list(messages)
|
|
18
|
+
super().__init__("\n".join(self.messages))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# --------------------------------------------------------------------------------------
|
|
22
|
+
# Settings
|
|
23
|
+
# --------------------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class Settings:
|
|
27
|
+
title: str = ""
|
|
28
|
+
subtitle: str = ""
|
|
29
|
+
direction: str = "LR" # default flow direction: LR, RL, TB, BT
|
|
30
|
+
aspect: Optional[float] = 1.6 # target width/height of the whole drawing (None = free)
|
|
31
|
+
theme: str = "light"
|
|
32
|
+
seed: int = 7
|
|
33
|
+
quality: str = "normal" # draft | normal | high | max
|
|
34
|
+
node_spacing: float = 34.0 # minimum gap between sibling blocks
|
|
35
|
+
subsystem_padding: float = 16.0 # inner padding of subsystem boxes
|
|
36
|
+
edge_spacing: float = 7.0 # pitch between parallel wires after nudging
|
|
37
|
+
port_spacing: float = 14.0 # minimum pitch between ports on a side
|
|
38
|
+
node_margin: float = 10.0 # wire clearance around nodes
|
|
39
|
+
font_family: str = "Helvetica, Arial, sans-serif"
|
|
40
|
+
font_size: float = 12.0
|
|
41
|
+
routing: str = "orthogonal" # orthogonal | straight
|
|
42
|
+
corner_radius: float = 3.0 # rounded wire corners (0 = sharp)
|
|
43
|
+
crossings: str = "hop" # hop | gap | none
|
|
44
|
+
legend: str = "auto" # auto | none | always
|
|
45
|
+
merge_nets: bool = True # edges leaving the same port route as one net (tree)
|
|
46
|
+
auto_group: str = "auto" # auto | visible | off -- automatic clustering of huge levels
|
|
47
|
+
max_level_items: int = 30 # auto-cluster threshold per subsystem level
|
|
48
|
+
passes: int = 0 # 0 = by quality
|
|
49
|
+
flow: str = "normal" # strong | normal | weak | none
|
|
50
|
+
align_edges: bool = True
|
|
51
|
+
show_ports: bool = True
|
|
52
|
+
port_labels: bool = True
|
|
53
|
+
junction_dots: bool = True
|
|
54
|
+
shadows: bool = True
|
|
55
|
+
background_grid: bool = False
|
|
56
|
+
edge_labels: bool = True
|
|
57
|
+
default_shape: str = "box"
|
|
58
|
+
bundle_parallel: bool = False # merge parallel edges between the same endpoints
|
|
59
|
+
margin: float = 24.0 # outer margin of the drawing
|
|
60
|
+
weights: Dict[str, float] = field(default_factory=dict) # floorplanner cost weight overrides
|
|
61
|
+
time_budget: float = 0.0 # optional soft limit in seconds for annealing (0 = none)
|
|
62
|
+
spacing_passes: int = -1 # routing-aware spacing iterations (-1 = by quality)
|
|
63
|
+
float_ports: bool = True # let a routing pass choose the side of automatic ports
|
|
64
|
+
uniform_instances: bool = True # instances of the same template share one internal layout
|
|
65
|
+
|
|
66
|
+
def quality_factor(self) -> float:
|
|
67
|
+
return {"draft": 0.25, "fast": 0.25, "normal": 1.0, "high": 2.5, "max": 6.0}.get(self.quality, 1.0)
|
|
68
|
+
|
|
69
|
+
def num_spacing_passes(self) -> int:
|
|
70
|
+
if self.spacing_passes >= 0:
|
|
71
|
+
return self.spacing_passes
|
|
72
|
+
return {"draft": 0, "fast": 0, "normal": 2, "high": 3, "max": 4}.get(self.quality, 2)
|
|
73
|
+
|
|
74
|
+
def num_passes(self) -> int:
|
|
75
|
+
if self.passes > 0:
|
|
76
|
+
return self.passes
|
|
77
|
+
return {"draft": 1, "fast": 1, "normal": 2, "high": 3, "max": 3}.get(self.quality, 2)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# --------------------------------------------------------------------------------------
|
|
81
|
+
# Graph elements
|
|
82
|
+
# --------------------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
_counter = itertools.count()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(eq=False, repr=False)
|
|
88
|
+
class Port:
|
|
89
|
+
id: str
|
|
90
|
+
owner: "Element"
|
|
91
|
+
side: Optional[str] = None # requested side (None = automatic)
|
|
92
|
+
label: str = ""
|
|
93
|
+
order: Optional[float] = None
|
|
94
|
+
direction: Optional[str] = None # in | out | inout
|
|
95
|
+
style: Dict[str, Any] = field(default_factory=dict)
|
|
96
|
+
fixed: bool = False # do not slide along the side
|
|
97
|
+
anonymous: bool = False # generated for an edge that named no port
|
|
98
|
+
line: Optional[int] = None
|
|
99
|
+
# layout results
|
|
100
|
+
assigned_side: Optional[str] = None
|
|
101
|
+
route_side: Optional[str] = None # side chosen by the floating-port routing pass
|
|
102
|
+
x: float = 0.0
|
|
103
|
+
y: float = 0.0
|
|
104
|
+
edges: List["Edge"] = field(default_factory=list)
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def full_id(self) -> str:
|
|
108
|
+
return f"{self.owner.id}:{self.id}"
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def point(self) -> Point:
|
|
112
|
+
return (self.x, self.y)
|
|
113
|
+
|
|
114
|
+
def __repr__(self):
|
|
115
|
+
return f"Port({self.full_id})"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass(eq=False, repr=False)
|
|
119
|
+
class Element:
|
|
120
|
+
id: str
|
|
121
|
+
label: str = ""
|
|
122
|
+
parent: Optional["Subsystem"] = None
|
|
123
|
+
classes: List[str] = field(default_factory=list)
|
|
124
|
+
attrs: Dict[str, str] = field(default_factory=dict)
|
|
125
|
+
style: Dict[str, Any] = field(default_factory=dict)
|
|
126
|
+
props: List[Tuple[str, str]] = field(default_factory=list)
|
|
127
|
+
ports: Dict[str, Port] = field(default_factory=dict)
|
|
128
|
+
tooltip: str = ""
|
|
129
|
+
url: str = ""
|
|
130
|
+
line: Optional[int] = None
|
|
131
|
+
# geometry (centre + size)
|
|
132
|
+
x: float = 0.0
|
|
133
|
+
y: float = 0.0
|
|
134
|
+
w: float = 0.0
|
|
135
|
+
h: float = 0.0
|
|
136
|
+
uid: int = field(default_factory=lambda: next(_counter))
|
|
137
|
+
|
|
138
|
+
kind = "element"
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def rect(self) -> Rect:
|
|
142
|
+
return Rect.from_center(self.x, self.y, self.w, self.h)
|
|
143
|
+
|
|
144
|
+
def set_rect(self, r: Rect):
|
|
145
|
+
self.x, self.y, self.w, self.h = r.cx, r.cy, r.w, r.h
|
|
146
|
+
|
|
147
|
+
def ancestors(self) -> List["Subsystem"]:
|
|
148
|
+
out = []
|
|
149
|
+
p = self.parent
|
|
150
|
+
while p is not None:
|
|
151
|
+
out.append(p)
|
|
152
|
+
p = p.parent
|
|
153
|
+
return out
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def depth(self) -> int:
|
|
157
|
+
return len(self.ancestors())
|
|
158
|
+
|
|
159
|
+
def is_descendant_of(self, sub: "Subsystem") -> bool:
|
|
160
|
+
p = self.parent
|
|
161
|
+
while p is not None:
|
|
162
|
+
if p is sub:
|
|
163
|
+
return True
|
|
164
|
+
p = p.parent
|
|
165
|
+
return False
|
|
166
|
+
|
|
167
|
+
def display_label(self) -> str:
|
|
168
|
+
return self.label if self.label is not None else self.id
|
|
169
|
+
|
|
170
|
+
def __hash__(self):
|
|
171
|
+
return self.uid
|
|
172
|
+
|
|
173
|
+
def __repr__(self):
|
|
174
|
+
return f"{type(self).__name__}({self.id})"
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclass(eq=False, repr=False)
|
|
178
|
+
class Node(Element):
|
|
179
|
+
shape: str = "box"
|
|
180
|
+
sublabel: str = ""
|
|
181
|
+
icon: str = ""
|
|
182
|
+
multiplicity: int = 1
|
|
183
|
+
min_w: Optional[float] = None
|
|
184
|
+
min_h: Optional[float] = None
|
|
185
|
+
virtual: bool = False # internal helper node (not drawn)
|
|
186
|
+
is_note: bool = False
|
|
187
|
+
|
|
188
|
+
kind = "node"
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@dataclass(eq=False, repr=False)
|
|
192
|
+
class Subsystem(Element):
|
|
193
|
+
children: List[Element] = field(default_factory=list)
|
|
194
|
+
direction: Optional[str] = None
|
|
195
|
+
algorithm: str = "auto" # auto | floorplan | layered | grid | row | column
|
|
196
|
+
aspect: Optional[float] = None
|
|
197
|
+
padding: Optional[float] = None
|
|
198
|
+
spacing: Optional[float] = None
|
|
199
|
+
collapsed: bool = False
|
|
200
|
+
visible: bool = True
|
|
201
|
+
auto: bool = False # created by auto-clustering
|
|
202
|
+
columns: Optional[int] = None # for grid algorithm
|
|
203
|
+
header_h: float = 0.0 # computed height reserved for the title
|
|
204
|
+
|
|
205
|
+
kind = "subsystem"
|
|
206
|
+
|
|
207
|
+
def iter_descendants(self) -> Iterator[Element]:
|
|
208
|
+
for c in self.children:
|
|
209
|
+
yield c
|
|
210
|
+
if isinstance(c, Subsystem):
|
|
211
|
+
yield from c.iter_descendants()
|
|
212
|
+
|
|
213
|
+
def leaves(self) -> List[Node]:
|
|
214
|
+
return [e for e in self.iter_descendants() if isinstance(e, Node)]
|
|
215
|
+
|
|
216
|
+
def subsystems(self) -> List["Subsystem"]:
|
|
217
|
+
return [e for e in self.iter_descendants() if isinstance(e, Subsystem)]
|
|
218
|
+
|
|
219
|
+
def contains(self, el: Element) -> bool:
|
|
220
|
+
return el.is_descendant_of(self)
|
|
221
|
+
|
|
222
|
+
def child_containing(self, el: Element) -> Optional[Element]:
|
|
223
|
+
"""Direct child of this subsystem that is or contains ``el``."""
|
|
224
|
+
cur = el
|
|
225
|
+
while cur is not None and cur.parent is not self:
|
|
226
|
+
cur = cur.parent
|
|
227
|
+
return cur
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@dataclass(eq=False, repr=False)
|
|
231
|
+
class Endpoint:
|
|
232
|
+
element: Element
|
|
233
|
+
port: Optional[Port] = None
|
|
234
|
+
ref: str = ""
|
|
235
|
+
|
|
236
|
+
@property
|
|
237
|
+
def node(self) -> Element:
|
|
238
|
+
return self.element
|
|
239
|
+
|
|
240
|
+
def __repr__(self):
|
|
241
|
+
return f"Endpoint({self.port.full_id if self.port else self.element.id})"
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@dataclass(eq=False, repr=False)
|
|
245
|
+
class Edge:
|
|
246
|
+
id: str
|
|
247
|
+
src: Endpoint
|
|
248
|
+
dst: Endpoint
|
|
249
|
+
label: str = ""
|
|
250
|
+
src_label: str = ""
|
|
251
|
+
dst_label: str = ""
|
|
252
|
+
classes: List[str] = field(default_factory=list)
|
|
253
|
+
attrs: Dict[str, str] = field(default_factory=dict)
|
|
254
|
+
style: Dict[str, Any] = field(default_factory=dict)
|
|
255
|
+
directed: bool = True
|
|
256
|
+
arrowhead: Optional[str] = None
|
|
257
|
+
arrowtail: Optional[str] = None
|
|
258
|
+
weight: float = 1.0
|
|
259
|
+
minlen: int = 1
|
|
260
|
+
constraint: bool = True # participates in flow direction
|
|
261
|
+
layout: bool = True # participates in placement at all
|
|
262
|
+
straight: bool = False # high priority for straight routing
|
|
263
|
+
bits: str = "" # bus width annotation (e.g. 64 -> slash marker)
|
|
264
|
+
net: Optional[str] = None
|
|
265
|
+
tooltip: str = ""
|
|
266
|
+
multiplicity: int = 1
|
|
267
|
+
line: Optional[int] = None
|
|
268
|
+
# results
|
|
269
|
+
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
|
+
routed: bool = False
|
|
274
|
+
uid: int = field(default_factory=lambda: next(_counter))
|
|
275
|
+
|
|
276
|
+
def __hash__(self):
|
|
277
|
+
return self.uid
|
|
278
|
+
|
|
279
|
+
def __repr__(self):
|
|
280
|
+
return f"Edge({self.id}: {self.src} -> {self.dst})"
|
|
281
|
+
|
|
282
|
+
def endpoints(self) -> Tuple[Endpoint, Endpoint]:
|
|
283
|
+
return self.src, self.dst
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@dataclass(eq=False, repr=False)
|
|
287
|
+
class BusTap:
|
|
288
|
+
endpoint: Endpoint
|
|
289
|
+
label: str = ""
|
|
290
|
+
direction: Optional[str] = None
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@dataclass(eq=False, repr=False)
|
|
294
|
+
class Bus:
|
|
295
|
+
id: str
|
|
296
|
+
label: str = ""
|
|
297
|
+
taps: List[BusTap] = field(default_factory=list)
|
|
298
|
+
classes: List[str] = field(default_factory=list)
|
|
299
|
+
attrs: Dict[str, str] = field(default_factory=dict)
|
|
300
|
+
style: Dict[str, Any] = field(default_factory=dict)
|
|
301
|
+
bits: str = ""
|
|
302
|
+
orientation: str = "auto"
|
|
303
|
+
tooltip: str = ""
|
|
304
|
+
line: Optional[int] = None
|
|
305
|
+
# results
|
|
306
|
+
segments: List[Tuple[Point, Point]] = field(default_factory=list)
|
|
307
|
+
junctions: List[Point] = field(default_factory=list)
|
|
308
|
+
label_pos: Optional[Tuple[float, float, float, float]] = None
|
|
309
|
+
routed: bool = False
|
|
310
|
+
uid: int = field(default_factory=lambda: next(_counter))
|
|
311
|
+
|
|
312
|
+
def __hash__(self):
|
|
313
|
+
return self.uid
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
@dataclass
|
|
317
|
+
class Hint:
|
|
318
|
+
kind: str
|
|
319
|
+
refs: List[str] = field(default_factory=list)
|
|
320
|
+
params: Dict[str, Any] = field(default_factory=dict)
|
|
321
|
+
line: Optional[int] = None
|
|
322
|
+
elements: List[Element] = field(default_factory=list) # resolved refs
|
|
323
|
+
|
|
324
|
+
def __repr__(self):
|
|
325
|
+
return f"Hint({self.kind} {' '.join(self.refs)} {self.params})"
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@dataclass
|
|
329
|
+
class LegendEntry:
|
|
330
|
+
label: str
|
|
331
|
+
kind: str = "node" # node | edge | subsystem | bus
|
|
332
|
+
classes: List[str] = field(default_factory=list)
|
|
333
|
+
style: Dict[str, Any] = field(default_factory=dict)
|
|
334
|
+
shape: Optional[str] = None
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
@dataclass
|
|
338
|
+
class Graph:
|
|
339
|
+
root: Subsystem
|
|
340
|
+
settings: Settings = field(default_factory=Settings)
|
|
341
|
+
elements: Dict[str, Element] = field(default_factory=dict)
|
|
342
|
+
edges: List[Edge] = field(default_factory=list)
|
|
343
|
+
buses: List[Bus] = field(default_factory=list)
|
|
344
|
+
hints: List[Hint] = field(default_factory=list)
|
|
345
|
+
classes: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
|
346
|
+
legend: List[LegendEntry] = field(default_factory=list)
|
|
347
|
+
legend_title: str = "Legend"
|
|
348
|
+
titleblock: Dict[str, str] = field(default_factory=dict)
|
|
349
|
+
warnings: List[str] = field(default_factory=list)
|
|
350
|
+
source: str = ""
|
|
351
|
+
# results
|
|
352
|
+
bbox: Optional[Rect] = None
|
|
353
|
+
layout_stats: Dict[str, Any] = field(default_factory=dict)
|
|
354
|
+
extras: Dict[str, Any] = field(default_factory=dict)
|
|
355
|
+
|
|
356
|
+
# -- queries -----------------------------------------------------------------------
|
|
357
|
+
def nodes(self, include_virtual: bool = False) -> List[Node]:
|
|
358
|
+
return [e for e in self.root.iter_descendants()
|
|
359
|
+
if isinstance(e, Node) and (include_virtual or not e.virtual)]
|
|
360
|
+
|
|
361
|
+
def subsystems(self) -> List[Subsystem]:
|
|
362
|
+
return [e for e in self.root.iter_descendants() if isinstance(e, Subsystem)]
|
|
363
|
+
|
|
364
|
+
def get(self, ident: str) -> Optional[Element]:
|
|
365
|
+
return self.elements.get(ident)
|
|
366
|
+
|
|
367
|
+
def lca(self, a: Element, b: Element) -> Subsystem:
|
|
368
|
+
"""Deepest subsystem strictly containing both elements."""
|
|
369
|
+
anc_b = set(b.ancestors())
|
|
370
|
+
for s in a.ancestors():
|
|
371
|
+
if s in anc_b:
|
|
372
|
+
return s
|
|
373
|
+
return self.root
|
|
374
|
+
|
|
375
|
+
def edge_scope(self, a: Element, b: Element) -> Subsystem:
|
|
376
|
+
"""Subsystem in which a connection between ``a`` and ``b`` lives.
|
|
377
|
+
|
|
378
|
+
A connection from a block to a port of one of its own ancestors lives inside
|
|
379
|
+
that ancestor; otherwise it lives in the strict lowest common ancestor."""
|
|
380
|
+
if isinstance(b, Subsystem) and a.is_descendant_of(b):
|
|
381
|
+
return b
|
|
382
|
+
if isinstance(a, Subsystem) and b.is_descendant_of(a):
|
|
383
|
+
return a
|
|
384
|
+
return self.lca(a, b)
|
|
385
|
+
|
|
386
|
+
def all_ports(self) -> List[Port]:
|
|
387
|
+
out = []
|
|
388
|
+
for e in self.root.iter_descendants():
|
|
389
|
+
out.extend(e.ports.values())
|
|
390
|
+
out.extend(self.root.ports.values())
|
|
391
|
+
return out
|
|
392
|
+
|
|
393
|
+
def warn(self, msg: str):
|
|
394
|
+
self.warnings.append(msg)
|