#!/usr/bin/env python3 """Generate an editable diagrams.net library for Conv lowering comparisons.""" from __future__ import annotations import argparse import xml.etree.ElementTree as ET from dataclasses import dataclass from pathlib import Path TILE = 760 GAP = 40 COLS = 4 INK = "#172033" MUTED = "#667085" GRID = "#d7deea" PALE = "#f8fafc" REFERENCE = "#5f6b7a" PIMCOMP = "#ef8354" RAPTOR = "#3b82f6" INPUT = ("#f4a261", "#52b788", "#4895ef") WEIGHT = ("#c86418", "#237a57", "#2768b2") OUTPUT = ("#8b5cf6", "#ec4899", "#06b6d4", "#eab308") SPATIAL = ("00", "01", "02", "10", "11", "12", "20", "21", "22") @dataclass(frozen=True) class Tile: slug: str owner: str title: str subtitle: str scene: str formula: str algorithm: str TILES = ( Tile("classic-reference", "REFERENCE", "Classic Conv + exact weight unfolding", "Original OIHW weights; the two implementations choose different K orders.", "reference", "Y[p,o] = Σc,kh,kw Xpatch[p,c,kh,kw] · W[o,c,kh,kw]", "At every output position, multiply the patch by one filter and add every product."), Tile("pimcomp-element", "PIMCOMP", "Element pipeline", "One patch vector per input cycle; mapped weights stay fixed.", "pimcomp_element", "patchPIM[1×K] · WflatPIM[K×O] → Yp[1×O]", "Keep Wflat in the arrays; stream one patch each cycle to produce all O outputs."), Tile("pimcomp-batch", "PIMCOMP", "Batch / replicated pipeline", "Complete Wflat copies divide patches or input samples.", "pimcomp_batch", "for replica r: Yr = patchr[1×K] · WflatPIM[K×O]", "Copy all weights R times and send different patches to the copies in parallel."), Tile("raptor-legacy-im2col", "RAPTOR", "Legacy explicit im2col", "Every patch becomes one row of a global P×K matrix.", "legacy", "Y[P×O] = im2col(X)[P×K] · WflatR[K×O]", "Write every image patch as one matrix row, then multiply the two large matrices."), Tile("raptor-packed-im2col", "RAPTOR", "Packed im2col", "Pack q patch rows and repeat Wflat on a block diagonal.", "packed", "packedY[1×qO] = [patch0|…|patchq−1] · diag(WflatR,…,WflatR)", "Join q patches and use diagonal weight copies so one multiply computes q independent outputs."), Tile("raptor-streamed-patch", "RAPTOR", "Streamed patch", "Gather one patch into bounded scratch; avoid global im2col.", "streamed_patch", "Yp[1×O] = scratchPatchp[1×K] · WflatR[K×O]", "Gather one patch, multiply it, write its output, and reuse scratch for the next patch."), Tile("raptor-streamed-packed", "RAPTOR", "Streamed packed", "Gather q patch rows in bounded scratch, then block-diagonal pack them.", "streamed_packed", "packedY = packedScratch[1×qK] · diag(WflatR×q)[qK×qO]", "Gather q patches in small scratch, join them, multiply by diagonal weights, then unpack q outputs."), Tile("raptor-depthwise", "RAPTOR", "Depthwise special case", "Each channel owns one row-major 3×3 kernel; channels never reduce together.", "depthwise", "Y[p,c] = Σkh,kw Xpatch[p,c,kh,kw] · W[c,kh,kw]", "For each channel separately, multiply its nine patch values by its nine weights and add."), Tile("raptor-output-channel-tiled", "RAPTOR", "Output-channel tiled", "Every O tile retains all channel-major K rows and selects output columns.", "c_tiled", "Y[:,Oj] = patch[1×K] · Wflat[:,Oj][K×|Oj|]; concat j", "Reuse the full patch for each output-filter group, then join the output groups."), Tile("raptor-input-k-tiled", "RAPTOR", "Input-K tiled", "Split matching K ranges; add their partial output vectors.", "k_tiled", "Y[1×O] = Σi patch[Ki] · Wflat[Ki,:]", "Multiply matching K slices independently, then add their partial output vectors."), Tile("raptor-tiled-2d", "RAPTOR", "Two-dimensional tiled", "Partition both K rows and output-filter columns.", "tiled_2d", "Y[:,Oj] = Σi patch[Ki] · Wflat[Ki,Oj]; concat j", "Split both directions: add results down K and join results across output groups."), Tile("raptor-row-strip", "RAPTOR", "Pixel-major row-strip", "A lane forms patches across one output row and slices K.", "row_strip", "for x: Y[r,x,:] = Σi patch[r,x,Ki] · Wflat[Ki,:]", "Move across one output row; at each x form a patch, multiply its K slices, and add."), Tile("raptor-row-strip-c-tiled", "RAPTOR", "Row-strip + output tiling", "Each row lane is duplicated across disjoint output-column tiles.", "row_strip_c", "for x,j: Y[r,x,Oj] = patch[r,x,:] · Wflat[:,Oj]", "Give each output-filter group a copy of the row lane, then join their output columns."), ) class Drawio: def __init__(self) -> None: rows = (len(TILES) + COLS - 1) // COLS self.mxfile = ET.Element("mxfile", host="app.diagrams.net", compressed="false") diagram = ET.SubElement(self.mxfile, "diagram", id="conv-lowering-library", name="Conv lowering tile library") model = ET.SubElement( diagram, "mxGraphModel", dx="1200", dy="900", grid="1", gridSize="10", guides="1", tooltips="1", connect="1", arrows="1", fold="1", page="0", pageScale="1", pageWidth=str(COLS * (TILE + GAP)), pageHeight=str(rows * (TILE + GAP)), math="0", shadow="0", ) self.root = ET.SubElement(model, "root") ET.SubElement(self.root, "mxCell", id="0") ET.SubElement(self.root, "mxCell", id="1", parent="0") self.counter = 2 def _id(self, prefix: str = "c") -> str: value = f"{prefix}-{self.counter}" self.counter += 1 return value def vertex(self, parent: str, x: float, y: float, w: float, h: float, value: str = "", style: str = "", *, cell_id: str | None = None) -> str: cell_id = cell_id or self._id() cell = ET.SubElement(self.root, "mxCell", id=cell_id, value=value, style=style, vertex="1", parent=parent) ET.SubElement(cell, "mxGeometry", x=str(x), y=str(y), width=str(w), height=str(h), **{"as": "geometry"}) return cell_id def group(self, x: float, y: float, name: str) -> str: return self.vertex("1", x, y, TILE, TILE, name, "group;connectable=0;", cell_id=f"tile-{name}") def edge(self, parent: str, source: str, target: str, value: str = "") -> str: style = ( "edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;" f"html=1;endArrow=block;endFill=1;strokeWidth=2;strokeColor={INK};" f"fontSize=10;fontColor={INK};labelBackgroundColor=#ffffff;" ) cell_id = self._id("e") cell = ET.SubElement(self.root, "mxCell", id=cell_id, value=value, style=style, edge="1", parent=parent, source=source, target=target) ET.SubElement(cell, "mxGeometry", relative="1", **{"as": "geometry"}) return cell_id def write(self, path: Path) -> None: ET.indent(self.mxfile, space=" ") path.parent.mkdir(parents=True, exist_ok=True) ET.ElementTree(self.mxfile).write(path, encoding="utf-8", xml_declaration=True) def rect_style(fill: str = "#ffffff", stroke: str = GRID, *, rounded: bool = True, font_size: int = 11, font_color: str = INK, align: str = "center", stroke_width: int = 1) -> str: return ( f"rounded={int(rounded)};whiteSpace=wrap;html=1;fillColor={fill};" f"strokeColor={stroke};strokeWidth={stroke_width};fontColor={font_color};" f"fontSize={font_size};fontFamily=Helvetica;align={align};verticalAlign=middle;" ) def text_style(size: int, *, color: str = INK, align: str = "left", bold: bool = False) -> str: return ( "text;html=1;strokeColor=none;fillColor=none;whiteSpace=wrap;" f"fontSize={size};fontColor={color};fontFamily=Helvetica;align={align};" f"verticalAlign=middle;fontStyle={1 if bold else 0};" ) def add_text(d: Drawio, parent: str, x: float, y: float, w: float, h: float, value: str, size: int = 11, *, color: str = INK, align: str = "left", bold: bool = False) -> str: return d.vertex(parent, x, y, w, h, value, text_style(size, color=color, align=align, bold=bold)) def add_box(d: Drawio, parent: str, x: float, y: float, w: float, h: float, value: str, *, fill: str = "#ffffff", stroke: str = GRID, size: int = 11, rounded: bool = True, stroke_width: int = 1) -> str: return d.vertex(parent, x, y, w, h, value, rect_style(fill, stroke, rounded=rounded, font_size=size, stroke_width=stroke_width)) def matrix3(d: Drawio, parent: str, x: float, y: float, size: float, color: str, *, labels: bool = True) -> None: cell = size / 3 for row in range(3): for col in range(3): value = SPATIAL[row * 3 + col] if labels else "" d.vertex(parent, x + col * cell, y + row * cell, cell, cell, value, rect_style(color, "#ffffff", rounded=False, font_size=7, stroke_width=1)) d.vertex(parent, x, y, size, size, "", "rounded=0;whiteSpace=wrap;html=1;fillColor=none;" f"strokeColor={INK};strokeWidth=1;") def source_panel(d: Drawio, parent: str) -> None: add_text(d, parent, 34, 132, 230, 24, "Input patch X[:,C,3,3]", 12, bold=True) for channel, color in enumerate(INPUT): x = 34 + channel * 70 matrix3(d, parent, x, 166, 48, color) add_text(d, parent, x, 216, 48, 18, f"C{channel}", 9, color=color, align="center", bold=True) add_text(d, parent, 340, 132, 380, 24, "Original W[O,C,3,3] (OIHW)", 12, bold=True, align="center") for output, outline in enumerate(OUTPUT): x = 340 + output * 96 add_box(d, parent, x, 158, 88, 78, "", fill="#ffffff", stroke=outline, stroke_width=2) for channel, color in enumerate(WEIGHT): gx = x + 7 + channel * 25 matrix3(d, parent, gx, 181, 19, color, labels=False) add_text(d, parent, gx, 163, 19, 16, f"C{channel}", 7, color=color, align="center", bold=True) add_text(d, parent, x, 216, 88, 18, f"filter O{output}", 9, color=outline, align="center", bold=True) def order_sequence(order: str) -> list[tuple[int, str]]: if order == "raptor": return [(channel, spatial) for channel in range(3) for spatial in SPATIAL] if order == "pimcomp": return [(channel, spatial) for spatial in SPATIAL for channel in range(3)] raise ValueError(order) def order_vector(d: Drawio, parent: str, y: float, order: str, *, weight: bool, label: str) -> None: x, width, height = 125, 595, 28 sequence = order_sequence(order) cell_w = width / len(sequence) colors = WEIGHT if weight else INPUT for index, (channel, spatial) in enumerate(sequence): value = f"w{spatial}" if weight else (spatial if order == "raptor" else f"C{channel}") font_color = "#ffffff" if weight else INK d.vertex(parent, x + index * cell_w, y, cell_w, height, value, rect_style(colors[channel], "#ffffff", rounded=False, font_size=7, font_color=font_color)) d.vertex(parent, x, y, width, height, "", f"rounded=0;fillColor=none;strokeColor={INK};strokeWidth=1;") add_text(d, parent, 35, y, 82, height, label, 8, color=MUTED, align="right", bold=True) if order == "raptor": for channel in range(3): add_text(d, parent, x + channel * width / 3, y - 18, width / 3, 16, f"C{channel}: row-major 00→01→02→10→…→22", 8, color=colors[channel], align="center", bold=True) else: for spatial_index, spatial in enumerate(SPATIAL): add_text(d, parent, x + spatial_index * width / 9, y - 18, width / 9, 16, f"({spatial}) C0,C1,C2", 7, color=MUTED, align="center", bold=True) def node(d: Drawio, parent: str, x: float, y: float, w: float, h: float, label: str, *, fill: str = PALE, stroke: str = RAPTOR) -> str: return add_box(d, parent, x, y, w, h, label, fill=fill, stroke=stroke, size=10, stroke_width=2) def layout_strip(d: Drawio, parent: str, x: float, y: float, w: float, h: float, order: str, colors: tuple[str, ...], *, channel: int | None = None, repeat: int = 1) -> None: sequence = ([channel] * 9 if channel is not None else [item[0] for item in order_sequence(order)]) row_h = h / repeat for copy in range(repeat): cell_w = w / len(sequence) for index, color_index in enumerate(sequence): d.vertex(parent, x + index * cell_w, y + copy * row_h, cell_w, row_h, "", rect_style(colors[color_index], "#ffffff", rounded=False, font_size=1)) d.vertex(parent, x, y, w, h, "", f"rounded=0;fillColor=none;strokeColor={INK};strokeWidth=1;") def layout_node(d: Drawio, parent: str, x: float, y: float, w: float, h: float, label: str, *, order: str | None = None, colors: tuple[str, ...] = INPUT, channel: int | None = None, repeat: int = 1, stroke: str = RAPTOR) -> str: result = node(d, parent, x, y, w, h, "", fill="#ffffff", stroke=stroke) label_height = h - (28 if order is not None else 10) add_text(d, parent, x + 6, y + 5, w - 12, label_height, label, 10, align="center", bold=True) if order is not None: layout_strip(d, parent, x + 8, y + h - 19, w - 16, 12, order, colors, channel=channel, repeat=repeat) return result def scene_linear(d: Drawio, parent: str, items: tuple[tuple[str, dict | None], ...], *, y: float = 320) -> None: margin, gap = 42, 34 width = (TILE - 2 * margin - gap * (len(items) - 1)) / len(items) ids = [] for index, (label, layout) in enumerate(items): x = margin + index * (width + gap) ids.append(layout_node(d, parent, x, y, width, 92, label, **(layout or {}))) for left, right in zip(ids, ids[1:]): d.edge(parent, left, right) def scene_batch(d: Drawio, parent: str) -> None: for row in range(3): y = 260 + row * 110 patch = layout_node(d, parent, 42, y, 210, 72, f"patches p{row}, p{row + 3}, … [1×K]", order="pimcomp", colors=INPUT, stroke=PIMCOMP) weights = layout_node(d, parent, 302, y, 220, 72, f"replica R{row}: Wflat [K×O]", order="pimcomp", colors=WEIGHT, stroke=PIMCOMP) result = node(d, parent, 610, y, 106, 72, f"Yp\n[1×O]", fill=OUTPUT[row], stroke=PIMCOMP) d.edge(parent, patch, weights, "×") d.edge(parent, weights, result) def scene_depthwise(d: Drawio, parent: str) -> None: for channel in range(3): y = 260 + channel * 110 patch = layout_node(d, parent, 46, y, 190, 72, f"patch C{channel} [1×9]", order="raptor", colors=INPUT, channel=channel) kernel = layout_node(d, parent, 300, y, 210, 72, f"W[C{channel},0,:,:] [9×1]", order="raptor", colors=WEIGHT, channel=channel) result = node(d, parent, 578, y, 136, 72, f"Y channel {channel}", fill=OUTPUT[channel]) d.edge(parent, patch, kernel, "×") d.edge(parent, kernel, result) def scene_c_tiled(d: Drawio, parent: str) -> None: for row, outputs in enumerate(((0, 1), (2, 3))): y = 280 + row * 130 patch = layout_node(d, parent, 42, y, 190, 84, "same full patch [1×K]", order="raptor", colors=INPUT) weights = layout_node( d, parent, 292, y, 244, 84, f"Wflat[:,O{outputs[0]}:O{outputs[-1] + 1}]\n[K×2], all K rows", order="raptor", colors=WEIGHT, stroke=OUTPUT[outputs[0]]) result = node(d, parent, 610, y, 106, 84, f"Y tile {row}\n[1×2]", fill=OUTPUT[outputs[0]]) d.edge(parent, patch, weights, "×") d.edge(parent, weights, result) add_text(d, parent, 250, 552, 260, 22, "concatenate tile 0 | tile 1 along O", 10, color=MUTED, align="center", bold=True) def scene_k_tiled(d: Drawio, parent: str) -> None: for row in range(3): y = 250 + row * 100 patch = layout_node(d, parent, 42, y, 188, 70, f"patch Ki{row}: C{row} [1×9]", order="raptor", colors=INPUT, channel=row) weights = layout_node(d, parent, 298, y, 224, 70, f"Wflat[Ki{row},:] [9×O]", order="raptor", colors=WEIGHT, channel=row) partial = node(d, parent, 596, y, 120, 70, f"partial {row}\n[1×O]", fill=PALE) d.edge(parent, patch, weights, "×") d.edge(parent, weights, partial) node(d, parent, 300, 570, 160, 46, "VADD Σ → Y [1×O]", fill="#ffffff") add_text(d, parent, 470, 578, 238, 28, "partial 0 + partial 1 + partial 2", 9, color=MUTED, align="center", bold=True) def scene_2d(d: Drawio, parent: str) -> None: for row in range(3): y = 250 + row * 100 patch = layout_node(d, parent, 38, y, 160, 70, f"patch Ki{row} [1×9]", order="raptor", colors=INPUT, channel=row) for col in range(2): layout_node(d, parent, 270 + col * 230, y, 180, 70, f"× Wflat[Ki{row},Oj{col}] [9×2]", order="raptor", colors=WEIGHT, channel=row, stroke=OUTPUT[col * 2]) add_text(d, parent, 270, 566, 440, 24, "Σ tile rows along K; concatenate tile columns along O", 10, color=MUTED, align="center", bold=True) def scene_row_strip_c(d: Drawio, parent: str) -> None: patch = layout_node(d, parent, 42, 330, 206, 88, "row-window patch for x [1×K]", order="raptor", colors=INPUT) for col, outputs in enumerate(((0, 1), (2, 3))): y = 270 + col * 150 weights = layout_node( d, parent, 310, y, 238, 88, f"lane r × O{outputs[0]}:O{outputs[-1] + 1}\nWflat[:,Oj] [K×2]", order="raptor", colors=WEIGHT, stroke=OUTPUT[outputs[0]]) result = node(d, parent, 614, y, 104, 88, f"Yj\n[1×2]", fill=OUTPUT[outputs[0]]) d.edge(parent, patch, weights, "reuse ×") d.edge(parent, weights, result) node(d, parent, 500, 570, 190, 46, "concat O → output row [1×4]") def operation_scene(d: Drawio, parent: str, scene: str) -> None: add_text(d, parent, 30, 204, 700, 22, "LOWERED COMPUTE", 11, color=MUTED, align="center", bold=True) if scene == "pimcomp_element": scene_linear(d, parent, ( ("input cycle p\npatchPIM [1×K]", {"order": "pimcomp", "colors": INPUT, "stroke": PIMCOMP}), ("mapped Array Group\nWflatPIM [K×O]", {"order": "pimcomp", "colors": WEIGHT, "stroke": PIMCOMP}), ("Yp [1×O]", {"stroke": PIMCOMP}), )) elif scene == "pimcomp_batch": scene_batch(d, parent) elif scene == "legacy": scene_linear(d, parent, ( ("global im2col\n[P×K]", {"order": "raptor", "colors": INPUT}), ("WflatR\n[K×O]", {"order": "raptor", "colors": WEIGHT}), ("Y rows\n[P×O]", None), )) elif scene == "packed": scene_linear(d, parent, ( ("q patch rows\n[q×K]", {"order": "raptor", "colors": INPUT, "repeat": 2}), ("pack → [1×qK]", {"order": "raptor", "colors": INPUT, "repeat": 2}), ("diag(WflatR×q)\n[qK×qO]", {"order": "raptor", "colors": WEIGHT, "repeat": 2}), ("packed Y\n[1×qO]", None), )) elif scene == "streamed_patch": scene_linear(d, parent, ( ("gather one patch", None), ("bounded scratch\n[1×K]", {"order": "raptor", "colors": INPUT}), ("WflatR\n[K×O]", {"order": "raptor", "colors": WEIGHT}), ("Yp\n[1×O]", None), )) elif scene == "streamed_packed": scene_linear(d, parent, ( ("q patches\n[q×K]", {"order": "raptor", "colors": INPUT, "repeat": 2}), ("packedScratch\n[1×qK]", {"order": "raptor", "colors": INPUT, "repeat": 2}), ("diag(WflatR×q)\n[qK×qO]", {"order": "raptor", "colors": WEIGHT, "repeat": 2}), ("packed Y\n[1×qO]", None), )) elif scene == "depthwise": scene_depthwise(d, parent) elif scene == "c_tiled": scene_c_tiled(d, parent) elif scene == "k_tiled": scene_k_tiled(d, parent) elif scene == "tiled_2d": scene_2d(d, parent) elif scene == "row_strip": scene_linear(d, parent, ( ("lane r row windows", None), ("for x: patch\n[1×K]", {"order": "raptor", "colors": INPUT}), ("K-sliced Wflat\n[Ki×O]", {"order": "raptor", "colors": WEIGHT}), ("Σ partials →\noutput row", None), )) elif scene == "row_strip_c": scene_row_strip_c(d, parent) else: raise ValueError(scene) def reference_body(d: Drawio, parent: str) -> None: add_box(d, parent, 24, 252, 712, 158, "", fill="#ffffff", stroke=PIMCOMP) add_text(d, parent, 40, 260, 680, 22, "PIMCOMP: spatial-major, row-wise positions; C interleaved", 11, color=PIMCOMP, align="center", bold=True) order_vector(d, parent, 306, "pimcomp", weight=False, label="Input patch") order_vector(d, parent, 366, "pimcomp", weight=True, label="Matching W") add_text(d, parent, 40, 394, 680, 16, "k=((kh·Kw)+kw)·Cin+c — c changes fastest", 9, color=PIMCOMP, align="center", bold=True) add_box(d, parent, 24, 424, 712, 158, "", fill="#ffffff", stroke=RAPTOR) add_text(d, parent, 40, 432, 680, 22, "RAPTOR: channel-major; each 3×3 plane is row-major", 11, color=RAPTOR, align="center", bold=True) order_vector(d, parent, 478, "raptor", weight=False, label="Input patch") order_vector(d, parent, 538, "raptor", weight=True, label="Matching W") add_text(d, parent, 40, 566, 680, 16, "k=((c·Kh)+kh)·Kw+kw — kw changes fastest", 9, color=RAPTOR, align="center", bold=True) add_box(d, parent, 120, 590, 520, 42, "", fill=PALE, stroke=GRID) add_text(d, parent, 132, 594, 496, 34, "Shade key: light = activation; dark = matching weight row. " "Depthwise: independent C0/C1/C2 row-major Kc=9 vectors.", 9, color=MUTED, align="center", bold=True) def layout_reference(d: Drawio, parent: str, tile: Tile, accent: str) -> None: if tile.scene == "depthwise": layout = "independent row-major Kc=9 per channel" elif tile.owner == "PIMCOMP": layout = "PIMCOMP spatial-major K order" else: layout = "RAPTOR channel-major K order" add_box(d, parent, 24, 140, 712, 42, "", fill=PALE, stroke=accent) add_text(d, parent, 38, 146, 684, 30, f"LAYOUT → see REFERENCE tile: {layout}", 10, color=accent, align="center", bold=True) def algorithm_card(d: Drawio, parent: str, tile: Tile, accent: str) -> None: add_box(d, parent, 24, 638, 712, 102, "", fill="#ffffff", stroke=accent) add_text(d, parent, 40, 646, 90, 34, "ALGORITHM", 9, color=accent, bold=True) add_text(d, parent, 132, 644, 588, 38, tile.algorithm, 10) add_text(d, parent, 40, 690, 90, 34, "MATH", 9, color=accent, bold=True) add_text(d, parent, 132, 686, 588, 42, tile.formula, 10, color=accent, bold=True) def render_tile(d: Drawio, tile: Tile, index: int) -> None: col, row = index % COLS, index // COLS parent = d.group(col * (TILE + GAP), row * (TILE + GAP), tile.slug) accent = {"REFERENCE": REFERENCE, "PIMCOMP": PIMCOMP, "RAPTOR": RAPTOR}[tile.owner] add_box(d, parent, 0, 0, TILE, TILE, "", fill="#fbfcff", stroke=accent, stroke_width=3) add_box(d, parent, 24, 20, 106, 28, tile.owner, fill=accent, stroke=accent, size=10) add_text(d, parent, 24, 56, 712, 36, tile.title, 22, bold=True) add_text(d, parent, 24, 92, 712, 30, tile.subtitle, 11, color=MUTED) if tile.scene == "reference": source_panel(d, parent) reference_body(d, parent) else: layout_reference(d, parent, tile, accent) operation_scene(d, parent, tile.scene) algorithm_card(d, parent, tile, accent) def validate(root: ET.Element) -> None: ids = [cell.get("id") for cell in root.findall(".//mxCell")] assert len(ids) == len(set(ids)), "draw.io cell IDs must be unique" assert {"0", "1"}.issubset(ids), "draw.io root cells are required" groups = [cell for cell in root.findall(".//mxCell") if cell.get("style") == "group;connectable=0;"] assert len(groups) == len(TILES), "one editable group is required per tile" def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("-o", "--output", type=Path, default=Path("conv_lowering_tiles.drawio")) args = parser.parse_args() diagram = Drawio() for index, tile in enumerate(TILES): render_tile(diagram, tile, index) validate(diagram.mxfile) diagram.write(args.output) print(args.output) return 0 if __name__ == "__main__": raise SystemExit(main())